From 060b402e05399adc752dce4ed8a65b28d268538f Mon Sep 17 00:00:00 2001 From: Charlie Tysse Date: Mon, 23 Mar 2026 16:14:41 -0400 Subject: [PATCH 1/5] chore: add .worktrees/ to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b0e095b..deb95be 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ yarn-debug.log* yarn-error.log* .vercel .env*.local + +# Worktrees +.worktrees/ From 2977d96047541f1ecb29ea8f2496c729ce675a80 Mon Sep 17 00:00:00 2001 From: Charlie Tysse Date: Mon, 23 Mar 2026 16:16:16 -0400 Subject: [PATCH 2/5] feat(demo): wire up real GA4 destination with consent mode Replace simulatedGA4 no-op with the real @junctionjs/destination-ga4 package. GA4 is conditionally enabled via NEXT_PUBLIC_GA4_MEASUREMENT_ID env var. When the env var is absent, the demo works as before (demo sink only + simulated vendors). Co-Authored-By: Claude Sonnet 4.6 --- apps/demo/lib/demo-sink.ts | 1 - apps/demo/lib/junction-config.ts | 24 +++++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/apps/demo/lib/demo-sink.ts b/apps/demo/lib/demo-sink.ts index 0e2f9e2..77e8da1 100644 --- a/apps/demo/lib/demo-sink.ts +++ b/apps/demo/lib/demo-sink.ts @@ -78,7 +78,6 @@ function noopDestination(name: string, consent: string[]): Destination Date: Tue, 24 Mar 2026 06:20:25 -0400 Subject: [PATCH 3/5] fix(ga4): use Arguments object for dataLayer and add consent defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs fixed: 1. gtag stub used arrow function with rest params, pushing Arrays to dataLayer. gtag.js silently ignores array entries — it expects the Arguments object. Switched to `function() { dataLayer.push(arguments) }` matching Google's official snippet. 2. When consentMode is enabled, gtag("consent", "default", {...}) must fire before gtag("config", ...) so gtag.js knows consent mode is active. Without this, consent state was never communicated to Google. --- packages/destination-ga4/src/index.ts | 30 +++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/destination-ga4/src/index.ts b/packages/destination-ga4/src/index.ts index 5006331..2f11743 100644 --- a/packages/destination-ga4/src/index.ts +++ b/packages/destination-ga4/src/index.ts @@ -196,10 +196,14 @@ function loadGtag(measurementId: string, gtagUrl?: string): void { if (typeof window === "undefined") return; if (typeof (window as any).gtag === "function") return; - // Initialize dataLayer + // Initialize dataLayer and gtag stub. + // MUST use `arguments` (not rest params) — gtag.js expects Arguments objects + // in the dataLayer queue, not arrays. Using an arrow function with ...args + // pushes plain arrays which gtag.js silently ignores. (window as any).dataLayer = (window as any).dataLayer || []; - (window as any).gtag = (...args: any[]) => { - (window as any).dataLayer.push(args); + // biome-ignore lint/style/noArguments: gtag.js requires the Arguments object + (window as any).gtag = () => { + (window as any).dataLayer.push(arguments); }; (window as any).gtag("js", new Date()); @@ -231,19 +235,33 @@ export function createGA4(): Destination { throw new Error("[Junction:GA4] measurementId is required"); } + consentModeEnabled = config.consentMode === true; + // Client-side: load gtag.js if needed if (typeof window !== "undefined" && config.loadScript !== false) { loadGtag(config.measurementId, config.gtagUrl); + // Set consent defaults BEFORE config — required by Google's consent mode v2. + // Without this, gtag doesn't know consent mode is active. + if (consentModeEnabled) { + (window as any).gtag("consent", "default", { + ad_storage: "denied", + analytics_storage: "denied", + ad_user_data: "denied", + ad_personalization: "denied", + personalization_storage: "denied", + functionality_storage: "granted", + security_storage: "granted", + }); + } + // Configure GA4 const gtagConfig: Record = { send_page_view: config.sendPageView ?? false, }; - (window as any).gtag?.("config", config.measurementId, gtagConfig); + (window as any).gtag("config", config.measurementId, gtagConfig); } - - consentModeEnabled = config.consentMode === true; }, transform(event: JctEvent, config: GA4Config) { From 3ef7d73da91d4834f23ddab71736d85c4e54f1d6 Mon Sep 17 00:00:00 2001 From: Charlie Tysse Date: Tue, 24 Mar 2026 06:21:02 -0400 Subject: [PATCH 4/5] fix(ga4): prevent Biome from converting gtag stub to arrow function Arrow functions don't have their own `arguments` object, which breaks the gtag dataLayer integration. Added biome-ignore for both useArrowFunction and noArguments rules. --- packages/destination-ga4/src/index.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/destination-ga4/src/index.ts b/packages/destination-ga4/src/index.ts index 2f11743..3df8629 100644 --- a/packages/destination-ga4/src/index.ts +++ b/packages/destination-ga4/src/index.ts @@ -196,16 +196,17 @@ function loadGtag(measurementId: string, gtagUrl?: string): void { if (typeof window === "undefined") return; if (typeof (window as any).gtag === "function") return; - // Initialize dataLayer and gtag stub. - // MUST use `arguments` (not rest params) — gtag.js expects Arguments objects - // in the dataLayer queue, not arrays. Using an arrow function with ...args - // pushes plain arrays which gtag.js silently ignores. + // Initialize dataLayer and gtag stub matching Google's official snippet exactly. + // The stub MUST use `arguments` (not rest params) — gtag.js expects Arguments + // objects in the dataLayer queue, not plain arrays. Arrow functions and rest + // params produce arrays which gtag.js silently ignores. (window as any).dataLayer = (window as any).dataLayer || []; - // biome-ignore lint/style/noArguments: gtag.js requires the Arguments object - (window as any).gtag = () => { + function gtagStub(..._: unknown[]) { + // biome-ignore lint/style/noArguments: gtag.js requires the Arguments object, not an Array (window as any).dataLayer.push(arguments); - }; - (window as any).gtag("js", new Date()); + } + (window as any).gtag = gtagStub; + gtagStub("js", new Date()); // Load script const script = document.createElement("script"); From 31bbecd73196f166144b93505bfd227398729721 Mon Sep 17 00:00:00 2001 From: Charlie Tysse Date: Tue, 24 Mar 2026 07:58:11 -0400 Subject: [PATCH 5/5] chore: add changeset for GA4 gtag stub fix --- .changeset/fix-ga4-gtag-stub.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/fix-ga4-gtag-stub.md diff --git a/.changeset/fix-ga4-gtag-stub.md b/.changeset/fix-ga4-gtag-stub.md new file mode 100644 index 0000000..2e83d1d --- /dev/null +++ b/.changeset/fix-ga4-gtag-stub.md @@ -0,0 +1,14 @@ +--- +"@junctionjs/destination-ga4": patch +--- + +Fix gtag.js integration: use Arguments object instead of Array for dataLayer + +The gtag stub function was using an arrow function with rest parameters, which +pushed plain Arrays to the dataLayer. gtag.js silently ignores array entries — +it expects the Arguments object. Switched to a named function declaration using +`arguments` to match Google's official snippet. + +Also added `gtag("consent", "default", {...})` call before `gtag("config", ...)` +when consent mode is enabled. Without this, gtag.js doesn't know consent mode +is active and consent state is never communicated to Google.