Skip to content

fix(analytics): replace GTM container and remove duplicate GA4 pageview tracking#3041

Open
baktun14 wants to merge 6 commits intomainfrom
fix/analytics-replace-gtm-remove-duplicate-ga4
Open

fix(analytics): replace GTM container and remove duplicate GA4 pageview tracking#3041
baktun14 wants to merge 6 commits intomainfrom
fix/analytics-replace-gtm-remove-duplicate-ga4

Conversation

@baktun14
Copy link
Copy Markdown
Contributor

@baktun14 baktun14 commented Apr 2, 2026

Why

The old GTM container (GTM-WT4KSXNQ) belongs to Antidote and we have no access to configure it. Additionally, an inline <GAnalytics trackPageViews /> component was sending pageviews directly to GA4 (G-LFRGN2J2RV) alongside GTM — double-counting page views, user counts, and funnel metrics.

A new GTM container (GTM-MSF384N3) has been created that we own and can configure. It handles GA4 directly, so the inline gtag snippet is no longer needed.

What

  • Replace GTM ID: GTM-WT4KSXNQGTM-MSF384N3 in apps/deploy-web/env/.env.production
  • Remove inline GA4 pageview tracking: Removed <GAnalytics trackPageViews /> from CustomGoogleAnalytics.tsx — the new GTM container handles GA4 pageviews directly
  • Fix user identification: Changed gtag("config", measurementId, { user_id }) to gtag("set", { user_id }) in AnalyticsService.identify() to avoid triggering an extra pageview on every identify call
  • Updated corresponding test assertion

No changes to: dataLayer initialization, custom event tracking, Amplitude, web vitals reporting, or any other tracking.

Summary by CodeRabbit

  • Chores
    • Updated Google Tag Manager ID for the deployed site.
    • Switched analytics emission to direct dataLayer pushes and removed the prior analytics client dependency.
  • Tests
    • Updated analytics tests to assert dataLayer output instead of the previous analytics client calls.

…ew tracking

The old GTM container (GTM-WT4KSXNQ) belongs to Antidote and is not configurable
by our team. An inline gtag snippet was also sending pageviews to the same GA4
property (G-LFRGN2J2RV), inflating page views and user counts.

- Replace GTM ID with new container GTM-MSF384N3
- Remove inline GAnalytics trackPageViews component
- Change gtag("config") to gtag("set") for user identification to avoid triggering extra pageviews
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Updated GTM ID, removed the CustomGoogleAnalytics component and its top-level usage, replaced Google Analytics client calls with direct writes to window.dataLayer in the analytics service, adjusted tests to assert dataLayer writes, added Window.dataLayer type, and removed the nextjs-google-analytics dependency.

Changes

Cohort / File(s) Summary
Configuration
apps/deploy-web/env/.env.production
Updated NEXT_PUBLIC_GTM_ID from GTM-WT4KSXNQGTM-MSF384N3.
Top-level UI / Component
apps/deploy-web/src/components/layout/CustomGoogleAnalytics.tsx, apps/deploy-web/src/pages/_app.tsx
Removed the CustomGoogleAnalytics component and its import/render in _app.tsx.
Analytics Service & Tests
apps/deploy-web/src/services/analytics/analytics.service.ts, apps/deploy-web/src/services/analytics/analytics.service.spec.ts
Replaced GA client integration with direct window.dataLayer pushes for identify and track; tests now assert entries in dataLayer rather than mocked gtag/GA calls.
Types
apps/deploy-web/src/types/global.ts
Added optional dataLayer?: Record<string, unknown>[] to global Window interface.
Dependencies
apps/deploy-web/package.json
Removed nextjs-google-analytics dependency.

Sequence Diagram(s)

sequenceDiagram
    participant App as Client App
    participant Service as AnalyticsService
    participant DL as window.dataLayer
    participant GTM as Google Tag Manager

    rect rgba(135,206,250,0.5)
    App->>Service: identify(user)
    Service->>DL: push { user_id: <id> }
    DL->>GTM: GTM reads dataLayer
    end

    rect rgba(144,238,144,0.5)
    App->>Service: track(event, props)
    Service->>DL: push { event: <name>, ...props }
    DL->>GTM: GTM reads event and handles tags
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through code and nudged the stream,
Pushed user ids where GTM gleams,
A component gone, the path made clear,
DataLayer listens when events appear,
thump-thump — carrots of telemetry, here.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main changes: replacing the GTM container ID and removing duplicate GA4 pageview tracking via the CustomGoogleAnalytics component.
Description check ✅ Passed The description provides clear context with a 'Why' section explaining the problem (unowned GTM container, duplicate pageview tracking) and a 'What' section detailing all changes made.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/analytics-replace-gtm-remove-duplicate-ga4

Comment @coderabbitai help to get the list of available commands and usage tips.

baktun14 added 2 commits April 2, 2026 14:26
…o TrackingScripts

Move web vitals reporting into TrackingScripts and delete the now-redundant
CustomGoogleAnalytics component.
Web vitals must run at the _app level to cover all pages, not just those
using Layout. TrackingScripts in Layout now only handles GTM and
Growth Channel scripts.
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 2, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 59.64%. Comparing base (e4b7a90) to head (26c92de).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3041      +/-   ##
==========================================
+ Coverage   59.62%   59.64%   +0.02%     
==========================================
  Files        1034     1033       -1     
  Lines       24248    24239       -9     
  Branches     6009     6005       -4     
==========================================
  Hits        14458    14458              
+ Misses       8539     8531       -8     
+ Partials     1251     1250       -1     
Flag Coverage Δ
api 81.24% <ø> (ø)
deploy-web 43.27% <100.00%> (+0.02%) ⬆️
log-collector 84.92% <ø> (ø)
notifications 86.06% <ø> (ø)
provider-console 81.48% <ø> (ø)
provider-proxy 85.21% <ø> (ø)
tx-signer 76.26% <ø> (ø)
Files with missing lines Coverage Δ
apps/deploy-web/src/pages/_app.tsx 0.00% <ø> (ø)
...oy-web/src/services/analytics/analytics.service.ts 94.20% <100.00%> (-0.17%) ⬇️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…r GA events

GTM now handles GA4 directly, so the nextjs-google-analytics package is
no longer needed. Custom events are pushed to window.dataLayer instead,
which GTM picks up natively. Also removes useReportWebVitals — web vitals
can be measured via GTM's built-in Core Web Vitals trigger.
@github-actions github-actions bot added size: S and removed size: XS labels Apr 2, 2026
…ibility

GTM does not expose window.gtag — it only reads from window.dataLayer.
Replace gtag("set", { user_id }) with dataLayer.push({ user_id }) so
user identification works correctly with the new GTM container.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/deploy-web/src/services/analytics/analytics.service.spec.ts (1)

95-111: ⚠️ Potential issue | 🟡 Minor

Assert the exact identify() payload here.

toContainEqual still passes if identify() pushes { user_id } and an extra GTM event, so this doesn’t fully cover the “no extra pageview on identify” regression. An exact array assertion would lock that down.

Suggested assertion
-      expect(dataLayer).toContainEqual({ user_id: user.id });
+      expect(dataLayer).toEqual([{ user_id: user.id }]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/deploy-web/src/services/analytics/analytics.service.spec.ts` around
lines 95 - 111, Replace the loose assertion on dataLayer with an exact array
equality so we lock down the identify payload: assert that dataLayer equals
exactly [{ user_id: user.id }] (e.g., replace the toContainEqual check with a
toEqual-style assertion) after calling service.identify(user), and keep/also
assert the amplitude mock identify/setUserId were not invoked when amplitude is
disabled (identify, setUserId) to ensure no extra GTM/pageview events are pushed
by service.identify.
🧹 Nitpick comments (1)
apps/deploy-web/src/services/analytics/analytics.service.ts (1)

156-156: Initialize dataLayer before the first push.

This getter can still return undefined, so both new push() paths silently no-op until GTM creates the array. I’d harden this here with window.dataLayer ??= [] instead of relying on apps/deploy-web/src/components/layout/TrackingScripts.tsx boot order.

Suggested hardening
-    private readonly getDataLayer: () => Record<string, unknown>[] | undefined = () => (isBrowser ? window.dataLayer : undefined),
+    private readonly getDataLayer: () => Record<string, unknown>[] | undefined = () => {
+      if (!isBrowser) {
+        return undefined;
+      }
+
+      window.dataLayer ??= [];
+      return window.dataLayer;
+    },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/deploy-web/src/services/analytics/analytics.service.ts` at line 156, The
getDataLayer getter can return undefined causing push() to no-op; modify
getDataLayer (and any code paths that call it) to ensure window.dataLayer is
initialized before use by assigning window.dataLayer ??= [] when isBrowser is
true so the getter always returns a Record<string, unknown>[]; update the getter
named getDataLayer to perform this initialization and then return
window.dataLayer to guarantee pushes succeed even if GTM script hasn't run yet.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/deploy-web/src/services/analytics/analytics.service.ts`:
- Around line 255-256: The current push uses { event: name, ...props } which
allows a caller-supplied event field in eventProperties to override the
transformed name; update the call in the analytics service (reference
transformGaEvent and getDataLayer) to ensure the GTM event name cannot be
overridden by either (a) spread props first and set event: name last, or (b)
strip the event key from props before spreading (delete or omit the event
property) and then push { event: name, ...cleanProps } so GTM always receives
the intended trigger name.

---

Outside diff comments:
In `@apps/deploy-web/src/services/analytics/analytics.service.spec.ts`:
- Around line 95-111: Replace the loose assertion on dataLayer with an exact
array equality so we lock down the identify payload: assert that dataLayer
equals exactly [{ user_id: user.id }] (e.g., replace the toContainEqual check
with a toEqual-style assertion) after calling service.identify(user), and
keep/also assert the amplitude mock identify/setUserId were not invoked when
amplitude is disabled (identify, setUserId) to ensure no extra GTM/pageview
events are pushed by service.identify.

---

Nitpick comments:
In `@apps/deploy-web/src/services/analytics/analytics.service.ts`:
- Line 156: The getDataLayer getter can return undefined causing push() to
no-op; modify getDataLayer (and any code paths that call it) to ensure
window.dataLayer is initialized before use by assigning window.dataLayer ??= []
when isBrowser is true so the getter always returns a Record<string, unknown>[];
update the getter named getDataLayer to perform this initialization and then
return window.dataLayer to guarantee pushes succeed even if GTM script hasn't
run yet.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29ec08dd-6db6-477f-9747-937631eac8ef

📥 Commits

Reviewing files that changed from the base of the PR and between 80630ce and f7ad9ac.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • apps/deploy-web/package.json
  • apps/deploy-web/src/pages/_app.tsx
  • apps/deploy-web/src/services/analytics/analytics.service.spec.ts
  • apps/deploy-web/src/services/analytics/analytics.service.ts
  • apps/deploy-web/src/types/global.ts
💤 Files with no reviewable changes (2)
  • apps/deploy-web/src/pages/_app.tsx
  • apps/deploy-web/package.json
✅ Files skipped from review due to trivial changes (1)
  • apps/deploy-web/src/types/global.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/deploy-web/src/services/analytics/analytics.service.ts (1)

156-156: Initialize window.dataLayer as fallback in the default getter.

Line 156 returns undefined if window.dataLayer is not yet defined, causing Lines 180 and 256 to silently skip events via optional chaining. Although isBrowser guards prevent server-side calls, initializing the queue here ensures early identify() and track() invocations buffer to the dataLayer even if GTM hasn't loaded, matching GTM's native pre-bootstrap behavior.

Suggested fix
-    private readonly getDataLayer: () => Record<string, unknown>[] | undefined = () => (isBrowser ? window.dataLayer : undefined),
+    private readonly getDataLayer: () => Record<string, unknown>[] | undefined = () => {
+      if (!isBrowser) {
+        return undefined;
+      }
+
+      window.dataLayer = window.dataLayer || [];
+      return window.dataLayer;
+    },

Also applies to: 179-180, 255-256

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/deploy-web/src/services/analytics/analytics.service.ts` at line 156, The
getter getDataLayer should initialize a client-side fallback queue instead of
returning undefined so early identify() and track() calls buffer until GTM
loads; modify the getDataLayer implementation to, when isBrowser is true, ensure
window.dataLayer exists (e.g., create an empty array if undefined) and return it
(update the return type if necessary), and rely on that initialized array in the
identify() and track() call sites so optional chaining no longer silently drops
events.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/deploy-web/src/services/analytics/analytics.service.ts`:
- Line 156: The getter getDataLayer should initialize a client-side fallback
queue instead of returning undefined so early identify() and track() calls
buffer until GTM loads; modify the getDataLayer implementation to, when
isBrowser is true, ensure window.dataLayer exists (e.g., create an empty array
if undefined) and return it (update the return type if necessary), and rely on
that initialized array in the identify() and track() call sites so optional
chaining no longer silently drops events.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c78218b6-d6c3-4ede-b381-6a6b2d42f3ce

📥 Commits

Reviewing files that changed from the base of the PR and between f7ad9ac and 26c92de.

📒 Files selected for processing (1)
  • apps/deploy-web/src/services/analytics/analytics.service.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant