SAMPLE DELIVERABLE

The plan comes before the tag.

Most broken analytics setups aren’t broken because someone wrote a bad tag. They’re broken because nobody wrote down what was supposed to be measured — so three people implemented three different versions of purchase over two years, and now nobody can say which number is real.

This is the document I write before I touch anything. It’s generic — no client, no real figures — but the structure, the naming rules and the QA criteria are exactly what I use. If you’ve never been handed one of these, this is what you should be asking for.

GA4 · GTM · SERVER-SIDE GTM · META CAPI · GOOGLE ADS EC · CONSENT MODE V212 MIN READ
CONTENTS
  1. 01SCOPE
  2. 02QUESTIONS
  3. 03ARCHITECTURE
  4. 04CONVENTIONS
  5. 05DATALAYER
  6. 06GA4
  7. 07CONSENT
  8. 08DESTINATIONS
  9. 09BUILD
  10. 10QA
  11. 11ACCESS
01 / SCOPE

What this document does

Every tag, trigger, variable and destination that gets built maps back to a row in this plan. If it isn’t in here, it doesn’t get implemented. If it is, it gets QA’d against section 10 before handover.

Four people use it, for four different reasons:

WhoWhat they use it for
MarketingConfirms the questions in section 02 are the ones they actually need answered.
DevelopersImplements the dataLayer contract in section 05, exactly as written. That’s the only section they need.
MeBuilds the web and server containers, GA4 configuration and destination mapping from sections 06–09.
Sign-offRuns the checklist in section 10. Nothing ships until every row passes.
02 / QUESTIONS

Tracking exists to answer something

Every event traces back to at least one business question. Anything that answers no question gets left out — which is most of what comes bundled in a template.

#QuestionMetricEvents
Q1Which channels actually produce revenue, not just sessions?Revenue and ROAS by session source / mediumpurchase
Q2Where do people drop out of checkout?Step-to-step conversion rateview_cartbegin_checkoutadd_shipping_infoadd_payment_infopurchase
Q3Which products get viewed but never added?View-to-add rate by itemview_item, add_to_cart
Q4Is one ad platform being credited fairly against another?Platform-reported vs GA4 vs backend orderspurchase, all destinations
Q5Does on-site search find anything useful?Search → view → purchase ratesearch, view_item, purchase
Q6What’s a returning customer worth versus a first-time buyer?Revenue per user by customer typepurchase + user properties

Deliberately out of scope: scroll depth, time-on-page engagement scoring, and any event that exists only because a template included it. They answer none of the above, and they burn through GA4’s 500-distinct-event limit on noise.

03 / ARCHITECTURE

Two containers, one event ID

Browser-side handles collection and consent. The server container handles distribution. This is what makes the deduplication in section 08 possible.

EVENT FLOWDIAGRAM
Website
   |
   +-- window.dataLayer.push({ ... })          <- the section 05 contract
           |
           v
   [ GTM WEB CONTAINER ]
           |  consent state (Consent Mode v2)
           |  generates event_id — shared downstream
           |
           v   first-party HTTPS  ->  sgtm.yourdomain.com
   [ GTM SERVER CONTAINER ]
           |
           +--> GA4
           +--> Meta Conversions API     (event_id dedup with the browser pixel)
           +--> Google Ads               (enhanced conversions for web)
           +--> BigQuery                 (raw event archive)

Why server-side, in four lines

First-party cookie set by the server.
Safari’s ITP caps JavaScript-set cookies at seven days. A server-set cookie on your own subdomain survives far longer, so returning-visitor attribution stops collapsing into “direct”.
Ad-blocker resilience.
Requests go to a subdomain of your own site, not to a domain on a public blocklist.
One place to control what leaves the site.
PII redaction, parameter allow-listing and consent enforcement happen server-side, where marketing can’t accidentally undo them.
Deduplication.
The browser pixel and the server event carry the same event_id, so the platform counts one conversion instead of two.

Environments: staging and production run separate containers, separate server endpoints and separate GA4 properties. Nothing is tested in the property the board reads.

04 / CONVENTIONS

Rules, not preferences

Half the value of a tracking plan is that the next person can guess the name of something correctly.

ObjectConventionExample
Eventsnake_case, GA4 recommended name where one existsadd_to_cart, generate_lead
Parametersnake_case, no vendor prefixesitem_list_name, shipping_tier
GTM tagPlatform - Type - DetailGA4 - Event - purchase
GTM triggerCE - <event_name>CE - add_to_cart
GTM variableDLV - <path> / CJS - <purpose>DLV - ecommerce.value
Server clientClient - <source>Client - GA4

Non-negotiable

No PII in GA4. Ever.
No email, phone, name or full address in any parameter or in the page path. Emails are SHA-256 hashed before they reach Meta or Google Ads, and never reach GA4 at all.
No custom event where a GA4 recommended event exists.
Recommended events unlock built-in reports. Custom names don’t.
currency is always a three-letter ISO code. value is always a number
— never a string, never with a symbol.
Every new event needs a row in this document before it gets built.
The document is the record, not the container.
No tag is published without a preview-mode screenshot
attached to the change ticket.
05 / DATALAYER

The developer contract

Everything downstream depends on these pushes landing with these exact key names.

  • Declare window.dataLayer = window.dataLayer || []; before the GTM snippet, in the <head>.
  • Clear the previous ecommerce object before every ecommerce push: window.dataLayer.push({ ecommerce: null });. Without this, item arrays bleed between events.
  • Push on the actual user action, not on page load.
  • value equals the sum of price × quantity for the items in that event, excluding shipping and tax unless stated.
  • Fire user_data as soon as identity is known, before the conversion event that depends on it.

Event inventory

EventFires whenKey parametersAnswers
view_item_listCollection or search results renderitems[], item_list_id, item_list_nameQ3, Q5
select_itemProduct card clickeditems[], item_list_nameQ3
view_itemProduct detail page rendersitems[], value, currencyQ3
add_to_cartAdd confirmed by the backenditems[], value, currencyQ2, Q3
remove_from_cartItem removeditems[], value, currencyQ2
view_cartCart drawer or /cart openeditems[], value, currencyQ2
begin_checkoutCheckout initiateditems[], value, currency, couponQ2
add_shipping_infoShipping method selecteditems[], value, shipping_tierQ2
add_payment_infoPayment method selecteditems[], value, payment_typeQ2
purchaseOrder confirmationtransaction_id, value, tax, shipping, currency, coupon, items[]Q1, Q2, Q4, Q6
refundRefund webhook, server-side onlytransaction_id, value, currencyQ1
searchOn-site search submittedsearch_term, results_countQ5
generate_leadForm successform_id, form_name, lead_typeQ6
login / sign_upAuth successmethodQ6
user_dataIdentity becomes knownuser_id, hashed identifiersQ4, Q6

purchase — the one that matters

If this is wrong, every number downstream is wrong.

PURCHASEJAVASCRIPT
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
  event: 'purchase',
  event_id: 'ord_10482_1730812345',   // stable and unique per order
  ecommerce: {
    transaction_id: '10482',          // order number as a string, not a UUID
    value: 128.50,                    // items only, ex. shipping and tax
    tax: 21.42,
    shipping: 4.95,
    currency: 'GBP',
    coupon: 'WELCOME10',
    items: [{
      item_id: 'SKU-4471',            // must match the merchant feed ID
      item_name: 'Example Product 30ml',
      item_brand: 'Example Brand',
      item_category: 'Category',
      item_category2: 'Subcategory',
      item_variant: '30ml',
      price: 64.25,                   // unit price, ex. tax
      quantity: 2,
      index: 0
    }]
  }
});

user_data — identity, hashed before it leaves the browser

Raw email never enters the dataLayer.

USER_DATAJAVASCRIPT
window.dataLayer.push({
  event: 'user_data',
  user_id: 'cust_88213',              // stable CRM ID, not the session ID
  user_data: {
    sha256_email_address: '<sha256(lowercase(trim(email)))>',
    sha256_phone_number: '<sha256(E.164 phone)>',
    address: {
      sha256_first_name: '<sha256>',
      sha256_last_name:  '<sha256>',
      postal_code: 'sw1a1aa',         // lowercased, spaces stripped
      country: 'GB'
    }
  },
  customer_type: 'returning',         // new | returning | subscriber
  logged_in: true
});
06 / GA4

Configuration

Key events

EventCountingValueWhy
purchaseEveryFrom eventPrimary revenue outcome
begin_checkoutEveryFrom eventMid-funnel optimisation signal
generate_leadOnce per sessionStaticStops form retries double-counting
subscription_startedEveryFrom eventSeparated from one-off purchase for Q6

Custom definitions

GA4 discards any parameter that isn’t registered, so this table isn’t optional.

NameParameterScopePurpose
Customer typecustomer_typeUserNew vs returning vs subscriber (Q6)
Logged inlogged_inUserSegment authenticated behaviour
Checkout stepcheckout_stepEventFunnel exploration (Q2)
Shipping tiershipping_tierEventDoes free shipping move AOV
Payment typepayment_typeEventPayment-method drop-off
Form nameform_nameEventWhich forms produce leads
Search results countresults_countEventZero-result searches (Q5)
Item list nameitem_list_nameEventMerchandising placement performance

Property settings

  • Data retention at 14 months on event and user data — the maximum, and not the default.
  • Google Signals on, reporting identity Blended.
  • Internal traffic filter active and set to Exclude, with an override for remote staff.
  • Unwanted referrals covering the payment gateway and checkout subdomain, so PayPal stops appearing as the source of your revenue.
  • Cross-domain measurement across the main domain and checkout subdomain.
  • BigQuery export on, daily plus streaming.
  • Data-driven attribution, 90-day acquisition lookback.
08 / DESTINATIONS

And why the numbers won’t match

Meta Conversions API

The browser pixel and the server event are both sent deliberately. Meta reconciles them into one conversion because they share an identifier — and that only works if the ID is generated once and reused.

RequirementImplementation
event_idGenerated once in the web container, sent to the pixel as eventID and to the server as event_id. Derived from the order number so it survives a refresh of the thank-you page.
event_nameIdentical string on both paths, casing included.
action_sourcewebsite on both paths.
event_timeUnix seconds from the same timestamp on both paths.
fbp / fbcRead from the _fbp and _fbc cookies and forwarded server-side. Without fbc, click-through attribution is lost.
User dataHashed email, phone, first and last name, postal code, country, plus client IP and user agent forwarded from the server.

Google Ads enhanced conversions

  • Conversion linker on all pages so the GCLID lands in a first-party cookie.
  • Enhanced conversions sent through the server container using the hashed identifiers from user_data — not by scraping the DOM of the confirmation page.
  • Deduplicated by order ID so a refreshed confirmation page can’t inflate conversions.
  • Customer Data Terms accepted in the Google Ads account before go-live. Enhanced conversions silently do nothing until they are.

Expected variance

Platforms will never agree exactly. The point of the plan is that the gap becomes explainable instead of mysterious. Agreed tolerances before go-live:

ComparisonAcceptable varianceWhy the gap exists
GA4 purchases vs backend orders≤ 2%Consent denial, ad blockers, bots
GA4 revenue vs backend revenue≤ 2%Currency rounding, refund timing
Meta reported vs GA4 last-clickMaterially higherView-through attribution and a 7-day click window — not an error
Google Ads vs GA4 conversions≤ 10%Attribution model and conversion-window differences
09 / BUILD

The tag sheet

Every tag traces to a section above. Abbreviated here to the ecommerce path.

ContainerTagTriggerConsent required
WebGA4 - ConfigInitialisation — All Pagesanalytics_storage
WebGA4 - Event - view_itemCE - view_itemanalytics_storage
WebGA4 - Event - add_to_cartCE - add_to_cartanalytics_storage
WebGA4 - Event - begin_checkoutCE - begin_checkoutanalytics_storage
WebGA4 - Event - purchaseCE - purchaseanalytics_storage
WebMeta - Pixel - PurchaseCE - purchasead_storage, ad_user_data
WebGoogle Ads - Conversion LinkerAll Pagesad_storage
ServerGA4 - ServerClient - GA4analytics_storage
ServerMeta - CAPI - PurchaseClient - GA4, event = purchasead_storage, ad_user_data
ServerGoogle Ads - Enhanced ConversionClient - GA4, event = purchasead_storage, ad_user_data
ServerBigQuery - Raw Event SinkClient - GA4, all eventsnone — no marketing identifiers written
10 / QA

What sign-off actually means

Nothing is handed over until every row passes on production, verified twice: once by me, once by you on a real order.

Per event

#CheckTool
1Fires exactly once per action — no duplicates on refresh or back-navigationGTM Preview, GA4 DebugView
2Every required parameter present and correctly typedGTM Preview
3items[] populated with real IDs matching the merchant feedGTM Preview
4value equals sum(price × quantity) for the items in the eventManual calculation
5No PII in any GA4-bound parameter or in the page pathNetwork tab
6Event reaches the server container and the intended destinationsServer container Preview
7Tag does not fire when consent is deniedConsent simulation
8Behaves correctly on mobile Safari and in an ad-blocked sessionReal device, uBlock Origin

Reconciliation — the real test

  • Three live test orders: one guest, one logged-in, one with a discount code.
  • For each, transaction_id, value, tax, shipping and item count match the backend order record exactly.
  • After 72 hours, GA4 purchase count and revenue compared against backend orders for the same window, inside the tolerances in section 08.
  • Meta shows the purchase event as deduplicated, with a browser and server share, and a match-quality score reported.
  • Google Ads reports enhanced conversions as active with a matched-conversion rate.
  • No orphan rows in BigQuery — every purchase has a matching order.

Definition of done

  • Every event in section 05 fires correctly in production and appears in GA4.
  • GA4 revenue reconciles to backend revenue inside tolerance over a rolling 7-day window.
  • Meta purchase deduplication confirmed, match quality reported and at target.
  • Google Ads enhanced conversions live and reporting a match rate.
  • Consent Mode v2 verified in both granted and denied states.
  • This document updated to as-built, and the container annotated to match.
  • Handover session recorded — container walkthrough, plus how to add a new event without breaking the plan.
11 / ACCESS

What I need from you

SystemAccess levelNeeded for
Google Tag ManagerPublishWeb container build
Google Analytics 4Editor / AdministratorProperty configuration, custom definitions
Google AdsStandardConversion actions, enhanced conversions
Meta Business ManagerPartner — pixel and datasetPixel and CAPI configuration
Google Cloud projectEditorServer container hosting, BigQuery export
DNSRecord creation onlysgtm subdomain CNAME
Site / theme codeStaging plus a deploy pathdataLayer implementation
CMPAdminConsent Mode v2 default state

Typical shape of the work: discovery and this document, 3–4 days. dataLayer implementation, 5–7 days, on your developers. Container build, 4–5 days. QA and reconciliation, 3–4 days. Handover, 1 day, with 30 days of support after.

This is week one.

Every engagement starts with a version of this document, written for your stack and your questions rather than a generic one. If your current setup doesn’t have one, that’s usually the reason nobody trusts the numbers.

This is what you get in week one. If your setup needs it, the call is free.