@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.
- package/dist/mparticle.common.js +21 -10
- package/dist/mparticle.esm.js +21 -10
- package/dist/mparticle.js +198 -65
- package/dist/types/src/ecommerce.interfaces.d.ts +8 -7
- package/dist/types/src/events.d.ts +3 -18
- package/dist/types/src/events.interfaces.d.ts +2 -2
- package/dist/types/src/helpers.d.ts +3 -54
- package/dist/types/src/pageViewTracker.d.ts +14 -1
- package/dist/types/src/polyfill.d.ts +13 -14
- package/dist/types/src/sdkRuntimeModels.d.ts +10 -3
- package/dist/types/src/store.d.ts +1 -0
- package/package.json +1 -1
- package/src/ecommerce.interfaces.ts +12 -8
- package/src/events.interfaces.ts +3 -3
- package/src/{events.js → events.ts} +152 -82
- package/src/{helpers.js → helpers.ts} +117 -63
- package/src/mp-instance.ts +32 -2
- package/src/pageViewTracker.ts +162 -47
- package/src/{polyfill.js → polyfill.ts} +36 -36
- package/src/sdkRuntimeModels.ts +19 -3
- package/src/store.ts +1 -0
|
@@ -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
|
-
|
|
13
|
+
const StorageNames = Constants.StorageNames;
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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(
|
|
180
|
-
|
|
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(
|
|
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
|
-
|
|
249
|
+
const sanitizedAttrs: Dictionary<string> = {};
|
|
196
250
|
|
|
197
|
-
for (
|
|
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 (
|
|
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 {
|
package/src/mp-instance.ts
CHANGED
|
@@ -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
|
|
package/src/pageViewTracker.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
}
|