Skip to content

fix(apv): prevent duplicate page views on SPA re-init (stacked wrapper + init guard) - #1331

Open
alexs-mparticle wants to merge 8 commits into
developmentfrom
fix/apv-reinit-duplicate-page-view
Open

fix(apv): prevent duplicate page views on SPA re-init (stacked wrapper + init guard)#1331
alexs-mparticle wants to merge 8 commits into
developmentfrom
fix/apv-reinit-duplicate-page-view

Conversation

@alexs-mparticle

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a window.__mpApvTracker__ singleton so that when Next.js re-evaluates the SDK module on each SPA navigation, the new module's tracker tears down the previous module's tracker before patching pushState. This eliminates stacked wrappers (N page views per navigation).
  • Guards logPageView() in completeSDKInitialization behind window.__mpApvInitPVLogged__ so the initial page view fires only once per hard page load, not on every mParticle.init() call.
  • Fixes _resetForTests to tear down any active PageViewTracker and clear both APV window flags between tests, preventing stale state from leaking across test cases.

Test plan

  • Verify a single page view is logged on hard load with AutoLogPageView: True
  • Verify SPA navigation (pushState) logs exactly one page view per route change
  • Verify calling mParticle.init() again on re-navigation does not fire a duplicate initial page view
  • npm test passes (2176 tests, 0 failures)

Notes

The companion PR for kit double-registration via configuredForwarders accumulation is being reviewed separately as a draft.

@alexs-mparticle
alexs-mparticle requested a review from a team as a code owner August 18, 2026 20:56
@cursor

cursor Bot commented Aug 18, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes global history patching and analytics event timing for AutoLogPageView; behavior is scoped to that feature but affects all SPA customers using it.

Overview
Fixes duplicate auto page views and stacked history wrappers when SPAs call mParticle.init() repeatedly or when frameworks like Next.js re-execute the SDK bundle on client navigation.

PageViewTracker now keeps tab-scoped state on window: __mpApvTracker__ for the active tracker and __mpApvInitPVLogged__ so the initial logPageView() runs only once per hard page load. On init(), any tracker left from a prior module load is torn down before patching pushState/replaceState; teardown clears the window registration and history restoration is refactored via restoreHistoryMethod.

SDK init creates the tracker only when missing, gates the first page view behind the init flag, still calls init() on re-init when a tracker already exists, and tears the tracker down if AutoLogPageView is turned off. _resetForTests tears down the instance tracker and calls PageViewTracker.resetWindowState so APV window flags do not leak between tests.

New unit and integration tests cover SPA re-init, Next.js-style module re-evaluation, stale tracker teardown, and pending deferred page views.

Reviewed by Cursor Bugbot for commit 1d6bdc4. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread src/pageViewTracker.ts Outdated
Comment thread src/pageViewTracker.ts
Two root causes fixed:

1. mParticle.init() re-called by SPA framework (e.g. on each route change):
   logPageView() fired on every completeSDKInitialization even when the
   PageViewTracker was already active. Fixed by guarding behind a
   window.__mpApvInitPVLogged__ flag (survives module re-evaluation) so the
   initial page view fires only once per hard page load.

2. Next.js re-evaluates the SDK module on each SPA navigation, resetting all
   module-level state. Each fresh module created a new PageViewTracker and
   patched window.history.pushState on top of the previous wrapper, stacking
   N wrappers after N navigations and firing N page views per click. Fixed by
   storing the active tracker on window.__mpApvTracker__ and tearing down any
   stale tracker from a previous module load before installing the new one.

_resetForTests now directly deletes both APV window flags so stale state
cannot leak across test cases regardless of instance assignment.

Adds a regression test for the re-init duplicate case.
@alexs-mparticle
alexs-mparticle force-pushed the fix/apv-reinit-duplicate-page-view branch from 40352d1 to 7257d7e Compare August 19, 2026 15:52

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7257d7e. Configure here.

Comment thread src/mp-instance.ts
Comment thread src/mp-instance.ts

@rmi22186 rmi22186 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-reviewed with multiple-instance support explicitly treated as out of scope for this Rokt/legacy APV feature. I found two lifecycle edge cases and a test-coverage gap. One additional question: development already marks wrappers with __mpApvWrapped__ and skips re-wrapping, so it would be useful to confirm whether the observed production duplication is from accumulated listeners/stale ownership rather than stacked pushState wrappers.

Comment thread src/pageViewTracker.ts
const prev = win[WIN_TRACKER_KEY];
if (prev && prev !== this) {
this.mpInstance.Logger.verbose(
'mParticle APV: [init] found stale tracker from previous module load — tearing down to prevent stacked wrappers'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could this handoff preserve or flush a page view already queued by prev? A route change queues its event with setTimeout; if module re-evaluation reaches this synchronous init before that timer runs, teardown() marks the previous tracker inactive and its callback aborts. The replacement tracker seeds the destination as lastPath, so that navigation is never logged. A regression test where the old tracker detects a route, the replacement initializes, and then timers flush would cover the target scenario.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment thread src/mp-instance.ts Outdated
}
const win = window as WindowWithApvFlags;
delete win[WIN_INIT_PV_KEY];
delete win[WIN_TRACKER_KEY];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This can delete the only reference to an active stale tracker without tearing it down. After module re-evaluation, instance._PageViewTracker may be absent or differ from win[WIN_TRACKER_KEY]; in that case its history wrapper/listener remains installed but becomes undiscoverable. Please teardown the tracker referenced by the window registry before clearing it (while avoiding a second teardown when both references are the same).

);
});

it('should not log a duplicate page view when init() is called again (SPA re-init)', async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This verifies repeated init() on the same SDK instance, where _PageViewTracker already exists. It does not exercise the new window-level handoff or WIN_INIT_PV_KEY path used when a fresh module creates a fresh tracker. Could we add a focused module-replacement test with an old tracker stored on window, including a pending route event and cleanup of stale global state?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment thread src/mp-instance.ts Outdated
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants