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. 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/ 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 { - (window as any).dataLayer.push(args); - }; - (window as any).gtag("js", new Date()); + 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 = gtagStub; + gtagStub("js", new Date()); // Load script const script = document.createElement("script"); @@ -231,19 +236,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) {