Skill Tracer
Skills › Marketing Skills › conversion-tracking › browser-side.md

browser-side.md

424 lines · skills/conversion-tracking/references/browser-side.md at 88c79f6

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Browser-side tracking: the complete free path
2
3Load this to install pixels and tags directly, or via Google Tag Manager with this skill's recipe system. This path is free, self-serve, and complete; for analytics-only destinations it's a sound default, though server-side still recovers the conversions ad blockers stop and gives more accurate numbers. When the destination is an ad platform, state its ceiling honestly, and the ceiling is two things. Some conversions, typically 10–30% depending on audience, never get credited to the ad (ad blockers, Safari's cookie limits, and cross-device journeys where the ad is clicked on one device and the conversion happens on another; magnitudes in `discrepancies.md`), and a default tag carries only the click ID, IP address, and user agent. Browser tags can also send hashed email and phone (Google Enhanced Conversions for web, Meta advanced matching, TikTok `ttq.identify`); turn those on first, knowing they share the tag's losses. Say both, so the user's choice is informed.
4
5Division of labour with the neighbouring references: this file is the on-page code. Account-side configuration (creating conversion actions, datasets, and goals) and per-platform diagnostics live in `google-ads.md`, `meta.md`, and `other-platforms.md`. The moment detection that should trigger everything here lives in `conversion-moments.md`. Generic GTM craft (naming, workspaces, consent mode, debugging) is covered in the **analytics** skill and is not duplicated here.
6
7Whatever is installed below, the non-negotiables apply: fire on confirmed success (never on a button click), and verify arrival in the platform, not just firing in the browser.
8

Browser-side tracking: the complete free path

Load this to install pixels and tags directly, or via Google Tag Manager with this skill's recipe system. This path is free, self-serve, and complete; for analytics-only destinations it's a sound default, though server-side still recovers the conversions ad blockers stop and gives more accurate numbers. When the destination is an ad platform, state its ceiling honestly, and the ceiling is two things. Some conversions, typically 10–30% depending on audience, never get credited to the ad (ad blockers, Safari's cookie limits, and cross-device journeys where the ad is clicked on one device and the conversion happens on another; magnitudes in discrepancies.md), and a default tag carries only the click ID, IP address, and user agent. Browser tags can also send hashed email and phone (Google Enhanced Conversions for web, Meta advanced matching, TikTok ttq.identify); turn those on first, knowing they share the tag's losses. Say both, so the user's choice is informed.

Division of labour with the neighbouring references: this file is the on-page code. Account-side configuration (creating conversion actions, datasets, and goals) and per-platform diagnostics live in google-ads.md, meta.md, and other-platforms.md. The moment detection that should trigger everything here lives in conversion-moments.md. Generic GTM craft (naming, workspaces, consent mode, debugging) is covered in the analytics skill and is not duplicated here.

Whatever is installed below, the non-negotiables apply: fire on confirmed success (never on a button click), and verify arrival in the platform, not just firing in the browser.

Raw lines 1 to 8
9## Platform Pixels Overview
11| Platform | Pixel/Tag Name | Events API | Key Events |
12|----------|---------------|:----------:|------------|
13| **Google Ads** | Google tag (gtag.js) | Data Manager API (offline and enhanced conversions for leads uploads) | purchase, sign_up, generate_lead |
14| **Meta** | Meta Pixel + CAPI | Conversions API | Purchase, Lead, ViewContent, AddToCart |
15| **LinkedIn** | Insight Tag | Conversions API | conversion (URL or event-based) |
16| **TikTok** | TikTok Pixel | Events API | Purchase, ViewContent, AddToCart, CompleteRegistration |
17| **Twitter/X** | X Pixel | Conversion API | Purchase, SignUp, Download |
19---

Platform Pixels Overview

PlatformPixel/Tag NameEvents APIKey Events
Google AdsGoogle tag (gtag.js)Data Manager API (offline and enhanced conversions for leads uploads)purchase, sign_up, generate_lead
MetaMeta Pixel + CAPIConversions APIPurchase, Lead, ViewContent, AddToCart
LinkedInInsight TagConversions APIconversion (URL or event-based)
TikTokTikTok PixelEvents APIPurchase, ViewContent, AddToCart, CompleteRegistration
Twitter/XX PixelConversion APIPurchase, SignUp, Download

Raw lines 9 to 20
21## Google Ads
23### Install the Google tag
25Add to every page, in `<head>`:
27```html
28<script async src="https://www.googletagmanager.com/gtag/js?id=AW-XXXXXXXXX"></script>
29<script>
30 window.dataLayer = window.dataLayer || [];
31 function gtag(){dataLayer.push(arguments);}
32 gtag('js', new Date());
33 gtag('config', 'AW-XXXXXXXXX');
34</script>
35```
37Replace `AW-XXXXXXXXX` with your Conversion ID, shown in the conversion action's tag setup in Google Ads (Goals, then Conversions, then Summary).

Install the Google tag

Add to every page, in <head>:

<script async src="https://www.googletagmanager.com/gtag/js?id=AW-XXXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'AW-XXXXXXXXX');
</script>

Replace AW-XXXXXXXXX with your Conversion ID, shown in the conversion action's tag setup in Google Ads (Goals, then Conversions, then Summary).

Raw lines 23 to 38
39### Set up conversion actions
41In Google Ads, go to Goals, then Conversions, then Summary, and click + Create conversion action. Use these settings:
43| Conversion | Category | Value | Count |
44|-----------|----------|-------|-------|
45| Purchase | Purchase | Dynamic (order value) | Every |
46| Sign up / Lead | Sign up | Fixed ($X estimated value) | One |
47| Demo request | Submit lead form (or Book appointment for a booked demo) | Fixed ($X estimated value) | One |
48| Free trial start | Sign up | Fixed ($X estimated value) | One |

Set up conversion actions

In Google Ads, go to Goals, then Conversions, then Summary, and click + Create conversion action. Use these settings:

ConversionCategoryValueCount
PurchasePurchaseDynamic (order value)Every
Sign up / LeadSign upFixed ($X estimated value)One
Demo requestSubmit lead form (or Book appointment for a booked demo)Fixed ($X estimated value)One
Free trial startSign upFixed ($X estimated value)One
Raw lines 39 to 49
50### Fire conversion events
52```javascript
53// Purchase
54gtag('event', 'conversion', {
55 'send_to': 'AW-XXXXXXXXX/CONVERSION_LABEL',
56 'value': 99.00,
57 'currency': 'USD',
58 'transaction_id': 'ORDER-123'
59});
61// Lead / Sign up
62gtag('event', 'conversion', {
63 'send_to': 'AW-XXXXXXXXX/CONVERSION_LABEL',
64 'value': 50.00,
65 'currency': 'USD'
66});
67```

Fire conversion events

// Purchase
gtag('event', 'conversion', {
  'send_to': 'AW-XXXXXXXXX/CONVERSION_LABEL',
  'value': 99.00,
  'currency': 'USD',
  'transaction_id': 'ORDER-123'
});

// Lead / Sign up
gtag('event', 'conversion', {
  'send_to': 'AW-XXXXXXXXX/CONVERSION_LABEL',
  'value': 50.00,
  'currency': 'USD'
});
Raw lines 50 to 68
69### Enhanced Conversions
71Sends hashed first-party data (email, phone) to improve attribution after cookie restrictions. Enable in Google Ads > Goals > Settings > Enhanced conversions.
73```javascript
74gtag('set', 'user_data', {
75 'email': 'user@example.com', // auto-hashed by gtag
76 'phone_number': '+11234567890'
77});
78```

Enhanced Conversions

Sends hashed first-party data (email, phone) to improve attribution after cookie restrictions. Enable in Google Ads > Goals > Settings > Enhanced conversions.

gtag('set', 'user_data', {
  'email': 'user@example.com',      // auto-hashed by gtag
  'phone_number': '+11234567890'
});
Raw lines 69 to 79
80### The GTM route and the recipe system
82With GTM installed, prefer importing a prebuilt container over hand-building tags. This skill ships importable recipes for 22 form and booking tools, 4 universal patterns, and 6 destinations:
84```
85python3 ../scripts/build_recipe.py --tool gravity-forms --send google-ads \
86 --conversion-id 123456789 --conversion-label AbCdEfGhIj -o import-me.json
87```
89The output file bundles the tool's detection listener, a custom event trigger on the canonical dataLayer event, a Conversion Linker, and the destination's conversion tag, all wired. Import it in GTM (Admin, then Import Container, choose the existing workspace, choose **Merge** with rename), review the preview diff, test in Preview mode, then publish. Tool slugs and event names are in `../assets/gtm-recipes/event-map.json`; the step-by-step walkthrough with expected outputs and failure modes is in `google-ads.md` Part A.
91Without GTM, paste the tool's detection snippet from `../assets/snippets/` into the site head via the platform's custom code setting, then wire the platform tag to the dataLayer event it pushes.

The GTM route and the recipe system

With GTM installed, prefer importing a prebuilt container over hand-building tags. This skill ships importable recipes for 22 form and booking tools, 4 universal patterns, and 6 destinations:

python3 ../scripts/build_recipe.py --tool gravity-forms --send google-ads \
    --conversion-id 123456789 --conversion-label AbCdEfGhIj -o import-me.json

The output file bundles the tool's detection listener, a custom event trigger on the canonical dataLayer event, a Conversion Linker, and the destination's conversion tag, all wired. Import it in GTM (Admin, then Import Container, choose the existing workspace, choose Merge with rename), review the preview diff, test in Preview mode, then publish. Tool slugs and event names are in ../assets/gtm-recipes/event-map.json; the step-by-step walkthrough with expected outputs and failure modes is in google-ads.md Part A.

Without GTM, paste the tool's detection snippet from ../assets/snippets/ into the site head via the platform's custom code setting, then wire the platform tag to the dataLayer event it pushes.

Raw lines 80 to 92
Runs code python3 ../scripts/build_recipe.py :85
93## Meta (Facebook/Instagram)

Meta (Facebook/Instagram)

Raw lines 93 to 94
95### Install the Meta Pixel
97Add to every page, in `<head>`:
99```html
100<script>
101 !function(f,b,e,v,n,t,s)
102 {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
103 n.callMethod.apply(n,arguments):n.queue.push(arguments)};
104 if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
105 n.queue=[];t=b.createElement(e);t.async=!0;
106 t.src=v;s=b.getElementsByTagName(e)[0];
107 s.parentNode.insertBefore(t,s)}(window, document,'script',
108 'https://connect.facebook.net/en_US/fbevents.js');
109 fbq('init', 'YOUR_PIXEL_ID');
110 fbq('track', 'PageView');
111</script>
112```
114Replace `YOUR_PIXEL_ID` from Meta Events Manager.

Install the Meta Pixel

Add to every page, in <head>:

<script>
  !function(f,b,e,v,n,t,s)
  {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
  n.callMethod.apply(n,arguments):n.queue.push(arguments)};
  if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
  n.queue=[];t=b.createElement(e);t.async=!0;
  t.src=v;s=b.getElementsByTagName(e)[0];
  s.parentNode.insertBefore(t,s)}(window, document,'script',
  'https://connect.facebook.net/en_US/fbevents.js');
  fbq('init', 'YOUR_PIXEL_ID');
  fbq('track', 'PageView');
</script>

Replace YOUR_PIXEL_ID from Meta Events Manager.

Raw lines 95 to 115
116### Standard events
118```javascript
119// View a product or key page
120fbq('track', 'ViewContent', {
121 content_name: 'Pro Plan',
122 content_category: 'Pricing',
123 value: 29.00,
124 currency: 'USD'
125});
127// Lead capture (form submit, demo request)
128fbq('track', 'Lead', {
129 content_name: 'Demo Request',
130 value: 50.00,
131 currency: 'USD'
132});
134// Purchase
135fbq('track', 'Purchase', {
136 value: 99.00,
137 currency: 'USD',
138 content_type: 'product',
139 contents: [{ id: 'pro-plan', quantity: 1 }]
140});
142// Add to cart (e-commerce)
143fbq('track', 'AddToCart', {
144 content_ids: ['SKU-123'],
145 content_type: 'product',
146 value: 49.00,
147 currency: 'USD'
148});
149```

Standard events

// View a product or key page
fbq('track', 'ViewContent', {
  content_name: 'Pro Plan',
  content_category: 'Pricing',
  value: 29.00,
  currency: 'USD'
});

// Lead capture (form submit, demo request)
fbq('track', 'Lead', {
  content_name: 'Demo Request',
  value: 50.00,
  currency: 'USD'
});

// Purchase
fbq('track', 'Purchase', {
  value: 99.00,
  currency: 'USD',
  content_type: 'product',
  contents: [{ id: 'pro-plan', quantity: 1 }]
});

// Add to cart (e-commerce)
fbq('track', 'AddToCart', {
  content_ids: ['SKU-123'],
  content_type: 'product',
  value: 49.00,
  currency: 'USD'
});
Raw lines 116 to 150
151### Conversions API (CAPI)
153Server-side tracking that works alongside the pixel. Recommended alongside the pixel since iOS 14 and browser cookie restrictions.
155Set up via:
156- **Direct integration** — send events from your server to Meta's API
157- **Partner integrations** — Shopify, WooCommerce, Segment, etc. have built-in CAPI support
158- **Conversions API Gateway** — Meta's own server, hosted on AWS (EKS or ECS) or GCP, or by a partner host such as Stape or TAGGRS
160Key: send the same events from both pixel (browser) AND CAPI (server), with a shared `event_id` for deduplication.

Conversions API (CAPI)

Server-side tracking that works alongside the pixel. Recommended alongside the pixel since iOS 14 and browser cookie restrictions.

Set up via:

  • Direct integration — send events from your server to Meta's API
  • Partner integrations — Shopify, WooCommerce, Segment, etc. have built-in CAPI support
  • Conversions API Gateway — Meta's own server, hosted on AWS (EKS or ECS) or GCP, or by a partner host such as Stape or TAGGRS

Key: send the same events from both pixel (browser) AND CAPI (server), with a shared event_id for deduplication.

Raw lines 151 to 161
162### Aggregated Event Measurement (historical note)
164Meta announced in May 2023 that it was dropping AEM's 8-event cap and domain-verification requirement, and the AEM configuration tab was gone from Events Manager by mid-2025; guides still describing them are out of date. What remains from the iOS 14 era is modeled and delayed iOS conversions, which belong in expectation-setting, not setup. Domain verification is still good practice for account security and required by some features, just not an AEM gate.

Aggregated Event Measurement (historical note)

Meta announced in May 2023 that it was dropping AEM's 8-event cap and domain-verification requirement, and the AEM configuration tab was gone from Events Manager by mid-2025; guides still describing them are out of date. What remains from the iOS 14 era is modeled and delayed iOS conversions, which belong in expectation-setting, not setup. Domain verification is still good practice for account security and required by some features, just not an AEM gate.

Raw lines 162 to 165
166## LinkedIn
168### Install the Insight Tag
170Add to every page, before `</body>`:
172```html
173<script type="text/javascript">
174 _linkedin_partner_id = "YOUR_PARTNER_ID";
175 window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || [];
176 window._linkedin_data_partner_ids.push(_linkedin_partner_id);
177 (function(l) {
178 if (!l){window.lintrk = function(a,b){window.lintrk.q.push([a,b])};
179 window.lintrk.q=[]}
180 var s = document.getElementsByTagName("script")[0];
181 var b = document.createElement("script");
182 b.type = "text/javascript";b.async = true;
183 b.src = "https://snap.licdn.com/li.lms-analytics/insight.min.js";
184 s.parentNode.insertBefore(b, s);})(window.lintrk);
185</script>
186```

Install the Insight Tag

Add to every page, before </body>:

<script type="text/javascript">
  _linkedin_partner_id = "YOUR_PARTNER_ID";
  window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || [];
  window._linkedin_data_partner_ids.push(_linkedin_partner_id);
  (function(l) {
    if (!l){window.lintrk = function(a,b){window.lintrk.q.push([a,b])};
    window.lintrk.q=[]}
    var s = document.getElementsByTagName("script")[0];
    var b = document.createElement("script");
    b.type = "text/javascript";b.async = true;
    b.src = "https://snap.licdn.com/li.lms-analytics/insight.min.js";
    s.parentNode.insertBefore(b, s);})(window.lintrk);
</script>
Raw lines 168 to 187
188### Conversion tracking
190LinkedIn supports two methods:
192**URL-based**: Fires when someone visits a specific URL (e.g., `/thank-you`).
193Set up in Campaign Manager > Analyze > Conversion Tracking > Create Conversion.
195**Event-based**: Fire manually on specific actions:
197```javascript
198window.lintrk('track', { conversion_id: YOUR_CONVERSION_ID });
199```

Conversion tracking

LinkedIn supports two methods:

URL-based: Fires when someone visits a specific URL (e.g., /thank-you). Set up in Campaign Manager > Analyze > Conversion Tracking > Create Conversion.

Event-based: Fire manually on specific actions:

window.lintrk('track', { conversion_id: YOUR_CONVERSION_ID });
Raw lines 188 to 200
201### LinkedIn CAPI
203For server-side tracking, LinkedIn offers a Conversions API. Set up via partner integrations (Segment, Tealium) or direct API calls. Deduplication isn't automatic: create one conversion rule per source (Insight Tag and CAPI) and send the same `eventId` from both, and LinkedIn drops the duplicate.
205---

LinkedIn CAPI

For server-side tracking, LinkedIn offers a Conversions API. Set up via partner integrations (Segment, Tealium) or direct API calls. Deduplication isn't automatic: create one conversion rule per source (Insight Tag and CAPI) and send the same eventId from both, and LinkedIn drops the duplicate.


Raw lines 201 to 206
209### Install the TikTok Pixel
211Add to every page, in `<head>`:
213```html
214<script>
215 !function (w, d, t) {
216 w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];
217 ttq.methods=["page","track","identify","instances","debug","on","off",
218 "once","ready","alias","group","enableCookie","disableCookie","holdConsent",
219 "revokeConsent","grantConsent"],ttq.setAndDefer=function(t,e)
220 {t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};
221 for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);
222 ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;
223 n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e};
224 ttq.load=function(e,n){var r="https://analytics.tiktok.com/i18n/pixel/events.js",
225 o=n&&n.partner;ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,
226 ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},
227 ttq._o[e]=n||{};var s=document.createElement("script");
228 s.type="text/javascript",s.async=!0,s.src=r+"?sdkid="+e+"&lib="+t;
229 var a=document.getElementsByTagName("script")[0];
230 a.parentNode.insertBefore(s,a)};
231 ttq.load('YOUR_PIXEL_ID');
232 ttq.page();
233 }(window, document, 'ttq');
234</script>
235```

Install the TikTok Pixel

Add to every page, in <head>:

<script>
  !function (w, d, t) {
    w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];
    ttq.methods=["page","track","identify","instances","debug","on","off",
    "once","ready","alias","group","enableCookie","disableCookie","holdConsent",
    "revokeConsent","grantConsent"],ttq.setAndDefer=function(t,e)
    {t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};
    for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);
    ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;
    n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e};
    ttq.load=function(e,n){var r="https://analytics.tiktok.com/i18n/pixel/events.js",
    o=n&&n.partner;ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,
    ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},
    ttq._o[e]=n||{};var s=document.createElement("script");
    s.type="text/javascript",s.async=!0,s.src=r+"?sdkid="+e+"&lib="+t;
    var a=document.getElementsByTagName("script")[0];
    a.parentNode.insertBefore(s,a)};
    ttq.load('YOUR_PIXEL_ID');
    ttq.page();
  }(window, document, 'ttq');
</script>
Raw lines 209 to 236
237### Standard events
239```javascript
240// View content
241ttq.track('ViewContent', {
242 content_id: 'pro-plan',
243 content_type: 'product',
244 content_name: 'Pro Plan',
245 value: 29.00,
246 currency: 'USD'
247});
249// Complete registration / sign up
250ttq.track('CompleteRegistration', {
251 content_name: 'Free Trial'
252});
254// Purchase
255ttq.track('Purchase', {
256 content_id: 'pro-plan',
257 content_type: 'product',
258 value: 99.00,
259 currency: 'USD',
260 quantity: 1
261});
263// Add to cart
264ttq.track('AddToCart', {
265 content_id: 'SKU-123',
266 content_type: 'product',
267 value: 49.00,
268 currency: 'USD'
269});
270```

Standard events

// View content
ttq.track('ViewContent', {
  content_id: 'pro-plan',
  content_type: 'product',
  content_name: 'Pro Plan',
  value: 29.00,
  currency: 'USD'
});

// Complete registration / sign up
ttq.track('CompleteRegistration', {
  content_name: 'Free Trial'
});

// Purchase
ttq.track('Purchase', {
  content_id: 'pro-plan',
  content_type: 'product',
  value: 99.00,
  currency: 'USD',
  quantity: 1
});

// Add to cart
ttq.track('AddToCart', {
  content_id: 'SKU-123',
  content_type: 'product',
  value: 49.00,
  currency: 'USD'
});
Raw lines 237 to 271
272### Events API (server-side)
274TikTok's Events API works like Meta's CAPI — send the same events from your server for better attribution. Use `event_id` for deduplication with browser pixel events.

Events API (server-side)

TikTok's Events API works like Meta's CAPI — send the same events from your server for better attribution. Use event_id for deduplication with browser pixel events.

Raw lines 272 to 275
276### Advanced Matching
278Pass hashed user data for better attribution:
280```javascript
281ttq.identify({
282 email: 'user@example.com', // auto-hashed
283 phone_number: '+11234567890'
284});
285```
287---

Advanced Matching

Pass hashed user data for better attribution:

ttq.identify({
  email: 'user@example.com',       // auto-hashed
  phone_number: '+11234567890'
});

Raw lines 276 to 288
289## GA4 as a destination
291Guided setup for recording form and booking conversions in Google Analytics 4, with an optional import into Google Ads. Account steps are guided (tell the user where to click, have them report back). Website and GTM steps the agent may be able to do directly.
293**Contents**
294- Step 1, check whether GA4 already runs
295- Steps 2 to 3, create the property and get the Measurement ID
296- Steps 4 to 6, wire the conversion (Path A with GTM, Path B without)
297- Steps 7 to 8, mark the key event and verify in DebugView
298- Steps 9 to 10, import the key event into Google Ads, then Verification and Common failures

GA4 as a destination

Guided setup for recording form and booking conversions in Google Analytics 4, with an optional import into Google Ads. Account steps are guided (tell the user where to click, have them report back). Website and GTM steps the agent may be able to do directly.

Contents

  • Step 1, check whether GA4 already runs
  • Steps 2 to 3, create the property and get the Measurement ID
  • Steps 4 to 6, wire the conversion (Path A with GTM, Path B without)
  • Steps 7 to 8, mark the key event and verify in DebugView
  • Steps 9 to 10, import the key event into Google Ads, then Verification and Common failures
Raw lines 289 to 299
300### Setup
302**Step 1 - Check whether GA4 already runs on the site**
303- **Goal.** Avoid creating a duplicate property or double-installing the tag.
304- **Do this.** Fetch or view the site's page source and search for `G-` inside a `googletagmanager.com/gtag/js?id=G-` script, a `gtag('config', 'G-` call, or a `GTM-` container (GTM may be loading GA4 internally). If you cannot check the source, ask the user whether they see website data in analytics.google.com.
305- **Expect to see.** Either a G- Measurement ID (GA4 runs, skip to Step 4) or nothing (continue to Step 2). Note whether GTM is present, that decides Path A vs Path B later.
306- **On error.** A `G-` string alone can be a false match. Confirm it sits inside a gtag script or config call. If GTM is present, the GA4 tag may live inside the container even though nothing shows in the page source.
308**Step 2 - Create the property (skip if GA4 already runs)**
309- **Goal.** Get a GA4 property that will hold the site's data.
310- **Do this.** Ask the user to sign in at analytics.google.com, click Admin (bottom left), then Create, then Property. Have them enter a property name, reporting time zone, and currency, click Next through the business questions, then Create.
311- **Expect to see.** A setup flow that moves straight into choosing a platform for data collection.
312- **On error.** No Create button means their account lacks editor access at the account level, someone with admin rights must do this. If they land in an old Universal Analytics screen, they are in the wrong account.
314**Step 3 - Add a web data stream and grab the Measurement ID**
315- **Goal.** Get the G- Measurement ID that all tagging references.
316- **Do this.** In Admin, under Data collection and modification, click Data streams, then Add stream, then Web. Enter the site URL and a stream name, then Create stream. Have the user read back the Measurement ID from the Stream details panel.
317- **Expect to see.** A Measurement ID starting with G-, for example G-ABC123XYZ.
318- **On error.** If the ID starts with AW- or GTM-, they copied the wrong thing, ask for the value labeled exactly "Measurement ID" on the web stream's details page.

Setup

Step 1 - Check whether GA4 already runs on the site

  • Goal. Avoid creating a duplicate property or double-installing the tag.
  • Do this. Fetch or view the site's page source and search for G- inside a googletagmanager.com/gtag/js?id=G- script, a gtag('config', 'G- call, or a GTM- container (GTM may be loading GA4 internally). If you cannot check the source, ask the user whether they see website data in analytics.google.com.
  • Expect to see. Either a G- Measurement ID (GA4 runs, skip to Step 4) or nothing (continue to Step 2). Note whether GTM is present, that decides Path A vs Path B later.
  • On error. A G- string alone can be a false match. Confirm it sits inside a gtag script or config call. If GTM is present, the GA4 tag may live inside the container even though nothing shows in the page source.

Step 2 - Create the property (skip if GA4 already runs)

  • Goal. Get a GA4 property that will hold the site's data.
  • Do this. Ask the user to sign in at analytics.google.com, click Admin (bottom left), then Create, then Property. Have them enter a property name, reporting time zone, and currency, click Next through the business questions, then Create.
  • Expect to see. A setup flow that moves straight into choosing a platform for data collection.
  • On error. No Create button means their account lacks editor access at the account level, someone with admin rights must do this. If they land in an old Universal Analytics screen, they are in the wrong account.

Step 3 - Add a web data stream and grab the Measurement ID

  • Goal. Get the G- Measurement ID that all tagging references.
  • Do this. In Admin, under Data collection and modification, click Data streams, then Add stream, then Web. Enter the site URL and a stream name, then Create stream. Have the user read back the Measurement ID from the Stream details panel.
  • Expect to see. A Measurement ID starting with G-, for example G-ABC123XYZ.
  • On error. If the ID starts with AW- or GTM-, they copied the wrong thing, ask for the value labeled exactly "Measurement ID" on the web stream's details page.
Raw lines 300 to 319
Involves a person ask the user :304
Involves a person Ask the user :310
320### Wire the conversion
322Pick 1 path. Path A when the site runs Google Tag Manager, Path B when it does not. Never both, that double counts.
324**Step 4 (Path A): Build and import the GTM file**
325- **Goal.** Add a detection tag for the tool plus a GA4 event tag that sends `generate_lead` when the tool's event fires.
326- **Do this.** From the repo root run, substituting the tool key from `../assets/gtm-recipes/event-map.json`. `python3 ../scripts/build_recipe.py --tool {tool} --send ga4 --measurement-id G-XXXXXXXXXX -o import-me.json` (add `--event-name` only if the account wants a name other than the default `generate_lead`). Then in GTM click Admin, then Import Container, choose the file, pick the Existing workspace, choose Merge, then Rename conflicting tags, triggers, and variables, and Confirm. If the property is brand new and nothing loads GA4 yet, also add the base tag in GTM (Tags, New, Google tag, enter the G- ID, fire on Initialization, All Pages).
327- **Expect to see.** The import preview lists the added tags and 1 trigger, no deletions.
328- **On error.** "measurement ID should start with G-" means an AW- or GTM- ID was passed. Deletions in the import preview mean Overwrite was picked instead of Merge, cancel and redo.
330**Step 5 (Path A): Preview, test, publish**
331- **Goal.** Prove the GA4 event fires on a real submission before going live.
332- **Do this.** Click Preview in GTM, connect to the page carrying the form, submit a test entry (ask the user before submitting on production). Confirm the tool's detection event (for example `gravity_form_submitted`, the canonical names live in `../assets/gtm-recipes/event-map.json` and `../assets/snippets/README.md`) appears and the GA4 event tag fired on it. Then Submit and Publish.
333- **Expect to see.** The detection event exactly once per submission, and the GA4 event tag marked Fired.
334- **On error.** No detection event means the wrong tool key or a form inside a cross-domain iframe, recheck `../assets/snippets/README.md`. The event firing twice means the detection code is installed twice (site head plus GTM), remove one.
336**Step 6 (Path B): Paste the snippet and the gtag listener**
337- **Goal.** Same outcome without GTM, using the platform's custom code setting.
338- **Do this.** Paste the matching `../assets/snippets/{tool}.js` file inside a `<script>` tag in the site's `<head>`, then add the code below. Replace `G-XXXXXXXXXX` with the Measurement ID and `TOOL_EVENT_NAME` with the canonical event name. Skip the first block if GA4 already loads on the site.
340```html
341<!-- Google tag (gtag.js). Skip if GA4 already loads on this site -->
342<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
343<script>
344 window.dataLayer = window.dataLayer || [];
345 function gtag(){dataLayer.push(arguments);}
346 gtag('js', new Date());
347 gtag('config', 'G-XXXXXXXXXX');
348</script>
350<!-- Listener. Sends generate_lead when the detection event is pushed -->
351<script>
352 (function () {
353 window.dataLayer = window.dataLayer || [];
354 var originalPush = window.dataLayer.push;
355 window.dataLayer.push = function (obj) {
356 var result = originalPush.apply(this, arguments);
357 if (obj && obj.event === 'TOOL_EVENT_NAME') {
358 gtag('event', 'generate_lead');
359 }
360 return result;
361 };
362 })();
363</script>
364```
366- **Expect to see.** On a test submission, the browser's Network tab shows a request to google-analytics.com containing `en=generate_lead`.
367- **On error.** No request usually means a placeholder was left unreplaced or the event name does not exactly match the canonical name. If the page also runs GTM, stop, this is a Path A site and running both paths double counts.
369**Step 7 - Mark generate_lead as a key event**
370- **Goal.** Tell GA4 this event is the one that matters, which also makes it importable into Google Ads.
371- **Do this.** Send 1 test submission first so the event exists. Then ask the user to open Admin, then under Data display click Events, find `generate_lead` in the table, and switch on the "Mark as key event" toggle. The event can take up to 24 hours to appear in this table (DebugView in Step 8 shows it within seconds, so use that to confirm firing in the meantime).
372- **Expect to see.** The toggle on, and `generate_lead` listed under Admin, Data display, Key events.
373- **On error.** Event not in the table yet means processing lag, wait up to 24 hours. If it never appears, the tag is not firing, go back to Step 5 or 6.
375**Step 8 - Verify in DebugView**
376- **Goal.** Watch the event arrive in GA4 in real time.
377- **Do this.** Enable debug mode by opening tagassistant.google.com and connecting it to the site (GTM Preview mode also enables it automatically). Then have the user open Admin, then under Data display click DebugView, pick the debug device in the top left, and submit a test on the site.
378- **Expect to see.** `generate_lead` appears in the event stream within seconds of the submission.
379- **On error.** An empty DebugView means debug mode is not active on the browser doing the testing, or an ad blocker is eating the hits, retry in a clean profile. Events in DebugView but never in reports points to a consent or filtering problem, see `discrepancies.md`.

Wire the conversion

Pick 1 path. Path A when the site runs Google Tag Manager, Path B when it does not. Never both, that double counts.

Step 4 (Path A): Build and import the GTM file

  • Goal. Add a detection tag for the tool plus a GA4 event tag that sends generate_lead when the tool's event fires.
  • Do this. From the repo root run, substituting the tool key from ../assets/gtm-recipes/event-map.json. python3 ../scripts/build_recipe.py --tool {tool} --send ga4 --measurement-id G-XXXXXXXXXX -o import-me.json (add --event-name only if the account wants a name other than the default generate_lead). Then in GTM click Admin, then Import Container, choose the file, pick the Existing workspace, choose Merge, then Rename conflicting tags, triggers, and variables, and Confirm. If the property is brand new and nothing loads GA4 yet, also add the base tag in GTM (Tags, New, Google tag, enter the G- ID, fire on Initialization, All Pages).
  • Expect to see. The import preview lists the added tags and 1 trigger, no deletions.
  • On error. "measurement ID should start with G-" means an AW- or GTM- ID was passed. Deletions in the import preview mean Overwrite was picked instead of Merge, cancel and redo.

Step 5 (Path A): Preview, test, publish

  • Goal. Prove the GA4 event fires on a real submission before going live.
  • Do this. Click Preview in GTM, connect to the page carrying the form, submit a test entry (ask the user before submitting on production). Confirm the tool's detection event (for example gravity_form_submitted, the canonical names live in ../assets/gtm-recipes/event-map.json and ../assets/snippets/README.md) appears and the GA4 event tag fired on it. Then Submit and Publish.
  • Expect to see. The detection event exactly once per submission, and the GA4 event tag marked Fired.
  • On error. No detection event means the wrong tool key or a form inside a cross-domain iframe, recheck ../assets/snippets/README.md. The event firing twice means the detection code is installed twice (site head plus GTM), remove one.

Step 6 (Path B): Paste the snippet and the gtag listener

  • Goal. Same outcome without GTM, using the platform's custom code setting.
  • Do this. Paste the matching ../assets/snippets/{tool}.js file inside a <script> tag in the site's <head>, then add the code below. Replace G-XXXXXXXXXX with the Measurement ID and TOOL_EVENT_NAME with the canonical event name. Skip the first block if GA4 already loads on the site.
<!-- Google tag (gtag.js). Skip if GA4 already loads on this site -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXXXXX');
</script>

<!-- Listener. Sends generate_lead when the detection event is pushed -->
<script>
  (function () {
    window.dataLayer = window.dataLayer || [];
    var originalPush = window.dataLayer.push;
    window.dataLayer.push = function (obj) {
      var result = originalPush.apply(this, arguments);
      if (obj && obj.event === 'TOOL_EVENT_NAME') {
        gtag('event', 'generate_lead');
      }
      return result;
    };
  })();
</script>
  • Expect to see. On a test submission, the browser's Network tab shows a request to google-analytics.com containing en=generate_lead.
  • On error. No request usually means a placeholder was left unreplaced or the event name does not exactly match the canonical name. If the page also runs GTM, stop, this is a Path A site and running both paths double counts.

Step 7 - Mark generate_lead as a key event

  • Goal. Tell GA4 this event is the one that matters, which also makes it importable into Google Ads.
  • Do this. Send 1 test submission first so the event exists. Then ask the user to open Admin, then under Data display click Events, find generate_lead in the table, and switch on the "Mark as key event" toggle. The event can take up to 24 hours to appear in this table (DebugView in Step 8 shows it within seconds, so use that to confirm firing in the meantime).
  • Expect to see. The toggle on, and generate_lead listed under Admin, Data display, Key events.
  • On error. Event not in the table yet means processing lag, wait up to 24 hours. If it never appears, the tag is not firing, go back to Step 5 or 6.

Step 8 - Verify in DebugView

  • Goal. Watch the event arrive in GA4 in real time.
  • Do this. Enable debug mode by opening tagassistant.google.com and connecting it to the site (GTM Preview mode also enables it automatically). Then have the user open Admin, then under Data display click DebugView, pick the debug device in the top left, and submit a test on the site.
  • Expect to see. generate_lead appears in the event stream within seconds of the submission.
  • On error. An empty DebugView means debug mode is not active on the browser doing the testing, or an ad blocker is eating the hits, retry in a clean profile. Events in DebugView but never in reports points to a consent or filtering problem, see discrepancies.md.
Raw lines 320 to 380
Runs code python3 ../scripts/build_recipe.py :326
Involves a person ask the user :332
381### Importing the key event into Google Ads
383This is sensible only when there is no direct Google Ads conversion tag on the site (Path A or B in `google-ads.md` Part A). A direct Ads tag is faster and more complete, imports arrive 1 to 3 days stale.
385**Step 9 - Link GA4 and Google Ads**
386- **Goal.** Give Google Ads permission to see the property's key events.
387- **Do this.** Ask the user to open GA4 Admin, then under Product links click Google Ads links, then Link, and choose their Google Ads account. They need editor rights on the GA4 property and admin access in Google Ads. Also confirm auto-tagging is on in Google Ads (Admin icon, Account settings, Auto-tagging).
388- **Expect to see.** The Ads account listed under Google Ads links.
389- **On error.** No accounts offered means the same Google login does not have admin access to the Ads account, link from a login that has both.
391**Step 10 - Import the key event in Google Ads**
392- **Goal.** Create a Google Ads conversion based on the GA4 key event.
393- **Do this.** Ask the user to open Google Ads, click the Goals icon, then Conversions, then Summary, then + Create conversion action (older accounts show + New conversion action), choose Import, then Google Analytics 4 properties, then Web, then Continue. Select `generate_lead` and click Import and continue. Google's docs also offer this flow from inside GA4 under Advertising, Conversion management.
394- **Expect to see.** A new conversion action in the Ads Summary table sourced from Google Analytics.
395- **On error.** The key event not listed means Step 7 or Step 9 is incomplete, or the event is under 24 hours old. Data taking days to appear is normal, imports lag 1 to 3 days.
397**The double-counting trap.** Never leave both an Ads website conversion action AND an imported GA4 key event set as Primary for the same real-world action. Bidding then counts every lead twice. Google usually auto-marks the import as Secondary when it detects the overlap, but verify it in the Summary table. If both exist, keep the direct Ads tag as Primary and the import as Secondary. Details in `google-ads.md` and `discrepancies.md`.

Importing the key event into Google Ads

This is sensible only when there is no direct Google Ads conversion tag on the site (Path A or B in google-ads.md Part A). A direct Ads tag is faster and more complete, imports arrive 1 to 3 days stale.

Step 9 - Link GA4 and Google Ads

  • Goal. Give Google Ads permission to see the property's key events.
  • Do this. Ask the user to open GA4 Admin, then under Product links click Google Ads links, then Link, and choose their Google Ads account. They need editor rights on the GA4 property and admin access in Google Ads. Also confirm auto-tagging is on in Google Ads (Admin icon, Account settings, Auto-tagging).
  • Expect to see. The Ads account listed under Google Ads links.
  • On error. No accounts offered means the same Google login does not have admin access to the Ads account, link from a login that has both.

Step 10 - Import the key event in Google Ads

  • Goal. Create a Google Ads conversion based on the GA4 key event.
  • Do this. Ask the user to open Google Ads, click the Goals icon, then Conversions, then Summary, then + Create conversion action (older accounts show + New conversion action), choose Import, then Google Analytics 4 properties, then Web, then Continue. Select generate_lead and click Import and continue. Google's docs also offer this flow from inside GA4 under Advertising, Conversion management.
  • Expect to see. A new conversion action in the Ads Summary table sourced from Google Analytics.
  • On error. The key event not listed means Step 7 or Step 9 is incomplete, or the event is under 24 hours old. Data taking days to appear is normal, imports lag 1 to 3 days.

The double-counting trap. Never leave both an Ads website conversion action AND an imported GA4 key event set as Primary for the same real-world action. Bidding then counts every lead twice. Google usually auto-marks the import as Secondary when it detects the overlap, but verify it in the Summary table. If both exist, keep the direct Ads tag as Primary and the import as Secondary. Details in google-ads.md and discrepancies.md.

Raw lines 381 to 398
Involves a person Ask the user :387
399### Verification
401Observable checks only:
4031. **DebugView.** `generate_lead` appears within seconds of a test submission (Step 8).
4042. **Realtime.** Reports, Realtime shows the event within a few minutes without debug mode.
4053. **Next day.** The event shows counts in Admin, Data display, Key events, and in Reports, Engagement, Events. GA4 processing takes 24 to 48 hours, so never judge same-day numbers.
4064. **Set expectations.** Even a perfect browser-side setup misses typically 10–30% (depending on audience) of conversions to ad blockers, Safari cookie limits, and cross-device journeys the browser can't connect back to the ad click. And GA4 will never exactly match Google Ads, that is normal. Both are covered in `discrepancies.md`.

Verification

Observable checks only:

  1. DebugView. generate_lead appears within seconds of a test submission (Step 8).
  2. Realtime. Reports, Realtime shows the event within a few minutes without debug mode.
  3. Next day. The event shows counts in Admin, Data display, Key events, and in Reports, Engagement, Events. GA4 processing takes 24 to 48 hours, so never judge same-day numbers.
  4. Set expectations. Even a perfect browser-side setup misses typically 10–30% (depending on audience) of conversions to ad blockers, Safari cookie limits, and cross-device journeys the browser can't connect back to the ad click. And GA4 will never exactly match Google Ads, that is normal. Both are covered in discrepancies.md.
Raw lines 399 to 407
408### Common failures
410For "GA4 and Google Ads don't match" questions and any silent undercounting, work through `discrepancies.md`. For problems on the Ads side of an import (statuses, Primary vs Secondary, attribution), use `google-ads.md`.

Common failures

For "GA4 and Google Ads don't match" questions and any silent undercounting, work through discrepancies.md. For problems on the Ads side of an import (statuses, Primary vs Secondary, attribution), use google-ads.md.

Raw lines 408 to 411
412## Validating what you installed
414The quick first-pass checklist, the full evidence procedure, and the report format live in `audit-playbook.md`; the fired-versus-arrived discipline and per-platform fast feedback surfaces are in `SKILL.md` under Verification discipline. Do not close a setup without them.

Validating what you installed

The quick first-pass checklist, the full evidence procedure, and the report format live in audit-playbook.md; the fired-versus-arrived discipline and per-platform fast feedback surfaces are in SKILL.md under Verification discipline. Do not close a setup without them.

Raw lines 412 to 415
416### Debugging tools
418| Platform | Tool |
419|----------|------|
420| Google | Google Tag Assistant, Chrome DevTools Network tab |
421| Meta | Meta Ads Data Advisor (formerly Pixel Helper) Chrome extension, Events Manager Test Events |
422| LinkedIn | The Insight Tag's status in Campaign Manager (Active can take up to 24 hours) and the browser Network tab (a request to ads.linkedin.com returning 302) |
423| TikTok | TikTok Pixel Helper (Chrome extension), Events Manager |
424| All | GTM Preview Mode (if using Google Tag Manager) |

Debugging tools

PlatformTool
GoogleGoogle Tag Assistant, Chrome DevTools Network tab
MetaMeta Ads Data Advisor (formerly Pixel Helper) Chrome extension, Events Manager Test Events
LinkedInThe Insight Tag's status in Campaign Manager (Active can take up to 24 hours) and the browser Network tab (a request to ads.linkedin.com returning 302)
TikTokTikTok Pixel Helper (Chrome extension), Events Manager
AllGTM Preview Mode (if using Google Tag Manager)
Raw lines 416 to 424