@mparticle/web-sdk 2.75.1 → 2.77.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.
@@ -66,6 +66,7 @@ export default class KitBlocker {
66
66
  LaunchReferral?: string;
67
67
  ExpandedEventCount: number;
68
68
  ActiveTimeOnSite: number;
69
+ TotalTimeOnSite: number;
69
70
  PageUrl?: string;
70
71
  IsBackgroundAST?: boolean;
71
72
  AlreadySentToForwarders?: boolean;
@@ -111,6 +112,7 @@ export default class KitBlocker {
111
112
  LaunchReferral?: string;
112
113
  ExpandedEventCount: number;
113
114
  ActiveTimeOnSite: number;
115
+ TotalTimeOnSite: number;
114
116
  PageUrl?: string;
115
117
  IsBackgroundAST?: boolean;
116
118
  AlreadySentToForwarders?: boolean;
@@ -20,6 +20,7 @@ import RoktManager from './roktManager';
20
20
  import { ICookieConsentManager } from './cookieConsentManager';
21
21
  import { ErrorReportingDispatcher } from './reporting/errorReportingDispatcher';
22
22
  import { LoggingDispatcher } from './reporting/loggingDispatcher';
23
+ import { PageViewTracker } from './pageViewTracker';
23
24
  export interface IErrorLogMessage {
24
25
  message?: string;
25
26
  name?: string;
@@ -44,6 +45,7 @@ export interface IMParticleWebSDKInstance extends MParticleWebSDK {
44
45
  _IdentityAPIClient: typeof IdentityAPIClient;
45
46
  _IntegrationCapture: IntegrationCapture;
46
47
  _NativeSdkHelpers: INativeSdkHelpers;
48
+ _PageViewTracker?: PageViewTracker;
47
49
  _Persistence: IPersistence;
48
50
  _CookieConsentManager: ICookieConsentManager;
49
51
  _ErrorReportingDispatcher: ErrorReportingDispatcher;
@@ -0,0 +1,21 @@
1
+ import { IMParticleWebSDKInstance } from './mp-instance';
2
+ export declare class PageViewTracker {
3
+ mpInstance: IMParticleWebSDKInstance;
4
+ private lastPath;
5
+ private isActive;
6
+ private originalPushState;
7
+ private originalReplaceState;
8
+ private pushStateWrapper;
9
+ private replaceStateWrapper;
10
+ private popStateListener;
11
+ constructor(mpInstance: IMParticleWebSDKInstance);
12
+ private isSupportedEnvironment;
13
+ init(): void;
14
+ private patchHistoryMethods;
15
+ private addNavigationListeners;
16
+ private safeHandleNavigation;
17
+ private getCurrentKey;
18
+ private handleNavigation;
19
+ private firePageView;
20
+ teardown(): void;
21
+ }
@@ -60,6 +60,7 @@ export interface SDKEvent {
60
60
  LaunchReferral?: string;
61
61
  ExpandedEventCount: number;
62
62
  ActiveTimeOnSite: number;
63
+ TotalTimeOnSite: number;
63
64
  PageUrl?: string;
64
65
  IsBackgroundAST?: boolean;
65
66
  AlreadySentToForwarders?: boolean;
@@ -146,6 +146,7 @@ export interface IStore {
146
146
  getRoktAccountId?(): string;
147
147
  setRoktAccountId?(accountId: string): void;
148
148
  getTimeOnSite?(): number | undefined;
149
+ getTotalTimeOnSite?(): number | undefined;
149
150
  getIntegrationName?(): string;
150
151
  setIntegrationName?(integrationName: string): void;
151
152
  addMpidToSessionHistory?(mpid: MPID, previousMpid?: MPID): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mparticle/web-sdk",
3
- "version": "2.75.1",
3
+ "version": "2.77.0",
4
4
  "description": "mParticle core SDK for web applications",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -103,7 +103,7 @@
103
103
  "@babel/preset-env": "^7.6.0",
104
104
  "@babel/preset-typescript": "^7.6.0",
105
105
  "@mparticle/data-planning-models": "^0.1.0",
106
- "@mparticle/event-models": "^1.3.0",
106
+ "@mparticle/event-models": "^1.3.1",
107
107
  "@rollup/plugin-babel": "6.0.3",
108
108
  "@rollup/plugin-commonjs": "25.0.4",
109
109
  "@rollup/plugin-json": "^5.0.2",
@@ -159,7 +159,7 @@
159
159
  "webpack-cli": "^5.0.2"
160
160
  },
161
161
  "peerDependencies": {
162
- "@mparticle/event-models": "^1.3.0"
162
+ "@mparticle/event-models": "^1.3.1"
163
163
  },
164
164
  "dependencies": {
165
165
  "@babel/runtime": "^7.23.2"
@@ -197,6 +197,7 @@ export class BatchUploader {
197
197
  SessionStartDate: sessionStartDate?.getTime() || now,
198
198
  Debug: SDKConfig.isDevelopmentMode,
199
199
  ActiveTimeOnSite: _timeOnSiteTimer?.getTimeInForeground() || 0,
200
+ TotalTimeOnSite: _Store.getTotalTimeOnSite?.() || 0,
200
201
  PageUrl: getHref() || null,
201
202
  IsBackgroundAST: true
202
203
  } as SDKEvent;
@@ -91,6 +91,7 @@ export default class _BatchValidator {
91
91
  isDevelopmentMode: false,
92
92
  onCreateBatch: mockFunction,
93
93
  },
94
+ getTotalTimeOnSite: mockFunction,
94
95
  },
95
96
  config: null,
96
97
  eCommerce: null,
@@ -57,6 +57,7 @@ import { LoggingDispatcher } from './reporting/loggingDispatcher';
57
57
  import { IErrorReportingService, ILoggingService } from './reporting/types';
58
58
  import { logDeprecatedMethodUsage } from './reporting/deprecatedMethodLogger';
59
59
  import { normalizeRoktLauncherOptions } from './roktLauncherOptions';
60
+ import { PageViewTracker } from './pageViewTracker';
60
61
 
61
62
  export interface IErrorLogMessage {
62
63
  message?: string;
@@ -87,6 +88,7 @@ export interface IMParticleWebSDKInstance extends MParticleWebSDK {
87
88
  _IdentityAPIClient: typeof IdentityAPIClient;
88
89
  _IntegrationCapture: IntegrationCapture;
89
90
  _NativeSdkHelpers: INativeSdkHelpers;
91
+ _PageViewTracker?: PageViewTracker;
90
92
  _Persistence: IPersistence;
91
93
  _CookieConsentManager: ICookieConsentManager;
92
94
  _ErrorReportingDispatcher: ErrorReportingDispatcher;
@@ -1585,6 +1587,13 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1585
1587
 
1586
1588
  if (getFeatureFlag(AutoLogPageView)) {
1587
1589
  mpInstance._Events.logPageView();
1590
+
1591
+ if (!mpInstance._PageViewTracker) {
1592
+ mpInstance._PageViewTracker = new PageViewTracker(mpInstance);
1593
+ }
1594
+ mpInstance._PageViewTracker.init();
1595
+ } else if (mpInstance._PageViewTracker) {
1596
+ mpInstance._PageViewTracker.teardown();
1588
1597
  }
1589
1598
 
1590
1599
  processIdentityCallback(
@@ -0,0 +1,267 @@
1
+ import { IMParticleWebSDKInstance } from './mp-instance';
2
+ import { EventType, MessageType } from './types';
3
+
4
+ type HistoryStateMethod = History['pushState'];
5
+
6
+ const WRAPPED_MARKER = '__mpApvWrapped__';
7
+
8
+ type MarkedHistoryMethod = HistoryStateMethod & {
9
+ [WRAPPED_MARKER]?: boolean;
10
+ };
11
+
12
+ export class PageViewTracker {
13
+ mpInstance: IMParticleWebSDKInstance;
14
+
15
+ private lastPath: string | null = null;
16
+ private isActive = false;
17
+
18
+ private originalPushState: HistoryStateMethod | null = null;
19
+ private originalReplaceState: HistoryStateMethod | null = null;
20
+
21
+ private pushStateWrapper: HistoryStateMethod | null = null;
22
+ private replaceStateWrapper: HistoryStateMethod | null = null;
23
+
24
+ private popStateListener: (() => void) | null = null;
25
+
26
+ constructor(mpInstance: IMParticleWebSDKInstance) {
27
+ this.mpInstance = mpInstance;
28
+ }
29
+
30
+ private isSupportedEnvironment(): boolean {
31
+ return (
32
+ typeof window !== 'undefined' &&
33
+ typeof window.history !== 'undefined' &&
34
+ typeof window.history.pushState === 'function' &&
35
+ typeof window.addEventListener === 'function'
36
+ );
37
+ }
38
+
39
+ public init(): void {
40
+ this.mpInstance.Logger.verbose(
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
+ }
49
+
50
+ if (this.isActive) {
51
+ this.mpInstance.Logger.verbose(
52
+ 'mParticle APV: [init] starting (teardown-first for idempotency)'
53
+ );
54
+ this.teardown();
55
+ }
56
+
57
+ this.isActive = true;
58
+
59
+ this.lastPath = this.getCurrentKey();
60
+
61
+ this.mpInstance.Logger.verbose(
62
+ `mParticle APV: [init] seeded lastPath: ${this.lastPath}`
63
+ );
64
+
65
+ this.patchHistoryMethods();
66
+ this.addNavigationListeners();
67
+
68
+ this.mpInstance.Logger.verbose(
69
+ 'mParticle APV: [init] patched pushState/replaceState + listening for popstate'
70
+ );
71
+ }
72
+
73
+ private patchHistoryMethods(): void {
74
+ const self = this;
75
+
76
+ const installed = window.history.pushState as MarkedHistoryMethod;
77
+ if (installed[WRAPPED_MARKER]) {
78
+ this.mpInstance.Logger.verbose(
79
+ 'mParticle APV: [patch] history already wrapped, skipping to avoid double-wrap'
80
+ );
81
+ return;
82
+ }
83
+
84
+ const originalPushState = window.history.pushState;
85
+ const originalReplaceState = window.history.replaceState;
86
+ this.originalPushState = originalPushState;
87
+ this.originalReplaceState = originalReplaceState;
88
+
89
+ const pushStateWrapper = function(
90
+ this: History,
91
+ ...args: Parameters<HistoryStateMethod>
92
+ ): void {
93
+ const result = originalPushState.apply(this, args);
94
+ self.safeHandleNavigation('pushState');
95
+ return result;
96
+ };
97
+
98
+ const replaceStateWrapper = function(
99
+ this: History,
100
+ ...args: Parameters<HistoryStateMethod>
101
+ ): void {
102
+ const result = originalReplaceState.apply(this, args);
103
+ self.safeHandleNavigation('replaceState');
104
+ return result;
105
+ };
106
+
107
+ Object.defineProperty(pushStateWrapper, WRAPPED_MARKER, {
108
+ value: true,
109
+ enumerable: false,
110
+ });
111
+ Object.defineProperty(replaceStateWrapper, WRAPPED_MARKER, {
112
+ value: true,
113
+ enumerable: false,
114
+ });
115
+
116
+ this.pushStateWrapper = pushStateWrapper;
117
+ this.replaceStateWrapper = replaceStateWrapper;
118
+
119
+ try {
120
+ window.history.pushState = pushStateWrapper;
121
+ window.history.replaceState = replaceStateWrapper;
122
+ } catch (e) {
123
+ this.mpInstance.Logger.verbose(
124
+ `mParticle APV: [error] failed to patch history methods (frozen/sealed), rolling back: ${e}`
125
+ );
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
+ }
143
+ }
144
+
145
+ private addNavigationListeners(): void {
146
+ this.popStateListener = () => this.safeHandleNavigation('popstate');
147
+ window.addEventListener('popstate', this.popStateListener);
148
+ }
149
+
150
+ private safeHandleNavigation(source: string): void {
151
+ try {
152
+ this.handleNavigation(source);
153
+ } catch (e) {
154
+ this.mpInstance.Logger.verbose(
155
+ `mParticle APV: [error] navigation handler threw (${source}), page view skipped: ${e}`
156
+ );
157
+ }
158
+ }
159
+
160
+ private getCurrentKey(): string {
161
+ return window.location.pathname;
162
+ }
163
+
164
+ private handleNavigation(source: string): void {
165
+ const candidatePath = this.getCurrentKey();
166
+
167
+ this.mpInstance.Logger.verbose(
168
+ `mParticle APV: [detect] navigation signal (source: ${source}, candidatePath: ${candidatePath}, lastPath: ${this.lastPath})`
169
+ );
170
+
171
+ if (candidatePath === this.lastPath) {
172
+ this.mpInstance.Logger.verbose(
173
+ `mParticle APV: [dedupe] pathname unchanged, skipping (source: ${source}, path: ${candidatePath})`
174
+ );
175
+ return;
176
+ }
177
+
178
+ this.mpInstance.Logger.verbose(
179
+ `mParticle APV: [accept] pathname changed, scheduling fire (source: ${source}, from: ${this.lastPath}, to: ${candidatePath})`
180
+ );
181
+ this.lastPath = candidatePath;
182
+
183
+ // Snapshot the path now: it is already settled at this point, and a
184
+ // same-tick navigation would otherwise overwrite window.location
185
+ // before the deferred flush reads it. The title is intentionally read
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)'
194
+ );
195
+ return;
196
+ }
197
+
198
+ this.mpInstance._SessionManager.resetSessionTimer();
199
+ this.firePageView(capturedPath);
200
+ }, 0);
201
+ }
202
+
203
+ // Mirrors the event shape of the public mParticle.logPageView(), but carries
204
+ // the captured SPA path rather than reading the live location.
205
+ private firePageView(path: string): void {
206
+ const title = window.document.title;
207
+
208
+ this.mpInstance.Logger.verbose(
209
+ `mParticle APV: [fire] deferred flush -> _Events.logEvent(PageView) (path: ${path}, title: ${title})`
210
+ );
211
+
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
+ });
222
+ }
223
+
224
+ public teardown(): void {
225
+ if (this.popStateListener) {
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;
266
+ }
267
+ }
@@ -355,11 +355,13 @@ export default class RoktManager {
355
355
  this.setUserAttributes(mappedAttributes);
356
356
 
357
357
  const timeOnSite = this.store?.getTimeOnSite?.();
358
+ const totalTimeOnSite = this.store?.getTotalTimeOnSite?.();
358
359
 
359
360
  const enrichedAttributes: RoktAttributes = {
360
361
  ...mappedAttributes,
361
362
  ...(sandboxValue !== null ? { sandbox: sandboxValue } : {}),
362
363
  ...(timeOnSite ? { active_time_on_site_ms: timeOnSite } : {}),
364
+ ...(totalTimeOnSite ? { total_time_on_site_ms: totalTimeOnSite } : {}),
363
365
  };
364
366
 
365
367
  // Propagate email from current user identities if not already in attributes
@@ -94,6 +94,7 @@ export interface SDKEvent {
94
94
  LaunchReferral?: string;
95
95
  ExpandedEventCount: number;
96
96
  ActiveTimeOnSite: number;
97
+ TotalTimeOnSite: number;
97
98
  PageUrl?: string;
98
99
  IsBackgroundAST?: boolean;
99
100
  AlreadySentToForwarders?: boolean;
@@ -664,6 +664,7 @@ export function convertBaseEventData(
664
664
  location: convertSDKLocation(sdkEvent.Location),
665
665
  source_message_id: sdkEvent.SourceMessageId,
666
666
  active_time_on_site_ms: sdkEvent.ActiveTimeOnSite,
667
+ total_time_on_site_ms: sdkEvent.TotalTimeOnSite,
667
668
  page_url: sdkEvent.PageUrl,
668
669
  };
669
670
 
@@ -311,6 +311,7 @@ export default function ServerModel(
311
311
  event.name
312
312
  ),
313
313
  ActiveTimeOnSite: mpInstance._timeOnSiteTimer?.getTimeInForeground(),
314
+ TotalTimeOnSite: mpInstance._Store.getTotalTimeOnSite?.(),
314
315
  PageUrl: getHref() || null,
315
316
  SourceMessageId:
316
317
  event.sourceMessageId ||
package/src/store.ts CHANGED
@@ -233,6 +233,7 @@ export interface IStore {
233
233
  getRoktAccountId?(): string;
234
234
  setRoktAccountId?(accountId: string): void;
235
235
  getTimeOnSite?(): number | undefined;
236
+ getTotalTimeOnSite?(): number | undefined;
236
237
  getIntegrationName?(): string;
237
238
  setIntegrationName?(integrationName: string): void;
238
239
 
@@ -687,6 +688,11 @@ export default function Store(
687
688
  this.getTimeOnSite = () =>
688
689
  mpInstance._timeOnSiteTimer?.getTimeInForeground();
689
690
 
691
+ this.getTotalTimeOnSite = () =>
692
+ this.sessionStartDate
693
+ ? Date.now() - this.sessionStartDate.getTime()
694
+ : undefined;
695
+
690
696
  this.addMpidToSessionHistory = (mpid: MPID, previousMPID?: MPID): void => {
691
697
  const indexOfMPID = this.currentSessionMPIDs.indexOf(mpid);
692
698