@mparticle/web-sdk 2.79.2 → 2.79.3
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/README.md +52 -0
- package/dist/mparticle.common.js +69 -28
- package/dist/mparticle.esm.js +69 -28
- package/dist/mparticle.js +375 -318
- package/dist/types/src/filteredMparticleUser.d.ts +15 -7
- package/dist/types/src/forwardingStatsUploader.d.ts +4 -4
- package/dist/types/src/mp-instance.d.ts +3 -2
- package/dist/types/src/nativeSdkHelpers.d.ts +3 -12
- package/dist/types/src/nativeSdkHelpers.interfaces.d.ts +15 -1
- package/dist/types/src/pageViewTracker.d.ts +27 -23
- package/dist/types/src/sdkRuntimeModels.d.ts +4 -3
- package/dist/types/src/store.d.ts +2 -0
- package/package.json +1 -1
- package/src/filteredMparticleUser.ts +160 -0
- package/src/{forwardingStatsUploader.js → forwardingStatsUploader.ts} +25 -12
- package/src/mp-instance.ts +27 -25
- package/src/nativeSdkHelpers.interfaces.ts +21 -1
- package/src/{nativeSdkHelpers.js → nativeSdkHelpers.ts} +59 -34
- package/src/pageViewTracker.ts +339 -279
- package/src/sdkRuntimeModels.ts +4 -3
- package/src/store.ts +2 -0
- package/src/filteredMparticleUser.js +0 -101
package/src/pageViewTracker.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { IMParticleWebSDKInstance } from './mp-instance';
|
|
2
|
+
import { BaseEvent } from './sdkRuntimeModels';
|
|
2
3
|
import { EventType, MessageType } from './types';
|
|
3
4
|
|
|
4
5
|
type HistoryStateMethod = History['pushState'];
|
|
6
|
+
type HistoryMethodName = 'pushState' | 'replaceState';
|
|
7
|
+
type NavigationSource = HistoryMethodName | 'popstate';
|
|
8
|
+
|
|
9
|
+
const HISTORY_METHODS: HistoryMethodName[] = ['pushState', 'replaceState'];
|
|
5
10
|
|
|
6
11
|
const WRAPPED_MARKER = '__mpApvWrapped__';
|
|
7
12
|
|
|
@@ -9,374 +14,429 @@ type MarkedHistoryMethod = HistoryStateMethod & {
|
|
|
9
14
|
[WRAPPED_MARKER]?: boolean;
|
|
10
15
|
};
|
|
11
16
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
+
// All APV state hangs off one window key, and it is the public debugging
|
|
18
|
+
// contract: `window.__mpApv__` is what you inspect in a console to see whether
|
|
19
|
+
// tracking is live.
|
|
20
|
+
//
|
|
21
|
+
// It lives on `window` rather than in module scope because Next.js re-executes
|
|
22
|
+
// the SDK bundle on every SPA navigation: module-level state is reset each time,
|
|
23
|
+
// but `window` persists for the lifetime of the tab.
|
|
24
|
+
//
|
|
25
|
+
// One object rather than a key per flag, so resetting is a single reassignment.
|
|
26
|
+
// Nothing has to be deleted, and there is no ordering in which half the state
|
|
27
|
+
// survives the reset.
|
|
28
|
+
export const WIN_APV_KEY = '__mpApv__';
|
|
29
|
+
|
|
30
|
+
interface IApvState {
|
|
31
|
+
// The tracker that currently owns the history patch. May have been
|
|
32
|
+
// registered by a module scope that no longer exists.
|
|
33
|
+
tracker?: PageViewTracker;
|
|
34
|
+
|
|
35
|
+
// Guards the initial page view so repeated mParticle.init() calls from SPA
|
|
36
|
+
// re-renders don't fire duplicate logPageView() events for the same page.
|
|
37
|
+
initialPageViewFired: boolean;
|
|
38
|
+
}
|
|
17
39
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
40
|
+
type WindowWithApv = Window & {
|
|
41
|
+
[WIN_APV_KEY]?: IApvState;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
interface IPageViewData {
|
|
45
|
+
hostname: string;
|
|
46
|
+
title: string;
|
|
47
|
+
path: string;
|
|
48
|
+
}
|
|
21
49
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
50
|
+
interface IPendingNavigation {
|
|
51
|
+
path: string;
|
|
52
|
+
timeoutId: ReturnType<typeof setTimeout>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Pure helpers. No `this`, no globals, no side effects — callable and assertable
|
|
57
|
+
// on their own, which is where the interesting rules live.
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
// Dedup keys on pathname only: a query-string- or hash-only change is the same
|
|
61
|
+
// page and must not fire a view.
|
|
62
|
+
export const isNewPage = (
|
|
63
|
+
lastPath: string | null,
|
|
64
|
+
candidatePath: string
|
|
65
|
+
): boolean => candidatePath !== lastPath;
|
|
66
|
+
|
|
67
|
+
export const supportsHistoryTracking = (win: Window | null): boolean =>
|
|
68
|
+
!!win &&
|
|
69
|
+
win.history !== undefined &&
|
|
70
|
+
typeof win.history.pushState === 'function' &&
|
|
71
|
+
typeof win.addEventListener === 'function';
|
|
72
|
+
|
|
73
|
+
// Mirrors the event shape of the public mParticle.logPageView(), but carries the
|
|
74
|
+
// path captured when the navigation was accepted rather than the live location.
|
|
75
|
+
export const buildPageViewEvent = (data: IPageViewData): BaseEvent => ({
|
|
76
|
+
messageType: MessageType.PageView,
|
|
77
|
+
name: 'PageView',
|
|
78
|
+
data: { ...data },
|
|
79
|
+
eventType: EventType.Unknown,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Window-scoped APV state. Every read and write goes through these helpers, so
|
|
84
|
+
// neither the tracker nor mp-instance touches `window` directly and
|
|
85
|
+
// `typeof window` is checked in exactly one place.
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
const apvWindow = (): WindowWithApv | null =>
|
|
89
|
+
typeof window === 'undefined' ? null : (window as WindowWithApv);
|
|
90
|
+
|
|
91
|
+
const freshState = (): IApvState => ({ initialPageViewFired: false });
|
|
92
|
+
|
|
93
|
+
// Reads tolerate absent state, so a read never mutates `window`. Only the
|
|
94
|
+
// writers below create it.
|
|
95
|
+
const readState = (): IApvState | undefined => {
|
|
96
|
+
const win = apvWindow();
|
|
97
|
+
return win ? win[WIN_APV_KEY] : undefined;
|
|
25
98
|
};
|
|
26
99
|
|
|
27
|
-
|
|
100
|
+
const writeState = (): IApvState | null => {
|
|
101
|
+
const win = apvWindow();
|
|
102
|
+
if (!win) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
28
105
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return !!(window as WindowWithApvFlags)[WIN_INIT_PV_KEY];
|
|
106
|
+
if (!win[WIN_APV_KEY]) {
|
|
107
|
+
win[WIN_APV_KEY] = freshState();
|
|
32
108
|
}
|
|
33
109
|
|
|
34
|
-
|
|
35
|
-
|
|
110
|
+
return win[WIN_APV_KEY];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const getActiveTracker = (): PageViewTracker | undefined => {
|
|
114
|
+
const state = readState();
|
|
115
|
+
return state ? state.tracker : undefined;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const hasInitialPageViewFired = (): boolean => {
|
|
119
|
+
const state = readState();
|
|
120
|
+
return !!(state && state.initialPageViewFired);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const markInitialPageViewFired = (): void => {
|
|
124
|
+
const state = writeState();
|
|
125
|
+
if (state) {
|
|
126
|
+
state.initialPageViewFired = true;
|
|
36
127
|
}
|
|
128
|
+
};
|
|
37
129
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
if (typeof window !== 'undefined') {
|
|
47
|
-
delete (window as WindowWithApvFlags)[WIN_INIT_PV_KEY];
|
|
48
|
-
}
|
|
49
|
-
setWindowTracker(undefined);
|
|
130
|
+
// Returns the page to its pre-APV state: stops the active tracker (restoring the
|
|
131
|
+
// history methods it patched), then swaps in fresh state. The state object is the
|
|
132
|
+
// authoritative handle, so this reaches trackers whose owning module is gone.
|
|
133
|
+
export const resetPageViewTracking = (): void => {
|
|
134
|
+
const win = apvWindow();
|
|
135
|
+
if (!win) {
|
|
136
|
+
return;
|
|
50
137
|
}
|
|
51
138
|
|
|
52
|
-
|
|
139
|
+
const active = getActiveTracker();
|
|
140
|
+
if (active) {
|
|
141
|
+
active.teardown();
|
|
142
|
+
}
|
|
53
143
|
|
|
54
|
-
|
|
55
|
-
|
|
144
|
+
// One reassignment replaces every flag at once, so nothing can be left behind
|
|
145
|
+
// whatever shape the previous state was in.
|
|
146
|
+
win[WIN_APV_KEY] = freshState();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const setActiveTracker = (tracker: PageViewTracker): void => {
|
|
150
|
+
const state = writeState();
|
|
151
|
+
if (state) {
|
|
152
|
+
state.tracker = tracker;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const clearActiveTracker = (tracker: PageViewTracker): void => {
|
|
157
|
+
const state = readState();
|
|
158
|
+
if (state && state.tracker === tracker) {
|
|
159
|
+
state.tracker = undefined;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
56
162
|
|
|
57
|
-
|
|
58
|
-
|
|
163
|
+
const currentPathname = (): string => window.location.pathname;
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// History patching
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
// Wraps pushState/replaceState so `onNavigate` runs after the real method, and
|
|
170
|
+
// returns the single function that undoes it — or null when nothing was
|
|
171
|
+
// installed (history is already wrapped, or a frozen/sealed History rejected the
|
|
172
|
+
// assignment). Handing back one undo closure keeps the wrapper and original
|
|
173
|
+
// references out of the tracker entirely: there is no half-patched state for a
|
|
174
|
+
// caller to inspect, and no way to restore the wrong pair.
|
|
175
|
+
export const patchHistory = (
|
|
176
|
+
onNavigate: (source: NavigationSource) => void,
|
|
177
|
+
log: (message: string) => void
|
|
178
|
+
): (() => void) | null => {
|
|
179
|
+
if ((window.history.pushState as MarkedHistoryMethod)[WRAPPED_MARKER]) {
|
|
180
|
+
log(
|
|
181
|
+
'[patch] history already wrapped, skipping to avoid double-wrap — STACKED WRAPPER DETECTED'
|
|
182
|
+
);
|
|
183
|
+
return null;
|
|
59
184
|
}
|
|
60
185
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
timeoutId: ReturnType<typeof setTimeout>;
|
|
64
|
-
}> = [];
|
|
186
|
+
const originals = {} as Record<HistoryMethodName, HistoryStateMethod>;
|
|
187
|
+
const wrappers = {} as Record<HistoryMethodName, HistoryStateMethod>;
|
|
65
188
|
|
|
66
|
-
|
|
67
|
-
|
|
189
|
+
HISTORY_METHODS.forEach(name => {
|
|
190
|
+
const original = window.history[name];
|
|
191
|
+
originals[name] = original;
|
|
68
192
|
|
|
69
|
-
|
|
70
|
-
|
|
193
|
+
const wrapper = function(
|
|
194
|
+
this: History,
|
|
195
|
+
...args: Parameters<HistoryStateMethod>
|
|
196
|
+
): void {
|
|
197
|
+
const result = original.apply(this, args);
|
|
198
|
+
onNavigate(name);
|
|
199
|
+
return result;
|
|
200
|
+
};
|
|
71
201
|
|
|
202
|
+
Object.defineProperty(wrapper, WRAPPED_MARKER, {
|
|
203
|
+
value: true,
|
|
204
|
+
enumerable: false,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
wrappers[name] = wrapper;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Restore per method: a third party may have patched one of the two on top of
|
|
211
|
+
// ours after we installed. Clobbering theirs would break their tracking, so
|
|
212
|
+
// leave anything that is no longer ours in place.
|
|
213
|
+
const restore = (): void =>
|
|
214
|
+
HISTORY_METHODS.forEach(name => {
|
|
215
|
+
if (window.history[name] === wrappers[name]) {
|
|
216
|
+
window.history[name] = originals[name];
|
|
217
|
+
log(`[teardown] restored original ${name}`);
|
|
218
|
+
} else {
|
|
219
|
+
log(
|
|
220
|
+
`[teardown] ${name} no longer ours; leaving in place, gating callback to no-op`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
HISTORY_METHODS.forEach(name => {
|
|
227
|
+
window.history[name] = wrappers[name];
|
|
228
|
+
});
|
|
229
|
+
} catch (e) {
|
|
230
|
+
log(
|
|
231
|
+
`[error] failed to patch history methods (frozen/sealed), rolling back: ${e}`
|
|
232
|
+
);
|
|
233
|
+
try {
|
|
234
|
+
restore();
|
|
235
|
+
} catch (restoreError) {
|
|
236
|
+
log(
|
|
237
|
+
`[error] failed to restore history methods after patch failure: ${restoreError}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return restore;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Tracker
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
export class PageViewTracker {
|
|
251
|
+
private readonly mpInstance: IMParticleWebSDKInstance;
|
|
252
|
+
|
|
253
|
+
private lastPath: string | null = null;
|
|
254
|
+
private active = false;
|
|
255
|
+
private pendingNavigations: IPendingNavigation[] = [];
|
|
256
|
+
|
|
257
|
+
private undoHistoryPatch: (() => void) | null = null;
|
|
72
258
|
private popStateListener: (() => void) | null = null;
|
|
73
259
|
|
|
74
260
|
constructor(mpInstance: IMParticleWebSDKInstance) {
|
|
75
261
|
this.mpInstance = mpInstance;
|
|
76
262
|
}
|
|
77
263
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
);
|
|
264
|
+
// True while this tracker owns the history patch and is listening for
|
|
265
|
+
// navigations. Flips to false on teardown, including the teardown a
|
|
266
|
+
// successor tracker performs during a handoff. Read-only, and the one thing
|
|
267
|
+
// worth checking on `window.__mpApv__.tracker` from a console.
|
|
268
|
+
public get isActive(): boolean {
|
|
269
|
+
return this.active;
|
|
85
270
|
}
|
|
86
271
|
|
|
87
272
|
public init(): void {
|
|
88
|
-
this.
|
|
89
|
-
if (!this.isSupportedEnvironment()) {
|
|
90
|
-
this.mpInstance.Logger.verbose(
|
|
91
|
-
'mParticle APV: [init] unsupported environment (no History API), not starting'
|
|
92
|
-
);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
273
|
+
this.log('[init] PageViewTracker Init');
|
|
95
274
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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'
|
|
275
|
+
if (!supportsHistoryTracking(apvWindow())) {
|
|
276
|
+
this.log(
|
|
277
|
+
'[init] unsupported environment (no History API), not starting'
|
|
106
278
|
);
|
|
107
|
-
|
|
108
|
-
prev.teardown();
|
|
279
|
+
return;
|
|
109
280
|
}
|
|
110
281
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
282
|
+
// Take over from whichever tracker is currently active: one left behind by
|
|
283
|
+
// a previous module load, or this same tracker on a repeat
|
|
284
|
+
// mParticle.init(). Their pending navigations are transferred rather than
|
|
285
|
+
// dropped — a route change may have queued a deferred fire that has not
|
|
286
|
+
// flushed yet, and dropping it loses a page view entirely.
|
|
287
|
+
const active = getActiveTracker();
|
|
288
|
+
let inheritedPaths = this.retire(active);
|
|
289
|
+
if (this.active && active !== this) {
|
|
290
|
+
inheritedPaths = inheritedPaths.concat(this.retire(this));
|
|
119
291
|
}
|
|
120
292
|
|
|
121
|
-
this.
|
|
293
|
+
this.active = true;
|
|
294
|
+
this.lastPath = currentPathname();
|
|
295
|
+
this.log(`[init] seeded lastPath: ${this.lastPath}`);
|
|
122
296
|
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
`mParticle APV: [init] seeded lastPath: ${this.lastPath}`
|
|
297
|
+
this.undoHistoryPatch = patchHistory(
|
|
298
|
+
source => this.safeHandleNavigation(source),
|
|
299
|
+
message => this.log(message)
|
|
127
300
|
);
|
|
301
|
+
this.popStateListener = () => this.safeHandleNavigation('popstate');
|
|
302
|
+
window.addEventListener('popstate', this.popStateListener);
|
|
128
303
|
|
|
129
|
-
this
|
|
130
|
-
this.addNavigationListeners();
|
|
131
|
-
|
|
132
|
-
setWindowTracker(this);
|
|
304
|
+
setActiveTracker(this);
|
|
133
305
|
|
|
134
|
-
this.
|
|
135
|
-
'
|
|
306
|
+
this.log(
|
|
307
|
+
'[init] patched pushState/replaceState + listening for popstate'
|
|
136
308
|
);
|
|
137
309
|
|
|
138
|
-
|
|
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
|
-
});
|
|
310
|
+
inheritedPaths.forEach(path => this.scheduleFire(path));
|
|
152
311
|
}
|
|
153
312
|
|
|
154
|
-
|
|
155
|
-
|
|
313
|
+
public teardown(): void {
|
|
314
|
+
// Discard pending deferred fires (e.g. AutoLogPageView switched off on
|
|
315
|
+
// re-init). A handoff calls takePendingNavigations() first, so by this
|
|
316
|
+
// point those paths are already transferred rather than dropped.
|
|
317
|
+
this.takePendingNavigations();
|
|
156
318
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this.
|
|
160
|
-
'mParticle APV: [patch] history already wrapped, skipping to avoid double-wrap — STACKED WRAPPER DETECTED'
|
|
161
|
-
);
|
|
162
|
-
return;
|
|
319
|
+
if (this.popStateListener) {
|
|
320
|
+
window.removeEventListener('popstate', this.popStateListener);
|
|
321
|
+
this.popStateListener = null;
|
|
163
322
|
}
|
|
164
323
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const pushStateWrapper = function(
|
|
171
|
-
this: History,
|
|
172
|
-
...args: Parameters<HistoryStateMethod>
|
|
173
|
-
): void {
|
|
174
|
-
const result = originalPushState.apply(this, args);
|
|
175
|
-
if (self._isActive) {
|
|
176
|
-
self.safeHandleNavigation('pushState');
|
|
177
|
-
}
|
|
178
|
-
return result;
|
|
179
|
-
};
|
|
324
|
+
if (this.undoHistoryPatch) {
|
|
325
|
+
this.undoHistoryPatch();
|
|
326
|
+
this.undoHistoryPatch = null;
|
|
327
|
+
}
|
|
180
328
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
): void {
|
|
185
|
-
const result = originalReplaceState.apply(this, args);
|
|
186
|
-
if (self._isActive) {
|
|
187
|
-
self.safeHandleNavigation('replaceState');
|
|
188
|
-
}
|
|
189
|
-
return result;
|
|
190
|
-
};
|
|
329
|
+
this.active = false;
|
|
330
|
+
clearActiveTracker(this);
|
|
331
|
+
}
|
|
191
332
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
});
|
|
333
|
+
// Stops the outgoing tracker and returns the paths it had queued so this
|
|
334
|
+
// tracker can re-schedule them once it is active. Private, but reachable
|
|
335
|
+
// across instances: the handoff protocol stays internal to the class.
|
|
336
|
+
private retire(outgoing: PageViewTracker | undefined): string[] {
|
|
337
|
+
if (!outgoing) {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
200
340
|
|
|
201
|
-
this.
|
|
202
|
-
|
|
341
|
+
this.log(
|
|
342
|
+
outgoing === this
|
|
343
|
+
? '[init] retiring this tracker for a repeat mParticle.init() — transferring pending navigations and tearing down'
|
|
344
|
+
: '[init] retiring a tracker from a previous module load — transferring pending navigations and tearing down'
|
|
345
|
+
);
|
|
203
346
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
} catch (e) {
|
|
208
|
-
this.mpInstance.Logger.verbose(
|
|
209
|
-
`mParticle APV: [error] failed to patch history methods (frozen/sealed), rolling back: ${e}`
|
|
210
|
-
);
|
|
211
|
-
try {
|
|
212
|
-
if (window.history.pushState === pushStateWrapper) {
|
|
213
|
-
window.history.pushState = originalPushState;
|
|
214
|
-
}
|
|
215
|
-
if (window.history.replaceState === replaceStateWrapper) {
|
|
216
|
-
window.history.replaceState = originalReplaceState;
|
|
217
|
-
}
|
|
218
|
-
} catch (restoreError) {
|
|
219
|
-
this.mpInstance.Logger.verbose(
|
|
220
|
-
`mParticle APV: [error] failed to restore history methods after patch failure: ${restoreError}`
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
this.pushStateWrapper = null;
|
|
224
|
-
this.replaceStateWrapper = null;
|
|
225
|
-
this.originalPushState = null;
|
|
226
|
-
this.originalReplaceState = null;
|
|
227
|
-
}
|
|
347
|
+
const paths = outgoing.takePendingNavigations();
|
|
348
|
+
outgoing.teardown();
|
|
349
|
+
return paths;
|
|
228
350
|
}
|
|
229
351
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
352
|
+
// Cancels this tracker's deferred fires and hands back their captured paths.
|
|
353
|
+
private takePendingNavigations(): string[] {
|
|
354
|
+
const pending = this.pendingNavigations;
|
|
355
|
+
this.pendingNavigations = [];
|
|
356
|
+
pending.forEach(p => clearTimeout(p.timeoutId));
|
|
357
|
+
return pending.map(p => p.path);
|
|
233
358
|
}
|
|
234
359
|
|
|
235
360
|
private safeHandleNavigation(source: NavigationSource): void {
|
|
361
|
+
// An orphaned wrapper still calls in: teardown can only restore a history
|
|
362
|
+
// method that is still ours, so one a third party patched over stays
|
|
363
|
+
// installed and keeps firing at a dead tracker. Bail before touching
|
|
364
|
+
// lastPath or queueing a fire the flush would just abort. Guarding here
|
|
365
|
+
// rather than at each wrapper also covers the popstate path.
|
|
366
|
+
if (!this.active) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
236
370
|
try {
|
|
237
371
|
this.handleNavigation(source);
|
|
238
372
|
} catch (e) {
|
|
239
|
-
this.
|
|
240
|
-
`
|
|
373
|
+
this.log(
|
|
374
|
+
`[error] navigation handler threw (${source}), page view skipped: ${e}`
|
|
241
375
|
);
|
|
242
376
|
}
|
|
243
377
|
}
|
|
244
378
|
|
|
245
|
-
private getCurrentKey(): string {
|
|
246
|
-
return window.location.pathname;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
379
|
private handleNavigation(source: NavigationSource): void {
|
|
250
|
-
const candidatePath =
|
|
380
|
+
const candidatePath = currentPathname();
|
|
251
381
|
|
|
252
|
-
this.
|
|
253
|
-
`
|
|
382
|
+
this.log(
|
|
383
|
+
`[detect] navigation signal (source: ${source}, candidatePath: ${candidatePath}, lastPath: ${this.lastPath})`
|
|
254
384
|
);
|
|
255
385
|
|
|
256
|
-
if (
|
|
257
|
-
this.
|
|
258
|
-
`
|
|
386
|
+
if (!isNewPage(this.lastPath, candidatePath)) {
|
|
387
|
+
this.log(
|
|
388
|
+
`[dedupe] pathname unchanged, skipping (source: ${source}, path: ${candidatePath})`
|
|
259
389
|
);
|
|
260
390
|
return;
|
|
261
391
|
}
|
|
262
392
|
|
|
263
|
-
this.
|
|
264
|
-
`
|
|
393
|
+
this.log(
|
|
394
|
+
`[accept] pathname changed, scheduling fire (source: ${source}, from: ${this.lastPath}, to: ${candidatePath})`
|
|
265
395
|
);
|
|
266
396
|
this.lastPath = candidatePath;
|
|
397
|
+
this.scheduleFire(candidatePath);
|
|
398
|
+
}
|
|
267
399
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const capturedPath = candidatePath;
|
|
274
|
-
|
|
400
|
+
// Deferred by a macrotask so the router's post-navigation render commit has a
|
|
401
|
+
// chance to update document.title before the event is built. The path is
|
|
402
|
+
// snapshotted now because it is already settled, whereas a same-tick
|
|
403
|
+
// navigation would overwrite window.location before the flush reads it.
|
|
404
|
+
private scheduleFire(path: string): void {
|
|
275
405
|
const timeoutId = setTimeout(() => {
|
|
276
406
|
this.pendingNavigations = this.pendingNavigations.filter(
|
|
277
407
|
p => p.timeoutId !== timeoutId
|
|
278
408
|
);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
409
|
+
|
|
410
|
+
if (!this.active) {
|
|
411
|
+
this.log(
|
|
412
|
+
'[defer] fire aborted, tracker inactive (torn down before flush)'
|
|
282
413
|
);
|
|
283
414
|
return;
|
|
284
415
|
}
|
|
285
416
|
|
|
286
417
|
this.mpInstance._SessionManager.resetSessionTimer();
|
|
287
|
-
this.firePageView(
|
|
418
|
+
this.firePageView(path);
|
|
288
419
|
}, 0);
|
|
289
|
-
|
|
420
|
+
|
|
421
|
+
this.pendingNavigations.push({ path, timeoutId });
|
|
290
422
|
}
|
|
291
423
|
|
|
292
|
-
// Mirrors the event shape of the public mParticle.logPageView(), but carries
|
|
293
|
-
// the captured SPA path rather than reading the live location.
|
|
294
424
|
private firePageView(path: string): void {
|
|
295
425
|
const title = window.document.title;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
this.mpInstance._Events.logEvent({
|
|
302
|
-
messageType: MessageType.PageView,
|
|
303
|
-
name: 'PageView',
|
|
304
|
-
data: {
|
|
305
|
-
hostname: window.location.hostname,
|
|
306
|
-
title,
|
|
307
|
-
path,
|
|
308
|
-
},
|
|
309
|
-
eventType: EventType.Unknown,
|
|
426
|
+
const event = buildPageViewEvent({
|
|
427
|
+
hostname: window.location.hostname,
|
|
428
|
+
title,
|
|
429
|
+
path,
|
|
310
430
|
});
|
|
311
|
-
}
|
|
312
431
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
if (this.popStateListener) {
|
|
350
|
-
window.removeEventListener('popstate', this.popStateListener);
|
|
351
|
-
this.popStateListener = null;
|
|
352
|
-
}
|
|
353
|
-
|
|
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;
|
|
432
|
+
this.log(
|
|
433
|
+
`[fire] deferred flush -> _Events.logEvent(PageView) (path: ${path}, title: ${title})`
|
|
434
|
+
);
|
|
361
435
|
|
|
362
|
-
this.
|
|
363
|
-
if (getWindowTracker() === this) {
|
|
364
|
-
setWindowTracker(undefined);
|
|
365
|
-
}
|
|
436
|
+
this.mpInstance._Events.logEvent(event);
|
|
366
437
|
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function getWindowTracker(): PageViewTracker | undefined {
|
|
370
|
-
if (typeof window === 'undefined') return undefined;
|
|
371
|
-
return (window as WindowWithApvFlags)[WIN_TRACKER_KEY];
|
|
372
|
-
}
|
|
373
438
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const win = window as WindowWithApvFlags;
|
|
377
|
-
if (tracker) {
|
|
378
|
-
win[WIN_TRACKER_KEY] = tracker;
|
|
379
|
-
} else {
|
|
380
|
-
delete win[WIN_TRACKER_KEY];
|
|
439
|
+
private log(message: string): void {
|
|
440
|
+
this.mpInstance.Logger.verbose(`mParticle APV: ${message}`);
|
|
381
441
|
}
|
|
382
442
|
}
|