@mparticle/web-sdk 2.79.0 → 2.79.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,72 @@
1
1
  import Types from './types';
2
2
  import Constants from './constants';
3
3
  import * as utils from './utils';
4
+ import { Dictionary } from './utils';
4
5
  import Validators from './validators';
5
6
  import KitFilterHelper from './kitFilterHelper';
7
+ import { IMParticleWebSDKInstance } from './mp-instance';
8
+ import { SDKHelpersApi } from './sdkRuntimeModels';
9
+ import { IMParticleUser, IdentityCallback, IdentityResult, ISDKUserIdentity } from './identity-user-interfaces';
10
+ import { IAliasResult } from './identity.interfaces';
11
+ import { AliasUsersCallback, MPID } from '@mparticle/web-sdk';
6
12
 
7
- var StorageNames = Constants.StorageNames;
13
+ const StorageNames = Constants.StorageNames;
8
14
 
9
- export default function Helpers(mpInstance) {
10
- var self = this;
11
- this.canLog = function() {
15
+ function appendFilteredUserIdentity(
16
+ filteredUserIdentities: ISDKUserIdentity[],
17
+ identity: ISDKUserIdentity,
18
+ userIdentityType: number
19
+ ): void {
20
+ if (userIdentityType === Types.IdentityType.CustomerId) {
21
+ filteredUserIdentities.unshift(identity);
22
+ } else {
23
+ filteredUserIdentities.push(identity);
24
+ }
25
+ }
26
+
27
+ function buildFilteredUserIdentities(
28
+ userIdentitiesObject: utils.Dictionary<string> | null | undefined,
29
+ filterList: number[],
30
+ inArray: (items: any[], value: any) => boolean
31
+ ): ISDKUserIdentity[] {
32
+ const filteredUserIdentities: ISDKUserIdentity[] = [];
33
+
34
+ if (!userIdentitiesObject || !Object.keys(userIdentitiesObject).length) {
35
+ return filteredUserIdentities;
36
+ }
37
+
38
+ for (const userIdentityName in userIdentitiesObject) {
39
+ if (!userIdentitiesObject.hasOwnProperty(userIdentityName)) {
40
+ continue;
41
+ }
42
+
43
+ const userIdentityType = Types.IdentityType.getIdentityType(
44
+ userIdentityName
45
+ );
46
+
47
+ if (inArray(filterList, userIdentityType)) {
48
+ continue;
49
+ }
50
+
51
+ appendFilteredUserIdentity(
52
+ filteredUserIdentities,
53
+ {
54
+ Type: userIdentityType,
55
+ Identity: userIdentitiesObject[userIdentityName],
56
+ },
57
+ userIdentityType
58
+ );
59
+ }
60
+
61
+ return filteredUserIdentities;
62
+ }
63
+
64
+ export default function Helpers(
65
+ this: SDKHelpersApi,
66
+ mpInstance: IMParticleWebSDKInstance
67
+ ): void {
68
+ const self = this;
69
+ this.canLog = function(): boolean {
12
70
  if (
13
71
  mpInstance._Store.isEnabled &&
14
72
  (mpInstance._Store.devToken ||
@@ -20,7 +78,7 @@ export default function Helpers(mpInstance) {
20
78
  return false;
21
79
  };
22
80
 
23
- this.getFeatureFlag = function(feature) {
81
+ this.getFeatureFlag = function(feature: string): boolean | string {
24
82
  if (mpInstance._Store.SDKConfig.flags.hasOwnProperty(feature)) {
25
83
  return mpInstance._Store.SDKConfig.flags[feature];
26
84
  }
@@ -28,12 +86,12 @@ export default function Helpers(mpInstance) {
28
86
  };
29
87
 
30
88
  this.invokeCallback = function(
31
- callback,
32
- code,
33
- body,
34
- mParticleUser,
35
- previousMpid
36
- ) {
89
+ callback: IdentityCallback,
90
+ code: number,
91
+ body: string,
92
+ mParticleUser?: IMParticleUser,
93
+ previousMpid?: MPID
94
+ ): void {
37
95
  if (!callback) {
38
96
  mpInstance.Logger.warning('There is no callback provided');
39
97
  }
@@ -51,9 +109,9 @@ export default function Helpers(mpInstance) {
51
109
  },
52
110
  getPreviousUser: function() {
53
111
  if (!previousMpid) {
54
- var users = mpInstance.Identity.getUsers();
55
- var mostRecentUser = users.shift();
56
- var currentUser =
112
+ const users = mpInstance.Identity.getUsers();
113
+ let mostRecentUser = users.shift();
114
+ const currentUser =
57
115
  mParticleUser ||
58
116
  mpInstance.Identity.getCurrentUser();
59
117
  if (
@@ -69,7 +127,7 @@ export default function Helpers(mpInstance) {
69
127
  return mpInstance.Identity.getUser(previousMpid);
70
128
  }
71
129
  },
72
- });
130
+ } as unknown as IdentityResult);
73
131
  }
74
132
  } catch (e) {
75
133
  mpInstance.Logger.error(
@@ -78,19 +136,23 @@ export default function Helpers(mpInstance) {
78
136
  }
79
137
  };
80
138
 
81
- this.invokeAliasCallback = function(callback, code, message) {
139
+ this.invokeAliasCallback = function(
140
+ callback: AliasUsersCallback,
141
+ code: number,
142
+ message?: string
143
+ ): void {
82
144
  if (!callback) {
83
145
  mpInstance.Logger.warning('There is no callback provided');
84
146
  }
85
147
  try {
86
148
  if (self.Validators.isFunction(callback)) {
87
- var callbackMessage = {
149
+ const callbackMessage: Dictionary = {
88
150
  httpCode: code,
89
151
  };
90
152
  if (message) {
91
153
  callbackMessage.message = message;
92
154
  }
93
- callback(callbackMessage);
155
+ callback(callbackMessage as IAliasResult);
94
156
  }
95
157
  } catch (e) {
96
158
  mpInstance.Logger.error(
@@ -101,12 +163,15 @@ export default function Helpers(mpInstance) {
101
163
 
102
164
  this.extend = utils.extend;
103
165
 
104
- this.createServiceUrl = function(secureServiceUrl, devToken) {
105
- var serviceScheme =
166
+ this.createServiceUrl = function(
167
+ secureServiceUrl: string,
168
+ devToken?: string
169
+ ): string {
170
+ const serviceScheme =
106
171
  window.mParticle && mpInstance._Store.SDKConfig.forceHttps
107
172
  ? 'https://'
108
173
  : window.location.protocol + '//';
109
- var baseUrl;
174
+ let baseUrl;
110
175
  if (mpInstance._Store.SDKConfig.forceHttps) {
111
176
  baseUrl = 'https://' + secureServiceUrl;
112
177
  } else {
@@ -118,8 +183,8 @@ export default function Helpers(mpInstance) {
118
183
  return baseUrl;
119
184
  };
120
185
 
121
- this.createXHR = function(cb) {
122
- var xhr;
186
+ this.createXHR = function(cb: () => void): XMLHttpRequest {
187
+ let xhr;
123
188
 
124
189
  try {
125
190
  xhr = new window.XMLHttpRequest();
@@ -129,11 +194,13 @@ export default function Helpers(mpInstance) {
129
194
 
130
195
  if (xhr && cb && 'withCredentials' in xhr) {
131
196
  xhr.onreadystatechange = cb;
132
- } else if (typeof window.XDomainRequest !== 'undefined') {
197
+ } else if (
198
+ typeof (window as Dictionary).XDomainRequest !== 'undefined'
199
+ ) {
133
200
  mpInstance.Logger.verbose('Creating XDomainRequest object');
134
201
 
135
202
  try {
136
- xhr = new window.XDomainRequest();
203
+ xhr = new (window as Dictionary).XDomainRequest();
137
204
  xhr.onload = cb;
138
205
  } catch (e) {
139
206
  mpInstance.Logger.error('Error creating XDomainRequest object');
@@ -143,41 +210,25 @@ export default function Helpers(mpInstance) {
143
210
  return xhr;
144
211
  };
145
212
 
146
- this.filterUserIdentities = function(userIdentitiesObject, filterList) {
147
- var filteredUserIdentities = [];
148
-
149
- if (userIdentitiesObject && Object.keys(userIdentitiesObject).length) {
150
- for (var userIdentityName in userIdentitiesObject) {
151
- if (userIdentitiesObject.hasOwnProperty(userIdentityName)) {
152
- var userIdentityType = Types.IdentityType.getIdentityType(
153
- userIdentityName
154
- );
155
- if (!self.inArray(filterList, userIdentityType)) {
156
- var identity = {
157
- Type: userIdentityType,
158
- Identity: userIdentitiesObject[userIdentityName],
159
- };
160
- if (
161
- userIdentityType === Types.IdentityType.CustomerId
162
- ) {
163
- filteredUserIdentities.unshift(identity);
164
- } else {
165
- filteredUserIdentities.push(identity);
166
- }
167
- }
168
- }
169
- }
170
- }
171
-
172
- return filteredUserIdentities;
213
+ this.filterUserIdentities = function(
214
+ userIdentitiesObject: Dictionary<string>,
215
+ filterList: number[]
216
+ ): ISDKUserIdentity[] {
217
+ return buildFilteredUserIdentities(
218
+ userIdentitiesObject,
219
+ filterList,
220
+ self.inArray
221
+ );
173
222
  };
174
223
 
175
224
  this.filterUserIdentitiesForForwarders =
176
225
  KitFilterHelper.filterUserIdentities;
177
226
  this.filterUserAttributes = KitFilterHelper.filterUserAttributes;
178
227
 
179
- this.isEventType = function(type) {
180
- for (var prop in Types.EventType) {
228
+ this.isEventType = function(
229
+ type: utils.valueof<typeof Types.EventType>
230
+ ): boolean {
231
+ for (const prop in Types.EventType) {
181
232
  if (Types.EventType.hasOwnProperty(prop)) {
182
233
  if (Types.EventType[prop] === type) {
183
234
  return true;
@@ -187,14 +238,17 @@ export default function Helpers(mpInstance) {
187
238
  return false;
188
239
  };
189
240
 
190
- this.sanitizeAttributes = function(attrs, name) {
241
+ this.sanitizeAttributes = function(
242
+ attrs: Dictionary,
243
+ name: string
244
+ ): Dictionary<string> | null {
191
245
  if (!attrs || !self.isObject(attrs)) {
192
246
  return null;
193
247
  }
194
248
 
195
- var sanitizedAttrs = {};
249
+ const sanitizedAttrs: Dictionary<string> = {};
196
250
 
197
- for (var prop in attrs) {
251
+ for (const prop in attrs) {
198
252
  // Make sure that attribute values are not objects or arrays, which are not valid
199
253
  if (
200
254
  attrs.hasOwnProperty(prop) &&
@@ -216,17 +270,17 @@ export default function Helpers(mpInstance) {
216
270
  };
217
271
 
218
272
  this.isDelayedByIntegration = function(
219
- delayedIntegrations,
220
- timeoutStart,
221
- now
222
- ) {
273
+ delayedIntegrations: Dictionary<boolean>,
274
+ timeoutStart: number,
275
+ now: number
276
+ ): boolean {
223
277
  if (
224
278
  now - timeoutStart >
225
279
  mpInstance._Store.SDKConfig.integrationDelayTimeout
226
280
  ) {
227
281
  return false;
228
282
  }
229
- for (var integration in delayedIntegrations) {
283
+ for (const integration in delayedIntegrations) {
230
284
  if (delayedIntegrations[integration] === true) {
231
285
  return true;
232
286
  } else {
@@ -236,7 +290,7 @@ export default function Helpers(mpInstance) {
236
290
  return false;
237
291
  };
238
292
 
239
- this.createMainStorageName = function(workspaceToken) {
293
+ this.createMainStorageName = function(workspaceToken: string): string {
240
294
  if (workspaceToken) {
241
295
  return StorageNames.currentStorageName + '_' + workspaceToken;
242
296
  } else {
@@ -297,6 +297,14 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
297
297
  window.localStorage
298
298
  );
299
299
  instance._Events.stopTracking();
300
+
301
+ const instanceTracker = instance._PageViewTracker;
302
+ if (instanceTracker) {
303
+ instanceTracker.teardown();
304
+ instance._PageViewTracker = undefined;
305
+ }
306
+ PageViewTracker.resetWindowState(instanceTracker);
307
+
300
308
  if (!keepPersistence) {
301
309
  instance._Persistence.resetPersistence();
302
310
  }
@@ -1585,14 +1593,36 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1585
1593
  mpInstance._SessionManager.initialize();
1586
1594
  mpInstance._Events.logAST();
1587
1595
 
1596
+ // Note: APV uses window-level singletons (WIN_TRACKER_KEY, WIN_INIT_PV_KEY)
1597
+ // that assume a single active SDK instance. Multiple-instance support is
1598
+ // out of scope
1588
1599
  if (getFeatureFlag(AutoLogPageView)) {
1589
- mpInstance._Events.logPageView();
1590
-
1591
1600
  if (!mpInstance._PageViewTracker) {
1601
+ mpInstance.Logger.verbose(
1602
+ 'mParticle APV: [sdk-init] creating new PageViewTracker for this instance'
1603
+ );
1592
1604
  mpInstance._PageViewTracker = new PageViewTracker(mpInstance);
1605
+
1606
+ // Fire the initial page view once per page load (hard navigation).
1607
+ // The flag survives module re-evaluation so repeated init() calls
1608
+ // from SPA re-renders don't fire duplicate logPageView() events.
1609
+ if (PageViewTracker.hasInitialPageViewFired()) {
1610
+ mpInstance.Logger.verbose(
1611
+ 'mParticle APV: [sdk-init] initial page view already logged this page load, skipping'
1612
+ );
1613
+ } else {
1614
+ PageViewTracker.markInitialPageViewFired();
1615
+ mpInstance._Events.logPageView();
1616
+ }
1617
+ } else {
1618
+ mpInstance.Logger.verbose(
1619
+ 'mParticle APV: [sdk-init] tracker already on this instance, skipping logPageView'
1620
+ );
1593
1621
  }
1594
1622
  mpInstance._PageViewTracker.init();
1595
1623
  } else if (mpInstance._PageViewTracker) {
1624
+ // AutoLogPageView was disabled on re-init: tear down the active tracker
1625
+ // so it stops listening. Re-created if the flag is re-enabled later.
1596
1626
  mpInstance._PageViewTracker.teardown();
1597
1627
  }
1598
1628
 
@@ -9,11 +9,59 @@ type MarkedHistoryMethod = HistoryStateMethod & {
9
9
  [WRAPPED_MARKER]?: boolean;
10
10
  };
11
11
 
12
+ // window key under which the single active tracker is stored.
13
+ // Survives module re-evaluation (Next.js re-executes the bundle on every SPA
14
+ // navigation, resetting all module-level state, but window persists for the
15
+ // lifetime of the tab).
16
+ export const WIN_TRACKER_KEY = '__mpApvTracker__';
17
+
18
+ // Guards the initial page view so repeated mParticle.init() calls from SPA
19
+ // re-renders don't fire duplicate logPageView() events for the same page.
20
+ export const WIN_INIT_PV_KEY = '__mpApvInitPVLogged__' as const;
21
+
22
+ export type WindowWithApvFlags = Window & {
23
+ [WIN_TRACKER_KEY]?: PageViewTracker;
24
+ [WIN_INIT_PV_KEY]?: boolean;
25
+ };
26
+
27
+ type NavigationSource = 'pushState' | 'replaceState' | 'popstate';
28
+
12
29
  export class PageViewTracker {
30
+ static hasInitialPageViewFired(): boolean {
31
+ return !!(window as WindowWithApvFlags)[WIN_INIT_PV_KEY];
32
+ }
33
+
34
+ static markInitialPageViewFired(): void {
35
+ (window as WindowWithApvFlags)[WIN_INIT_PV_KEY] = true;
36
+ }
37
+
38
+ // Tears down any window-registered tracker that differs from instanceTracker
39
+ // (e.g. left by a previous module load), then clears both APV window flags.
40
+ // Call from _resetForTests after tearing down the instance-level tracker.
41
+ static resetWindowState(instanceTracker?: PageViewTracker): void {
42
+ const windowTracker = getWindowTracker();
43
+ if (windowTracker && windowTracker !== instanceTracker) {
44
+ windowTracker.teardown();
45
+ }
46
+ if (typeof window !== 'undefined') {
47
+ delete (window as WindowWithApvFlags)[WIN_INIT_PV_KEY];
48
+ }
49
+ setWindowTracker(undefined);
50
+ }
51
+
13
52
  mpInstance: IMParticleWebSDKInstance;
14
53
 
15
54
  private lastPath: string | null = null;
16
- private isActive = false;
55
+ private _isActive = false;
56
+
57
+ get isActive(): boolean {
58
+ return this._isActive;
59
+ }
60
+
61
+ private pendingNavigations: Array<{
62
+ path: string;
63
+ timeoutId: ReturnType<typeof setTimeout>;
64
+ }> = [];
17
65
 
18
66
  private originalPushState: HistoryStateMethod | null = null;
19
67
  private originalReplaceState: HistoryStateMethod | null = null;
@@ -37,9 +85,7 @@ export class PageViewTracker {
37
85
  }
38
86
 
39
87
  public init(): void {
40
- this.mpInstance.Logger.verbose(
41
- 'mParticle APV: [init] PageViewTracker Init'
42
- );
88
+ this.mpInstance.Logger.verbose('mParticle APV: [init] PageViewTracker Init');
43
89
  if (!this.isSupportedEnvironment()) {
44
90
  this.mpInstance.Logger.verbose(
45
91
  'mParticle APV: [init] unsupported environment (no History API), not starting'
@@ -47,14 +93,32 @@ export class PageViewTracker {
47
93
  return;
48
94
  }
49
95
 
50
- if (this.isActive) {
96
+ // Tear down any tracker left over from a previous module load.
97
+ // window[WIN_TRACKER_KEY] outlives module re-evaluation; this is the
98
+ // only way to reach a tracker instance in a dead module scope.
99
+ // Transfer any pending navigations first so a route change that queued a
100
+ // deferred fire before module re-evaluation occurs is not silently dropped.
101
+ const prev = getWindowTracker();
102
+ let inheritedPaths: string[] = [];
103
+ if (prev && prev !== this) {
104
+ this.mpInstance.Logger.verbose(
105
+ 'mParticle APV: [init] found stale tracker from previous module load — transferring pending navigations and tearing down'
106
+ );
107
+ inheritedPaths = prev.takePendingNavigations();
108
+ prev.teardown();
109
+ }
110
+
111
+ if (this._isActive) {
51
112
  this.mpInstance.Logger.verbose(
52
113
  'mParticle APV: [init] starting (teardown-first for idempotency)'
53
114
  );
115
+ // Preserve pending navigations through the self-teardown so a route
116
+ // change queued before mParticle.init() was called again is not lost.
117
+ inheritedPaths = [...inheritedPaths, ...this.takePendingNavigations()];
54
118
  this.teardown();
55
119
  }
56
120
 
57
- this.isActive = true;
121
+ this._isActive = true;
58
122
 
59
123
  this.lastPath = this.getCurrentKey();
60
124
 
@@ -65,9 +129,26 @@ export class PageViewTracker {
65
129
  this.patchHistoryMethods();
66
130
  this.addNavigationListeners();
67
131
 
132
+ setWindowTracker(this);
133
+
68
134
  this.mpInstance.Logger.verbose(
69
135
  'mParticle APV: [init] patched pushState/replaceState + listening for popstate'
70
136
  );
137
+
138
+ // Re-fire navigations that the stale tracker had queued but not yet flushed
139
+ // when module re-evaluation occurred. Title is read at flush time so the
140
+ // router's post-navigation render commit still has a chance to update it.
141
+ inheritedPaths.forEach(path => {
142
+ const timeoutId = setTimeout(() => {
143
+ this.pendingNavigations = this.pendingNavigations.filter(
144
+ p => p.timeoutId !== timeoutId
145
+ );
146
+ if (!this._isActive) return;
147
+ this.mpInstance._SessionManager.resetSessionTimer();
148
+ this.firePageView(path);
149
+ }, 0);
150
+ this.pendingNavigations.push({ path, timeoutId });
151
+ });
71
152
  }
72
153
 
73
154
  private patchHistoryMethods(): void {
@@ -76,7 +157,7 @@ export class PageViewTracker {
76
157
  const installed = window.history.pushState as MarkedHistoryMethod;
77
158
  if (installed[WRAPPED_MARKER]) {
78
159
  this.mpInstance.Logger.verbose(
79
- 'mParticle APV: [patch] history already wrapped, skipping to avoid double-wrap'
160
+ 'mParticle APV: [patch] history already wrapped, skipping to avoid double-wrap — STACKED WRAPPER DETECTED'
80
161
  );
81
162
  return;
82
163
  }
@@ -91,7 +172,9 @@ export class PageViewTracker {
91
172
  ...args: Parameters<HistoryStateMethod>
92
173
  ): void {
93
174
  const result = originalPushState.apply(this, args);
94
- self.safeHandleNavigation('pushState');
175
+ if (self._isActive) {
176
+ self.safeHandleNavigation('pushState');
177
+ }
95
178
  return result;
96
179
  };
97
180
 
@@ -100,7 +183,9 @@ export class PageViewTracker {
100
183
  ...args: Parameters<HistoryStateMethod>
101
184
  ): void {
102
185
  const result = originalReplaceState.apply(this, args);
103
- self.safeHandleNavigation('replaceState');
186
+ if (self._isActive) {
187
+ self.safeHandleNavigation('replaceState');
188
+ }
104
189
  return result;
105
190
  };
106
191
 
@@ -147,7 +232,7 @@ export class PageViewTracker {
147
232
  window.addEventListener('popstate', this.popStateListener);
148
233
  }
149
234
 
150
- private safeHandleNavigation(source: string): void {
235
+ private safeHandleNavigation(source: NavigationSource): void {
151
236
  try {
152
237
  this.handleNavigation(source);
153
238
  } catch (e) {
@@ -161,7 +246,7 @@ export class PageViewTracker {
161
246
  return window.location.pathname;
162
247
  }
163
248
 
164
- private handleNavigation(source: string): void {
249
+ private handleNavigation(source: NavigationSource): void {
165
250
  const candidatePath = this.getCurrentKey();
166
251
 
167
252
  this.mpInstance.Logger.verbose(
@@ -187,8 +272,11 @@ export class PageViewTracker {
187
272
  // commit has a chance to update document.title first.
188
273
  const capturedPath = candidatePath;
189
274
 
190
- setTimeout(() => {
191
- if (!this.isActive) {
275
+ const timeoutId = setTimeout(() => {
276
+ this.pendingNavigations = this.pendingNavigations.filter(
277
+ p => p.timeoutId !== timeoutId
278
+ );
279
+ if (!this._isActive) {
192
280
  this.mpInstance.Logger.verbose(
193
281
  'mParticle APV: [defer] fire aborted, tracker inactive (torn down before flush)'
194
282
  );
@@ -198,6 +286,7 @@ export class PageViewTracker {
198
286
  this.mpInstance._SessionManager.resetSessionTimer();
199
287
  this.firePageView(capturedPath);
200
288
  }, 0);
289
+ this.pendingNavigations.push({ path: capturedPath, timeoutId });
201
290
  }
202
291
 
203
292
  // Mirrors the event shape of the public mParticle.logPageView(), but carries
@@ -221,47 +310,73 @@ export class PageViewTracker {
221
310
  });
222
311
  }
223
312
 
313
+ // Cancels pending deferred fires and returns their captured paths so a
314
+ // successor tracker can re-schedule them. Call before teardown() during
315
+ // module-replacement handoff.
316
+ public takePendingNavigations(): string[] {
317
+ const paths = this.pendingNavigations.map(p => p.path);
318
+ this.pendingNavigations.forEach(p => clearTimeout(p.timeoutId));
319
+ this.pendingNavigations = [];
320
+ return paths;
321
+ }
322
+
323
+ private restoreHistoryMethod(
324
+ methodName: 'pushState' | 'replaceState',
325
+ original: HistoryStateMethod | null,
326
+ wrapper: HistoryStateMethod | null
327
+ ): void {
328
+ if (!original) return;
329
+ const stillOurs = wrapper !== null && window.history[methodName] === wrapper;
330
+ if (stillOurs) {
331
+ window.history[methodName] = original;
332
+ this.mpInstance.Logger.verbose(
333
+ `mParticle APV: [teardown] restored original ${methodName}`
334
+ );
335
+ } else {
336
+ this.mpInstance.Logger.verbose(
337
+ `mParticle APV: [teardown] ${methodName} no longer ours; leaving in place, gating callback to no-op`
338
+ );
339
+ }
340
+ }
341
+
224
342
  public teardown(): void {
343
+ // Cancel any pending deferred fires (e.g. when the feature flag is
344
+ // disabled on re-init). For module-replacement, call takePendingNavigations()
345
+ // before teardown() instead so paths are transferred, not discarded.
346
+ this.pendingNavigations.forEach(p => clearTimeout(p.timeoutId));
347
+ this.pendingNavigations = [];
348
+
225
349
  if (this.popStateListener) {
226
350
  window.removeEventListener('popstate', this.popStateListener);
227
351
  this.popStateListener = null;
228
352
  }
229
- const pushStateStillOurs =
230
- this.pushStateWrapper !== null &&
231
- window.history.pushState === this.pushStateWrapper;
232
- if (this.originalPushState) {
233
- if (pushStateStillOurs) {
234
- window.history.pushState = this.originalPushState;
235
- this.mpInstance.Logger.verbose(
236
- 'mParticle APV: [teardown] restored original pushState'
237
- );
238
- } else {
239
- this.mpInstance.Logger.verbose(
240
- 'mParticle APV: [teardown] pushState no longer ours; leaving in place, gating callback to no-op'
241
- );
242
- }
243
- this.originalPushState = null;
244
- this.pushStateWrapper = null;
245
- }
246
353
 
247
- const replaceStateStillOurs =
248
- this.replaceStateWrapper !== null &&
249
- window.history.replaceState === this.replaceStateWrapper;
250
- if (this.originalReplaceState) {
251
- if (replaceStateStillOurs) {
252
- window.history.replaceState = this.originalReplaceState;
253
- this.mpInstance.Logger.verbose(
254
- 'mParticle APV: [teardown] restored original replaceState'
255
- );
256
- } else {
257
- this.mpInstance.Logger.verbose(
258
- 'mParticle APV: [teardown] replaceState no longer ours; leaving in place, gating callback to no-op'
259
- );
260
- }
261
- this.originalReplaceState = null;
262
- this.replaceStateWrapper = null;
354
+ this.restoreHistoryMethod('pushState', this.originalPushState, this.pushStateWrapper);
355
+ this.originalPushState = null;
356
+ this.pushStateWrapper = null;
357
+
358
+ this.restoreHistoryMethod('replaceState', this.originalReplaceState, this.replaceStateWrapper);
359
+ this.originalReplaceState = null;
360
+ this.replaceStateWrapper = null;
361
+
362
+ this._isActive = false;
363
+ if (getWindowTracker() === this) {
364
+ setWindowTracker(undefined);
263
365
  }
366
+ }
367
+ }
368
+
369
+ function getWindowTracker(): PageViewTracker | undefined {
370
+ if (typeof window === 'undefined') return undefined;
371
+ return (window as WindowWithApvFlags)[WIN_TRACKER_KEY];
372
+ }
264
373
 
265
- this.isActive = false;
374
+ function setWindowTracker(tracker: PageViewTracker | undefined): void {
375
+ if (typeof window === 'undefined') return;
376
+ const win = window as WindowWithApvFlags;
377
+ if (tracker) {
378
+ win[WIN_TRACKER_KEY] = tracker;
379
+ } else {
380
+ delete win[WIN_TRACKER_KEY];
266
381
  }
267
382
  }