@mparticle/web-sdk 2.79.1 → 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 -17
- package/dist/mparticle.esm.js +69 -17
- package/dist/mparticle.js +390 -201
- 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 +28 -11
- 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 +37 -5
- package/src/nativeSdkHelpers.interfaces.ts +21 -1
- package/src/{nativeSdkHelpers.js → nativeSdkHelpers.ts} +59 -34
- package/src/pageViewTracker.ts +356 -181
- 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,259 +14,429 @@ type MarkedHistoryMethod = HistoryStateMethod & {
|
|
|
9
14
|
[WRAPPED_MARKER]?: boolean;
|
|
10
15
|
};
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
}
|
|
14
39
|
|
|
15
|
-
|
|
16
|
-
|
|
40
|
+
type WindowWithApv = Window & {
|
|
41
|
+
[WIN_APV_KEY]?: IApvState;
|
|
42
|
+
};
|
|
17
43
|
|
|
18
|
-
|
|
19
|
-
|
|
44
|
+
interface IPageViewData {
|
|
45
|
+
hostname: string;
|
|
46
|
+
title: string;
|
|
47
|
+
path: string;
|
|
48
|
+
}
|
|
20
49
|
|
|
21
|
-
|
|
22
|
-
|
|
50
|
+
interface IPendingNavigation {
|
|
51
|
+
path: string;
|
|
52
|
+
timeoutId: ReturnType<typeof setTimeout>;
|
|
53
|
+
}
|
|
23
54
|
|
|
24
|
-
|
|
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;
|
|
98
|
+
};
|
|
25
99
|
|
|
26
|
-
|
|
27
|
-
|
|
100
|
+
const writeState = (): IApvState | null => {
|
|
101
|
+
const win = apvWindow();
|
|
102
|
+
if (!win) {
|
|
103
|
+
return null;
|
|
28
104
|
}
|
|
29
105
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
typeof window !== 'undefined' &&
|
|
33
|
-
typeof window.history !== 'undefined' &&
|
|
34
|
-
typeof window.history.pushState === 'function' &&
|
|
35
|
-
typeof window.addEventListener === 'function'
|
|
36
|
-
);
|
|
106
|
+
if (!win[WIN_APV_KEY]) {
|
|
107
|
+
win[WIN_APV_KEY] = freshState();
|
|
37
108
|
}
|
|
38
109
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'mParticle APV: [init] PageViewTracker Init'
|
|
42
|
-
);
|
|
43
|
-
if (!this.isSupportedEnvironment()) {
|
|
44
|
-
this.mpInstance.Logger.verbose(
|
|
45
|
-
'mParticle APV: [init] unsupported environment (no History API), not starting'
|
|
46
|
-
);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
110
|
+
return win[WIN_APV_KEY];
|
|
111
|
+
};
|
|
49
112
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.teardown();
|
|
55
|
-
}
|
|
113
|
+
export const getActiveTracker = (): PageViewTracker | undefined => {
|
|
114
|
+
const state = readState();
|
|
115
|
+
return state ? state.tracker : undefined;
|
|
116
|
+
};
|
|
56
117
|
|
|
57
|
-
|
|
118
|
+
export const hasInitialPageViewFired = (): boolean => {
|
|
119
|
+
const state = readState();
|
|
120
|
+
return !!(state && state.initialPageViewFired);
|
|
121
|
+
};
|
|
58
122
|
|
|
59
|
-
|
|
123
|
+
export const markInitialPageViewFired = (): void => {
|
|
124
|
+
const state = writeState();
|
|
125
|
+
if (state) {
|
|
126
|
+
state.initialPageViewFired = true;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
60
129
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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;
|
|
137
|
+
}
|
|
64
138
|
|
|
65
|
-
|
|
66
|
-
|
|
139
|
+
const active = getActiveTracker();
|
|
140
|
+
if (active) {
|
|
141
|
+
active.teardown();
|
|
142
|
+
}
|
|
67
143
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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;
|
|
71
153
|
}
|
|
154
|
+
};
|
|
72
155
|
|
|
73
|
-
|
|
74
|
-
|
|
156
|
+
const clearActiveTracker = (tracker: PageViewTracker): void => {
|
|
157
|
+
const state = readState();
|
|
158
|
+
if (state && state.tracker === tracker) {
|
|
159
|
+
state.tracker = undefined;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
75
162
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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;
|
|
184
|
+
}
|
|
83
185
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
this.originalPushState = originalPushState;
|
|
87
|
-
this.originalReplaceState = originalReplaceState;
|
|
186
|
+
const originals = {} as Record<HistoryMethodName, HistoryStateMethod>;
|
|
187
|
+
const wrappers = {} as Record<HistoryMethodName, HistoryStateMethod>;
|
|
88
188
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
): void {
|
|
93
|
-
const result = originalPushState.apply(this, args);
|
|
94
|
-
self.safeHandleNavigation('pushState');
|
|
95
|
-
return result;
|
|
96
|
-
};
|
|
189
|
+
HISTORY_METHODS.forEach(name => {
|
|
190
|
+
const original = window.history[name];
|
|
191
|
+
originals[name] = original;
|
|
97
192
|
|
|
98
|
-
const
|
|
193
|
+
const wrapper = function(
|
|
99
194
|
this: History,
|
|
100
195
|
...args: Parameters<HistoryStateMethod>
|
|
101
196
|
): void {
|
|
102
|
-
const result =
|
|
103
|
-
|
|
197
|
+
const result = original.apply(this, args);
|
|
198
|
+
onNavigate(name);
|
|
104
199
|
return result;
|
|
105
200
|
};
|
|
106
201
|
|
|
107
|
-
Object.defineProperty(
|
|
108
|
-
value: true,
|
|
109
|
-
enumerable: false,
|
|
110
|
-
});
|
|
111
|
-
Object.defineProperty(replaceStateWrapper, WRAPPED_MARKER, {
|
|
202
|
+
Object.defineProperty(wrapper, WRAPPED_MARKER, {
|
|
112
203
|
value: true,
|
|
113
204
|
enumerable: false,
|
|
114
205
|
});
|
|
115
206
|
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
});
|
|
118
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
|
+
);
|
|
119
233
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
`mParticle APV: [error] failed to patch history methods (frozen/sealed), rolling back: ${e}`
|
|
234
|
+
restore();
|
|
235
|
+
} catch (restoreError) {
|
|
236
|
+
log(
|
|
237
|
+
`[error] failed to restore history methods after patch failure: ${restoreError}`
|
|
125
238
|
);
|
|
126
|
-
try {
|
|
127
|
-
if (window.history.pushState === pushStateWrapper) {
|
|
128
|
-
window.history.pushState = originalPushState;
|
|
129
|
-
}
|
|
130
|
-
if (window.history.replaceState === replaceStateWrapper) {
|
|
131
|
-
window.history.replaceState = originalReplaceState;
|
|
132
|
-
}
|
|
133
|
-
} catch (restoreError) {
|
|
134
|
-
this.mpInstance.Logger.verbose(
|
|
135
|
-
`mParticle APV: [error] failed to restore history methods after patch failure: ${restoreError}`
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
this.pushStateWrapper = null;
|
|
139
|
-
this.replaceStateWrapper = null;
|
|
140
|
-
this.originalPushState = null;
|
|
141
|
-
this.originalReplaceState = null;
|
|
142
239
|
}
|
|
240
|
+
return null;
|
|
143
241
|
}
|
|
144
242
|
|
|
145
|
-
|
|
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;
|
|
258
|
+
private popStateListener: (() => void) | null = null;
|
|
259
|
+
|
|
260
|
+
constructor(mpInstance: IMParticleWebSDKInstance) {
|
|
261
|
+
this.mpInstance = mpInstance;
|
|
262
|
+
}
|
|
263
|
+
|
|
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;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public init(): void {
|
|
273
|
+
this.log('[init] PageViewTracker Init');
|
|
274
|
+
|
|
275
|
+
if (!supportsHistoryTracking(apvWindow())) {
|
|
276
|
+
this.log(
|
|
277
|
+
'[init] unsupported environment (no History API), not starting'
|
|
278
|
+
);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
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));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.active = true;
|
|
294
|
+
this.lastPath = currentPathname();
|
|
295
|
+
this.log(`[init] seeded lastPath: ${this.lastPath}`);
|
|
296
|
+
|
|
297
|
+
this.undoHistoryPatch = patchHistory(
|
|
298
|
+
source => this.safeHandleNavigation(source),
|
|
299
|
+
message => this.log(message)
|
|
300
|
+
);
|
|
146
301
|
this.popStateListener = () => this.safeHandleNavigation('popstate');
|
|
147
302
|
window.addEventListener('popstate', this.popStateListener);
|
|
303
|
+
|
|
304
|
+
setActiveTracker(this);
|
|
305
|
+
|
|
306
|
+
this.log(
|
|
307
|
+
'[init] patched pushState/replaceState + listening for popstate'
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
inheritedPaths.forEach(path => this.scheduleFire(path));
|
|
311
|
+
}
|
|
312
|
+
|
|
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();
|
|
318
|
+
|
|
319
|
+
if (this.popStateListener) {
|
|
320
|
+
window.removeEventListener('popstate', this.popStateListener);
|
|
321
|
+
this.popStateListener = null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (this.undoHistoryPatch) {
|
|
325
|
+
this.undoHistoryPatch();
|
|
326
|
+
this.undoHistoryPatch = null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this.active = false;
|
|
330
|
+
clearActiveTracker(this);
|
|
148
331
|
}
|
|
149
332
|
|
|
150
|
-
|
|
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
|
+
}
|
|
340
|
+
|
|
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
|
+
);
|
|
346
|
+
|
|
347
|
+
const paths = outgoing.takePendingNavigations();
|
|
348
|
+
outgoing.teardown();
|
|
349
|
+
return paths;
|
|
350
|
+
}
|
|
351
|
+
|
|
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);
|
|
358
|
+
}
|
|
359
|
+
|
|
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
|
+
|
|
151
370
|
try {
|
|
152
371
|
this.handleNavigation(source);
|
|
153
372
|
} catch (e) {
|
|
154
|
-
this.
|
|
155
|
-
`
|
|
373
|
+
this.log(
|
|
374
|
+
`[error] navigation handler threw (${source}), page view skipped: ${e}`
|
|
156
375
|
);
|
|
157
376
|
}
|
|
158
377
|
}
|
|
159
378
|
|
|
160
|
-
private
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private handleNavigation(source: string): void {
|
|
165
|
-
const candidatePath = this.getCurrentKey();
|
|
379
|
+
private handleNavigation(source: NavigationSource): void {
|
|
380
|
+
const candidatePath = currentPathname();
|
|
166
381
|
|
|
167
|
-
this.
|
|
168
|
-
`
|
|
382
|
+
this.log(
|
|
383
|
+
`[detect] navigation signal (source: ${source}, candidatePath: ${candidatePath}, lastPath: ${this.lastPath})`
|
|
169
384
|
);
|
|
170
385
|
|
|
171
|
-
if (
|
|
172
|
-
this.
|
|
173
|
-
`
|
|
386
|
+
if (!isNewPage(this.lastPath, candidatePath)) {
|
|
387
|
+
this.log(
|
|
388
|
+
`[dedupe] pathname unchanged, skipping (source: ${source}, path: ${candidatePath})`
|
|
174
389
|
);
|
|
175
390
|
return;
|
|
176
391
|
}
|
|
177
392
|
|
|
178
|
-
this.
|
|
179
|
-
`
|
|
393
|
+
this.log(
|
|
394
|
+
`[accept] pathname changed, scheduling fire (source: ${source}, from: ${this.lastPath}, to: ${candidatePath})`
|
|
180
395
|
);
|
|
181
396
|
this.lastPath = candidatePath;
|
|
397
|
+
this.scheduleFire(candidatePath);
|
|
398
|
+
}
|
|
399
|
+
|
|
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 {
|
|
405
|
+
const timeoutId = setTimeout(() => {
|
|
406
|
+
this.pendingNavigations = this.pendingNavigations.filter(
|
|
407
|
+
p => p.timeoutId !== timeoutId
|
|
408
|
+
);
|
|
182
409
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// later (in the deferred flush) so the router's post-navigation render
|
|
187
|
-
// commit has a chance to update document.title first.
|
|
188
|
-
const capturedPath = candidatePath;
|
|
189
|
-
|
|
190
|
-
setTimeout(() => {
|
|
191
|
-
if (!this.isActive) {
|
|
192
|
-
this.mpInstance.Logger.verbose(
|
|
193
|
-
'mParticle APV: [defer] fire aborted, tracker inactive (torn down before flush)'
|
|
410
|
+
if (!this.active) {
|
|
411
|
+
this.log(
|
|
412
|
+
'[defer] fire aborted, tracker inactive (torn down before flush)'
|
|
194
413
|
);
|
|
195
414
|
return;
|
|
196
415
|
}
|
|
197
416
|
|
|
198
417
|
this.mpInstance._SessionManager.resetSessionTimer();
|
|
199
|
-
this.firePageView(
|
|
418
|
+
this.firePageView(path);
|
|
200
419
|
}, 0);
|
|
420
|
+
|
|
421
|
+
this.pendingNavigations.push({ path, timeoutId });
|
|
201
422
|
}
|
|
202
423
|
|
|
203
|
-
// Mirrors the event shape of the public mParticle.logPageView(), but carries
|
|
204
|
-
// the captured SPA path rather than reading the live location.
|
|
205
424
|
private firePageView(path: string): void {
|
|
206
425
|
const title = window.document.title;
|
|
426
|
+
const event = buildPageViewEvent({
|
|
427
|
+
hostname: window.location.hostname,
|
|
428
|
+
title,
|
|
429
|
+
path,
|
|
430
|
+
});
|
|
207
431
|
|
|
208
|
-
this.
|
|
209
|
-
`
|
|
432
|
+
this.log(
|
|
433
|
+
`[fire] deferred flush -> _Events.logEvent(PageView) (path: ${path}, title: ${title})`
|
|
210
434
|
);
|
|
211
435
|
|
|
212
|
-
this.mpInstance._Events.logEvent(
|
|
213
|
-
messageType: MessageType.PageView,
|
|
214
|
-
name: 'PageView',
|
|
215
|
-
data: {
|
|
216
|
-
hostname: window.location.hostname,
|
|
217
|
-
title,
|
|
218
|
-
path,
|
|
219
|
-
},
|
|
220
|
-
eventType: EventType.Unknown,
|
|
221
|
-
});
|
|
436
|
+
this.mpInstance._Events.logEvent(event);
|
|
222
437
|
}
|
|
223
438
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
window.removeEventListener('popstate', this.popStateListener);
|
|
227
|
-
this.popStateListener = null;
|
|
228
|
-
}
|
|
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
|
-
|
|
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;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
this.isActive = false;
|
|
439
|
+
private log(message: string): void {
|
|
440
|
+
this.mpInstance.Logger.verbose(`mParticle APV: ${message}`);
|
|
266
441
|
}
|
|
267
442
|
}
|
package/src/sdkRuntimeModels.ts
CHANGED
|
@@ -297,6 +297,7 @@ export interface IMParticleInstanceManager extends MParticleWebSDK {
|
|
|
297
297
|
|
|
298
298
|
// Public Methods
|
|
299
299
|
getInstance(instanceName?: string): IMParticleWebSDKInstance;
|
|
300
|
+
uiwebviewBridgeName?: string;
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
// Used in cases where server requires booleans as strings
|
|
@@ -373,7 +374,7 @@ export interface SDKHelpersApi {
|
|
|
373
374
|
createXHR?(cb: () => void): XMLHttpRequest;
|
|
374
375
|
extend?(...args: any[]);
|
|
375
376
|
findKeyInObject?(obj: any, key: string): string;
|
|
376
|
-
parseNumber
|
|
377
|
+
parseNumber(value: string | number): number;
|
|
377
378
|
generateUniqueId();
|
|
378
379
|
generateHash?(value: string): number;
|
|
379
380
|
// https://go.mparticle.com/work/SQDSDKS-6317
|
|
@@ -382,11 +383,11 @@ export interface SDKHelpersApi {
|
|
|
382
383
|
parseStringOrNumber?(value: string | number): string | number | null;
|
|
383
384
|
inArray?(items: any[], value: any): boolean;
|
|
384
385
|
converted?(s: string): string;
|
|
385
|
-
filterUserIdentitiesForForwarders
|
|
386
|
+
filterUserIdentitiesForForwarders(
|
|
386
387
|
userIdentities: Dictionary,
|
|
387
388
|
filterList: number[]
|
|
388
389
|
): Dictionary;
|
|
389
|
-
filterUserAttributes
|
|
390
|
+
filterUserAttributes(
|
|
390
391
|
userAttributes: Dictionary,
|
|
391
392
|
filterList: number[]
|
|
392
393
|
): Dictionary;
|
package/src/store.ts
CHANGED
|
@@ -207,6 +207,8 @@ export interface IStore {
|
|
|
207
207
|
pixelConfigurations: IPixelConfiguration[];
|
|
208
208
|
integrationDelayTimeoutStart: number; // UNIX Timestamp
|
|
209
209
|
webviewBridgeEnabled?: boolean;
|
|
210
|
+
bridgeV1Available?: boolean;
|
|
211
|
+
bridgeV2Available?: boolean;
|
|
210
212
|
wrapperSDKInfo: WrapperSDKInfo;
|
|
211
213
|
roktAccountId: string | null;
|
|
212
214
|
integrationName: string | null;
|