@mparticle/web-sdk 3.8.1 → 3.10.0
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 +48 -44
- package/dist/mparticle.esm.js +48 -44
- package/dist/mparticle.js +190 -78
- package/dist/types/src/pageViewTracker.d.ts +0 -5
- package/dist/types/src/roktManager.d.ts +7 -0
- package/dist/types/src/routeChangeMonitor.d.ts +9 -0
- package/package.json +1 -1
- package/src/mp-instance.ts +1 -1
- package/src/pageViewTracker.ts +8 -102
- package/src/persistence.ts +13 -6
- package/src/roktManager.ts +56 -0
- package/src/routeChangeMonitor.ts +207 -0
package/src/roktManager.ts
CHANGED
|
@@ -21,6 +21,8 @@ import { ErrorCodes, IErrorReportingService, ILoggingService, WSDKErrorSeverity
|
|
|
21
21
|
import { IRoktLauncherOptions, normalizeRoktLauncherOptions } from "./roktLauncherOptions";
|
|
22
22
|
import { PARTNER_MODULE_IDS } from "./cookieSyncManager";
|
|
23
23
|
import IntegrationCapture from "./integrationCapture";
|
|
24
|
+
import Constants from "./constants";
|
|
25
|
+
import { subscribeToRouteChange } from "./routeChangeMonitor";
|
|
24
26
|
|
|
25
27
|
const PASSBACK_CONVERSION_TRACKING_ID = 'passbackconversiontrackingid';
|
|
26
28
|
|
|
@@ -86,6 +88,9 @@ export interface IRoktKit {
|
|
|
86
88
|
// Optional because the Rokt Kit ships on its own release cadence; a kit
|
|
87
89
|
// published before terminate() existed will not implement it.
|
|
88
90
|
terminate?: () => Promise<void>;
|
|
91
|
+
// Set by the kit only when it wants route changes; nothing subscribes otherwise. The
|
|
92
|
+
// manager also calls it once on attach.
|
|
93
|
+
onRouteChange?: () => void;
|
|
89
94
|
launcherOptions?: Dictionary<any>;
|
|
90
95
|
settings?: IRoktKitSettings;
|
|
91
96
|
integrationName?: string;
|
|
@@ -130,6 +135,11 @@ export default class RoktManager {
|
|
|
130
135
|
private onReadyCallback: (() => void) | null = null;
|
|
131
136
|
private initialized: boolean = false;
|
|
132
137
|
private isShoppableAdsLoaded: boolean = false;
|
|
138
|
+
private stopRouteChangeWatch: (() => void) | null = null;
|
|
139
|
+
|
|
140
|
+
// Keys this manager's route subscription, so a re-executed bundle's manager replaces
|
|
141
|
+
// it while a second named instance keeps its own.
|
|
142
|
+
constructor(private readonly instanceName?: string) {}
|
|
133
143
|
|
|
134
144
|
/**
|
|
135
145
|
* Sets a callback to be invoked when RoktManager becomes ready
|
|
@@ -219,6 +229,8 @@ export default class RoktManager {
|
|
|
219
229
|
public attachKit(kit: IRoktKit): void {
|
|
220
230
|
this.kit = kit;
|
|
221
231
|
|
|
232
|
+
this.watchRouteChanges();
|
|
233
|
+
|
|
222
234
|
if (kit.settings?.accountId) {
|
|
223
235
|
this.store.setRoktAccountId(kit.settings.accountId);
|
|
224
236
|
}
|
|
@@ -236,6 +248,50 @@ export default class RoktManager {
|
|
|
236
248
|
}
|
|
237
249
|
}
|
|
238
250
|
|
|
251
|
+
// Core already emits a page view per navigation when AutoLogPageView is on, so the kit
|
|
252
|
+
// subscribes to route changes only when it is off. The call on attach happens either
|
|
253
|
+
// way: the auto page view fires once per page load, so a re-init emits none.
|
|
254
|
+
private watchRouteChanges(): void {
|
|
255
|
+
if (!isFunction(this.kit?.onRouteChange)) {
|
|
256
|
+
this.stopRouteChangeWatch?.();
|
|
257
|
+
this.stopRouteChangeWatch = null;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (this.isAutoLogPageViewEnabled()) {
|
|
262
|
+
this.stopRouteChangeWatch?.();
|
|
263
|
+
this.stopRouteChangeWatch = null;
|
|
264
|
+
} else if (!this.stopRouteChangeWatch) {
|
|
265
|
+
this.stopRouteChangeWatch = subscribeToRouteChange(
|
|
266
|
+
`rokt:${this.instanceName}`,
|
|
267
|
+
() => this.notifyRouteChange()
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// A full navigation or a re-init lands on the trigger route without a route change
|
|
272
|
+
// of its own.
|
|
273
|
+
this.notifyRouteChange();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Reads `this.kit` at call time, so a kit attached by a later init() takes over.
|
|
277
|
+
private notifyRouteChange(): void {
|
|
278
|
+
try {
|
|
279
|
+
this.kit?.onRouteChange?.();
|
|
280
|
+
} catch (e) {
|
|
281
|
+
this.logger?.error(
|
|
282
|
+
`RoktManager: Error in onRouteChange: ${getErrorMessage(e)}`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private isAutoLogPageViewEnabled(): boolean {
|
|
288
|
+
return (
|
|
289
|
+
this.store?.SDKConfig?.flags?.[
|
|
290
|
+
Constants.FeatureFlags.AutoLogPageView
|
|
291
|
+
] === true
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
239
295
|
/**
|
|
240
296
|
* Renders ads based on the options provided
|
|
241
297
|
*
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Dictionary } from './utils';
|
|
2
|
+
|
|
3
|
+
type HistoryStateMethod = History['pushState'];
|
|
4
|
+
type HistoryMethodName = 'pushState' | 'replaceState';
|
|
5
|
+
export type RouteChangeSource = HistoryMethodName | 'popstate';
|
|
6
|
+
export type RouteChangeListener = (source: RouteChangeSource) => void;
|
|
7
|
+
|
|
8
|
+
const HISTORY_METHODS: HistoryMethodName[] = ['pushState', 'replaceState'];
|
|
9
|
+
|
|
10
|
+
const WRAPPED_MARKER = '__mpApvWrapped__';
|
|
11
|
+
|
|
12
|
+
type MarkedHistoryMethod = HistoryStateMethod & {
|
|
13
|
+
[WRAPPED_MARKER]?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const supportsHistoryTracking = (win: Window | null): boolean =>
|
|
17
|
+
!!win &&
|
|
18
|
+
win.history !== undefined &&
|
|
19
|
+
typeof win.history.pushState === 'function' &&
|
|
20
|
+
typeof win.addEventListener === 'function';
|
|
21
|
+
|
|
22
|
+
// Wraps pushState/replaceState so `onNavigate` runs after the real method, and
|
|
23
|
+
// returns the single function that undoes it — or null when nothing was
|
|
24
|
+
// installed (history is already wrapped, or a frozen/sealed History rejected the
|
|
25
|
+
// assignment). Handing back one undo closure keeps the wrapper and original
|
|
26
|
+
// references out of the tracker entirely: there is no half-patched state for a
|
|
27
|
+
// caller to inspect, and no way to restore the wrong pair.
|
|
28
|
+
export const patchHistory = (
|
|
29
|
+
onNavigate: RouteChangeListener,
|
|
30
|
+
log: (message: string) => void
|
|
31
|
+
): (() => void) | null => {
|
|
32
|
+
if ((window.history.pushState as MarkedHistoryMethod)[WRAPPED_MARKER]) {
|
|
33
|
+
log(
|
|
34
|
+
'[patch] history already wrapped, skipping to avoid double-wrap — STACKED WRAPPER DETECTED'
|
|
35
|
+
);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const originals = {} as Record<HistoryMethodName, HistoryStateMethod>;
|
|
40
|
+
const wrappers = {} as Record<HistoryMethodName, HistoryStateMethod>;
|
|
41
|
+
|
|
42
|
+
HISTORY_METHODS.forEach(name => {
|
|
43
|
+
const original = window.history[name];
|
|
44
|
+
originals[name] = original;
|
|
45
|
+
|
|
46
|
+
const wrapper = function(
|
|
47
|
+
this: History,
|
|
48
|
+
...args: Parameters<HistoryStateMethod>
|
|
49
|
+
): void {
|
|
50
|
+
const result = original.apply(this, args);
|
|
51
|
+
onNavigate(name);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
Object.defineProperty(wrapper, WRAPPED_MARKER, {
|
|
56
|
+
value: true,
|
|
57
|
+
enumerable: false,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
wrappers[name] = wrapper;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Restore per method: a third party may have patched one of the two on top of
|
|
64
|
+
// ours after we installed. Clobbering theirs would break their tracking, so
|
|
65
|
+
// leave anything that is no longer ours in place.
|
|
66
|
+
const restore = (): void =>
|
|
67
|
+
HISTORY_METHODS.forEach(name => {
|
|
68
|
+
if (window.history[name] === wrappers[name]) {
|
|
69
|
+
window.history[name] = originals[name];
|
|
70
|
+
log(`[teardown] restored original ${name}`);
|
|
71
|
+
} else {
|
|
72
|
+
log(
|
|
73
|
+
`[teardown] ${name} no longer ours; leaving in place, gating callback to no-op`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
HISTORY_METHODS.forEach(name => {
|
|
80
|
+
window.history[name] = wrappers[name];
|
|
81
|
+
});
|
|
82
|
+
} catch (e) {
|
|
83
|
+
log(
|
|
84
|
+
`[error] failed to patch history methods (frozen/sealed), rolling back: ${e}`
|
|
85
|
+
);
|
|
86
|
+
try {
|
|
87
|
+
restore();
|
|
88
|
+
} catch (restoreError) {
|
|
89
|
+
log(
|
|
90
|
+
`[error] failed to restore history methods after patch failure: ${restoreError}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return restore;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// One History patch for the whole SDK, so page-view tracking and preselection do not
|
|
100
|
+
// stack two wrappers.
|
|
101
|
+
//
|
|
102
|
+
// State lives on `window`, not module scope, because Next.js re-executes the bundle per
|
|
103
|
+
// SPA navigation while the patch from the previous execution stays installed.
|
|
104
|
+
export const WIN_ROUTE_MONITOR_KEY = '__mpRouteMonitor__';
|
|
105
|
+
|
|
106
|
+
// Keyed by owner, so a later bundle execution's subscriber replaces its predecessor
|
|
107
|
+
// rather than joining it.
|
|
108
|
+
interface IRouteMonitorState {
|
|
109
|
+
listeners: Dictionary<RouteChangeListener>;
|
|
110
|
+
undoHistoryPatch: (() => void) | null;
|
|
111
|
+
popStateListener: (() => void) | null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type WindowWithRouteMonitor = Window & {
|
|
115
|
+
[WIN_ROUTE_MONITOR_KEY]?: IRouteMonitorState;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const monitorState = (): IRouteMonitorState => {
|
|
119
|
+
const win = window as WindowWithRouteMonitor;
|
|
120
|
+
|
|
121
|
+
if (!win[WIN_ROUTE_MONITOR_KEY]) {
|
|
122
|
+
win[WIN_ROUTE_MONITOR_KEY] = {
|
|
123
|
+
listeners: {},
|
|
124
|
+
undoHistoryPatch: null,
|
|
125
|
+
popStateListener: null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return win[WIN_ROUTE_MONITOR_KEY] as IRouteMonitorState;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Reads the shared state at call time, so a wrapper installed by a previous bundle
|
|
133
|
+
// execution still reaches the current listeners.
|
|
134
|
+
const emit = (source: RouteChangeSource): void => {
|
|
135
|
+
Object.values(monitorState().listeners).forEach(listener => {
|
|
136
|
+
try {
|
|
137
|
+
listener(source);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
// One subscriber must not stop the others, and must never break navigation.
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const install = (
|
|
145
|
+
state: IRouteMonitorState,
|
|
146
|
+
log: (message: string) => void
|
|
147
|
+
): void => {
|
|
148
|
+
if (state.undoHistoryPatch || state.popStateListener) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
state.undoHistoryPatch = patchHistory(emit, log);
|
|
153
|
+
state.popStateListener = (): void => emit('popstate');
|
|
154
|
+
window.addEventListener('popstate', state.popStateListener);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const uninstall = (state: IRouteMonitorState): void => {
|
|
158
|
+
if (state.popStateListener) {
|
|
159
|
+
window.removeEventListener('popstate', state.popStateListener);
|
|
160
|
+
state.popStateListener = null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (state.undoHistoryPatch) {
|
|
164
|
+
state.undoHistoryPatch();
|
|
165
|
+
state.undoHistoryPatch = null;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Installs on the first subscriber and tears down after the last leaves, so a workspace
|
|
170
|
+
// that needs neither is never patched. Unsubscribing only removes the listener while it
|
|
171
|
+
// still owns its key, so an outgoing instance cannot remove its replacement.
|
|
172
|
+
export const subscribeToRouteChange = (
|
|
173
|
+
key: string,
|
|
174
|
+
listener: RouteChangeListener,
|
|
175
|
+
log: (message: string) => void = () => undefined
|
|
176
|
+
): (() => void) => {
|
|
177
|
+
if (!supportsHistoryTracking(typeof window === 'undefined' ? null : window)) {
|
|
178
|
+
return () => undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const state = monitorState();
|
|
182
|
+
|
|
183
|
+
state.listeners[key] = listener;
|
|
184
|
+
install(state, log);
|
|
185
|
+
|
|
186
|
+
return (): void => {
|
|
187
|
+
if (state.listeners[key] !== listener) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
delete state.listeners[key];
|
|
192
|
+
if (Object.keys(state.listeners).length === 0) {
|
|
193
|
+
uninstall(state);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const resetRouteChangeMonitor = (): void => {
|
|
199
|
+
if (typeof window === 'undefined') {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const state = monitorState();
|
|
204
|
+
state.listeners = {};
|
|
205
|
+
uninstall(state);
|
|
206
|
+
delete (window as WindowWithRouteMonitor)[WIN_ROUTE_MONITOR_KEY];
|
|
207
|
+
};
|