@mparticle/web-sdk 2.57.0 → 2.59.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mparticle/web-sdk",
3
- "version": "2.57.0",
3
+ "version": "2.59.0",
4
4
  "description": "mParticle core SDK for web applications",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
package/src/apiClient.ts CHANGED
@@ -10,6 +10,7 @@ import { IMParticleUser, ISDKUserAttributes } from './identity-user-interfaces';
10
10
  import { AsyncUploader, FetchUploader, XHRUploader } from './uploaders';
11
11
  import { IMParticleWebSDKInstance } from './mp-instance';
12
12
  import { appendUserInfo } from './user-utils';
13
+ import { hasExplicitIdentifier } from './identity-utils';
13
14
 
14
15
  export interface IAPIClient {
15
16
  uploader: BatchUploader | null;
@@ -88,6 +89,35 @@ export default function APIClient(
88
89
  });
89
90
  };
90
91
 
92
+ // When noFunctional is set and there are no identities passed, the SDK will not fully initialize.
93
+ // In this case, there will be no MPID, but we still want kits to initialize and forward the event to kits.
94
+ // The original event is queued for the MP server upload path so it can be sent once an MPID is returned.
95
+ // Returns true if the event was handled by this path (caller should return early).
96
+ const handleNoFunctionalPreMpidEvent = (event: SDKEvent, mpid: string | undefined): boolean => {
97
+ const noFunctionalWithoutId =
98
+ mpInstance._CookieConsentManager?.getNoFunctional() &&
99
+ !hasExplicitIdentifier(mpInstance._Store);
100
+
101
+ if (!noFunctionalWithoutId || mpid || !mpInstance._Store.configurationLoaded || mpInstance._Store.requireDelay) {
102
+ return false;
103
+ }
104
+
105
+ let forwarderEvent = event;
106
+ if (kitBlocker?.kitBlockingEnabled) {
107
+ forwarderEvent = kitBlocker.createBlockedEvent(event);
108
+ }
109
+ if (forwarderEvent) {
110
+ mpInstance._Forwarders.sendEventToForwarders(forwarderEvent);
111
+ event.AlreadySentToForwarders = true;
112
+ }
113
+ mpInstance.Logger.verbose(
114
+ 'noFunctional event forwarded to kits and queued for MP server upload when MPID is available.'
115
+ );
116
+ mpInstance._Store.eventQueue.push(event);
117
+
118
+ return true;
119
+ };
120
+
91
121
  this.sendEventToServer = function(event, _options) {
92
122
  const defaultOptions = {
93
123
  shouldUploadEvent: true,
@@ -112,8 +142,14 @@ export default function APIClient(
112
142
  mpInstance._Store.integrationDelayTimeoutStart,
113
143
  Date.now()
114
144
  );
115
- // We queue events if there is no MPID (MPID is null, or === 0), or there are integrations that that require this to stall because integration attributes
116
- // need to be set, or if we are still fetching the config (self hosted only), and so require delaying events
145
+
146
+ if (handleNoFunctionalPreMpidEvent(event, mpid)) {
147
+ return;
148
+ }
149
+
150
+ // We queue events if there is no MPID (MPID is null, or === 0), or there are integrations that
151
+ // require this to stall because integration attributes need to be set, or if we are still
152
+ // fetching the config (self hosted only), and so require delaying events
117
153
  if (
118
154
  !mpid ||
119
155
  mpInstance._Store.requireDelay ||
@@ -146,8 +182,10 @@ export default function APIClient(
146
182
  }
147
183
 
148
184
  // We need to check event again, because kitblocking
149
- // can nullify the event
150
- if (event) {
185
+ // can nullify the event.
186
+ // Skip if forwarders were already called in the noFunctional pre-MPID path
187
+ // to prevent double-sending when the event queue is later flushed.
188
+ if (event && !event.AlreadySentToForwarders) {
151
189
  mpInstance._Forwarders.sendEventToForwarders(event);
152
190
  }
153
191
  }
@@ -74,7 +74,10 @@ export class BatchUploader {
74
74
  // so that we don't have to check it every time
75
75
  this.offlineStorageEnabled = this.isOfflineStorageAvailable();
76
76
 
77
- if (this.offlineStorageEnabled) {
77
+ // When noFunctional is true, prevent events/batches storage
78
+ const noFunctional = mpInstance._CookieConsentManager?.getNoFunctional();
79
+
80
+ if (this.offlineStorageEnabled && !noFunctional) {
78
81
  this.eventVault = new SessionStorageVault<SDKEvent[]>(
79
82
  `${mpInstance._Store.storageName}-events`,
80
83
  {
package/src/constants.ts CHANGED
@@ -139,6 +139,8 @@ const Constants = {
139
139
  identityUrl: 'identity.mparticle.com/v1/',
140
140
  aliasUrl: 'jssdks.mparticle.com/v1/identity/',
141
141
  userAudienceUrl: 'nativesdks.mparticle.com/v1/',
142
+ loggingUrl: 'apps.rokt-api.com/v1/log',
143
+ errorUrl: 'apps.rokt-api.com/v1/errors',
142
144
  },
143
145
  // These are the paths that are used to construct the CNAME urls
144
146
  CNAMEUrlPaths: {
@@ -148,6 +150,8 @@ const Constants = {
148
150
  configUrl: '/tags/JS/v2/',
149
151
  identityUrl: '/identity/v1/',
150
152
  aliasUrl: '/webevents/v1/identity/',
153
+ loggingUrl: '/v1/log',
154
+ errorUrl: '/v1/errors',
151
155
  },
152
156
  Base64CookieKeys: {
153
157
  csm: 1,
@@ -73,6 +73,12 @@ export default function CookieSyncManager(
73
73
  return;
74
74
  }
75
75
 
76
+ // When noFunctional is true, persistence is not saved, so we cannot track cookie sync
77
+ // dates. Skip cookie sync to avoid running it on every page load.
78
+ if (mpInstance._CookieConsentManager?.getNoFunctional()) {
79
+ return;
80
+ }
81
+
76
82
  const persistence = mpInstance._Persistence.getPersistence();
77
83
 
78
84
  if (isEmpty(persistence)) {
@@ -8,10 +8,12 @@ export default class ForegroundTimeTracker {
8
8
  public startTime: number = 0;
9
9
  public totalTime: number = 0;
10
10
 
11
- constructor(timerKey: string) {
11
+ constructor(timerKey: string, private noFunctional: boolean = false) {
12
12
  this.localStorageName = `mprtcl-tos-${timerKey}`;
13
13
  this.timerVault = new LocalStorageVault<number>(this.localStorageName);
14
- this.loadTimeFromStorage();
14
+ if (!this.noFunctional) {
15
+ this.loadTimeFromStorage();
16
+ }
15
17
  this.addHandlers();
16
18
  if (document.hidden === false) {
17
19
  this.startTracking();
@@ -63,7 +65,7 @@ export default class ForegroundTimeTracker {
63
65
  }
64
66
 
65
67
  public updateTimeInPersistence(): void {
66
- if (this.isTrackerActive) {
68
+ if (this.isTrackerActive && !this.noFunctional) {
67
69
  this.timerVault.store(Math.round(this.totalTime));
68
70
  }
69
71
  }
@@ -1,5 +1,5 @@
1
1
  import Constants, { ONE_DAY_IN_SECONDS, MILLIS_IN_ONE_SEC } from './constants';
2
- import { Dictionary, parseNumber, isObject, generateHash } from './utils';
2
+ import { Dictionary, parseNumber, isObject, generateHash, isEmpty } from './utils';
3
3
  import { BaseVault } from './vault';
4
4
  import Types from './types';
5
5
  import {
@@ -13,6 +13,7 @@ import {
13
13
  IIdentityResponse,
14
14
  IMParticleUser,
15
15
  } from './identity-user-interfaces';
16
+ import { IStore } from './store';
16
17
 
17
18
  const { Identify, Modify, Login, Logout } = Constants.IdentityMethods;
18
19
  export const CACHE_HEADER = 'x-mp-max-age' as const;
@@ -298,3 +299,25 @@ export const hasIdentityRequestChanged = (
298
299
  JSON.stringify(currentUserIdentities) !== JSON.stringify(newIdentities)
299
300
  );
300
301
  };
302
+
303
+ /**
304
+ * Checks if deviceId or other user identifiers (like email) were explicitly provided
305
+ * by the partner via config.deviceId or config.identifyRequest.userIdentities.
306
+ * When noFunctional is true, then cookies are blocked, so the partner must explicitly
307
+ * pass deviceId or other identifiers to prevent new users from being created on each page load.
308
+ *
309
+ * @param store - The SDK store (provides SDKConfig.deviceId and SDKConfig.identifyRequest.userIdentities)
310
+ * @returns true if deviceId or other identifiers were explicitly provided in config, false otherwise
311
+ */
312
+ export const hasExplicitIdentifier = (store: IStore | undefined | null): boolean => {
313
+ const userIdentities = store?.SDKConfig?.identifyRequest?.userIdentities;
314
+ if (
315
+ userIdentities &&
316
+ isObject(userIdentities) &&
317
+ !isEmpty(userIdentities) &&
318
+ Object.values(userIdentities).some(Boolean)
319
+ ) {
320
+ return true;
321
+ }
322
+ return !!store?.SDKConfig?.deviceId;
323
+ };
@@ -25,6 +25,7 @@ import {
25
25
  IIdentityResponse,
26
26
  } from './identity-user-interfaces';
27
27
  import { IMParticleWebSDKInstance } from './mp-instance';
28
+ import { ErrorCodes } from './logging/types';
28
29
 
29
30
  const { HTTPCodes, Messages, IdentityMethods } = Constants;
30
31
 
@@ -326,10 +327,13 @@ export default function IdentityAPIClient(
326
327
  const requestCount = mpInstance._Store.identifyRequestCount;
327
328
  mpInstance.captureTiming(`${requestCount}-identityRequestEnd`);
328
329
  }
329
-
330
+
330
331
  const errorMessage = (err as Error).message || err.toString();
331
- Logger.error('Error sending identity request to servers - ' + errorMessage);
332
-
332
+ Logger.error(
333
+ 'Error sending identity request to servers' + ' - ' + errorMessage,
334
+ ErrorCodes.IDENTITY_REQUEST
335
+ );
336
+
333
337
  mpInstance.processQueueOnIdentityFailure?.();
334
338
  invokeCallback(callback, HTTPCodes.noHttpCoverage, errorMessage);
335
339
  }
package/src/logger.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { LogLevelType, SDKInitConfig, SDKLoggerApi } from './sdkRuntimeModels';
2
+ import { ReportingLogger } from './logging/reportingLogger';
3
+ import { ErrorCodes } from './logging/types';
2
4
 
3
5
  export type ILoggerConfig = Pick<SDKInitConfig, 'logLevel' | 'logger'>;
4
6
  export type IConsoleLogger = Partial<Pick<SDKLoggerApi, 'error' | 'warning' | 'verbose'>>;
@@ -6,10 +8,14 @@ export type IConsoleLogger = Partial<Pick<SDKLoggerApi, 'error' | 'warning' | 'v
6
8
  export class Logger {
7
9
  private logLevel: LogLevelType;
8
10
  private logger: IConsoleLogger;
11
+ private reportingLogger: ReportingLogger;
9
12
 
10
- constructor(config: ILoggerConfig) {
13
+ constructor(config: ILoggerConfig,
14
+ reportingLogger?: ReportingLogger,
15
+ ) {
11
16
  this.logLevel = config.logLevel ?? LogLevelType.Warning;
12
17
  this.logger = config.logger ?? new ConsoleLogger();
18
+ this.reportingLogger = reportingLogger;
13
19
  }
14
20
 
15
21
  public verbose(msg: string): void {
@@ -22,21 +28,24 @@ export class Logger {
22
28
  }
23
29
 
24
30
  public warning(msg: string): void {
25
- if(this.logLevel === LogLevelType.None)
31
+ if(this.logLevel === LogLevelType.None)
26
32
  return;
27
33
 
28
- if (this.logger.warning &&
34
+ if (this.logger.warning &&
29
35
  (this.logLevel === LogLevelType.Verbose || this.logLevel === LogLevelType.Warning)) {
30
36
  this.logger.warning(msg);
31
37
  }
32
38
  }
33
39
 
34
- public error(msg: string): void {
35
- if(this.logLevel === LogLevelType.None)
40
+ public error(msg: string, codeForReporting?: ErrorCodes): void {
41
+ if(this.logLevel === LogLevelType.None)
36
42
  return;
37
43
 
38
44
  if (this.logger.error) {
39
45
  this.logger.error(msg);
46
+ if (codeForReporting) {
47
+ this.reportingLogger?.error(msg, codeForReporting);
48
+ }
40
49
  }
41
50
  }
42
51
 
@@ -0,0 +1,192 @@
1
+ import { ErrorCodes, LogRequestBody, WSDKErrorSeverity } from "./types";
2
+ import { FetchUploader, IFetchPayload } from "../uploaders";
3
+ import { IStore, SDKConfig } from "../store";
4
+ import { SDKInitConfig } from "../sdkRuntimeModels";
5
+ import Constants from "../constants";
6
+
7
+ // Header key constants
8
+ const HEADER_ACCEPT = 'Accept' as const;
9
+ const HEADER_CONTENT_TYPE = 'Content-Type' as const;
10
+ const HEADER_ROKT_LAUNCHER_VERSION = 'rokt-launcher-version' as const;
11
+ const HEADER_ROKT_LAUNCHER_INSTANCE_GUID = 'rokt-launcher-instance-guid' as const;
12
+ const HEADER_ROKT_WSDK_VERSION = 'rokt-wsdk-version' as const;
13
+
14
+ interface IReportingLoggerPayload extends IFetchPayload {
15
+ headers: IFetchPayload['headers'] & {
16
+ [HEADER_ROKT_LAUNCHER_INSTANCE_GUID]?: string;
17
+ [HEADER_ROKT_LAUNCHER_VERSION]: string;
18
+ [HEADER_ROKT_WSDK_VERSION]: string;
19
+ };
20
+ body: string;
21
+ }
22
+
23
+ export class ReportingLogger {
24
+ private readonly isEnabled: boolean;
25
+ private readonly reporter: string = 'mp-wsdk';
26
+ private readonly rateLimiter: IRateLimiter;
27
+ private store: IStore | null;
28
+ private readonly loggingUrl: string;
29
+ private readonly errorUrl: string;
30
+ private readonly isLoggingEnabled: boolean;
31
+
32
+ constructor(
33
+ config: SDKConfig | SDKInitConfig | any,
34
+ private readonly sdkVersion: string,
35
+ store?: IStore,
36
+ private readonly launcherInstanceGuid?: string,
37
+ rateLimiter?: IRateLimiter,
38
+ ) {
39
+ this.loggingUrl = `https://${config.loggingUrl || Constants.DefaultBaseUrls.loggingUrl}`;
40
+ this.errorUrl = `https://${config.errorUrl || Constants.DefaultBaseUrls.errorUrl}`;
41
+ this.isLoggingEnabled = config.isLoggingEnabled || false;
42
+ this.store = store ?? null;
43
+ this.isEnabled = this.isReportingEnabled();
44
+ this.rateLimiter = rateLimiter ?? new RateLimiter();
45
+ }
46
+
47
+ public setStore(store: IStore): void {
48
+ this.store = store;
49
+ }
50
+
51
+ public info(msg: string, code?: ErrorCodes) {
52
+ this.sendLog(WSDKErrorSeverity.INFO, msg, code);
53
+ }
54
+
55
+ public error(msg: string, code?: ErrorCodes, stackTrace?: string) {
56
+ this.sendError(WSDKErrorSeverity.ERROR, msg, code, stackTrace);
57
+ }
58
+
59
+ public warning(msg: string, code?: ErrorCodes) {
60
+ this.sendError(WSDKErrorSeverity.WARNING, msg, code);
61
+ }
62
+
63
+ private sendToServer(url: string, severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void {
64
+ if (!this.canSendLog(severity))
65
+ return;
66
+
67
+ try {
68
+ const logRequest = this.buildLogRequest(severity, msg, code, stackTrace);
69
+ const uploader = new FetchUploader(url);
70
+ const payload: IReportingLoggerPayload = {
71
+ method: 'POST',
72
+ headers: this.getHeaders(),
73
+ body: JSON.stringify(logRequest),
74
+ };
75
+ uploader.upload(payload).catch((error) => {
76
+ console.error('ReportingLogger: Failed to send log', error);
77
+ });
78
+ } catch (error) {
79
+ console.error('ReportingLogger: Failed to send log', error);
80
+ }
81
+ }
82
+
83
+ private sendLog(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void {
84
+ this.sendToServer(this.loggingUrl, severity, msg, code, stackTrace);
85
+ }
86
+
87
+ private sendError(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): void {
88
+ this.sendToServer(this.errorUrl, severity, msg, code, stackTrace);
89
+ }
90
+
91
+ private buildLogRequest(severity: WSDKErrorSeverity, msg: string, code?: ErrorCodes, stackTrace?: string): LogRequestBody {
92
+ return {
93
+ additionalInformation: {
94
+ message: msg,
95
+ version: this.getVersion(),
96
+ },
97
+ severity: severity,
98
+ code: code ?? ErrorCodes.UNKNOWN_ERROR,
99
+ url: this.getUrl(),
100
+ deviceInfo: this.getUserAgent(),
101
+ stackTrace: stackTrace,
102
+ reporter: this.reporter,
103
+ // Integration will be set to integrationName once the kit connects via RoktManager.attachKit()
104
+ integration: this.store?.getIntegrationName() ?? 'mp-wsdk'
105
+ };
106
+ }
107
+
108
+ private getVersion(): string {
109
+ return this.store?.getIntegrationName?.() ?? `mParticle_wsdkv_${this.sdkVersion}`;
110
+ }
111
+
112
+ private isReportingEnabled(): boolean {
113
+ return this.isDebugModeEnabled() ||
114
+ (this.isRoktDomainPresent() && this.isFeatureFlagEnabled());
115
+ }
116
+
117
+ private isRoktDomainPresent(): boolean {
118
+ return typeof window !== 'undefined' && Boolean(window['ROKT_DOMAIN']);
119
+ }
120
+
121
+ private isFeatureFlagEnabled = (): boolean => this.isLoggingEnabled;
122
+
123
+ private isDebugModeEnabled(): boolean {
124
+ return (
125
+ typeof window !== 'undefined' &&
126
+ (window.
127
+ location?.
128
+ search?.
129
+ toLowerCase()?.
130
+ includes('mp_enable_logging=true') ?? false)
131
+ );
132
+ }
133
+
134
+ private canSendLog(severity: WSDKErrorSeverity): boolean {
135
+ return this.isEnabled && !this.isRateLimited(severity);
136
+ }
137
+
138
+ private isRateLimited(severity: WSDKErrorSeverity): boolean {
139
+ return this.rateLimiter.incrementAndCheck(severity);
140
+ }
141
+
142
+ private getUrl(): string | undefined {
143
+ return typeof window !== 'undefined' ? window.location?.href : undefined;
144
+ }
145
+
146
+ private getUserAgent(): string | undefined {
147
+ return typeof window !== 'undefined' ? window.navigator?.userAgent : undefined;
148
+ }
149
+
150
+ private getHeaders(): IReportingLoggerPayload['headers'] {
151
+ const headers: IReportingLoggerPayload['headers'] = {
152
+ [HEADER_ACCEPT]: 'text/plain;charset=UTF-8',
153
+ [HEADER_CONTENT_TYPE]: 'application/json',
154
+ [HEADER_ROKT_LAUNCHER_VERSION]: this.getVersion(),
155
+ [HEADER_ROKT_WSDK_VERSION]: 'joint',
156
+ };
157
+
158
+ if (this.launcherInstanceGuid) {
159
+ headers[HEADER_ROKT_LAUNCHER_INSTANCE_GUID] = this.launcherInstanceGuid;
160
+ }
161
+
162
+ const accountId = this.store?.getRoktAccountId?.();
163
+ if (accountId) {
164
+ headers['rokt-account-id'] = accountId;
165
+ }
166
+
167
+ return headers;
168
+ }
169
+ }
170
+
171
+ export interface IRateLimiter {
172
+ incrementAndCheck(severity: WSDKErrorSeverity): boolean;
173
+ }
174
+
175
+ export class RateLimiter implements IRateLimiter {
176
+ private readonly rateLimits: Map<WSDKErrorSeverity, number> = new Map([
177
+ [WSDKErrorSeverity.ERROR, 10],
178
+ [WSDKErrorSeverity.WARNING, 10],
179
+ [WSDKErrorSeverity.INFO, 10],
180
+ ]);
181
+ private logCount: Map<WSDKErrorSeverity, number> = new Map();
182
+
183
+ public incrementAndCheck(severity: WSDKErrorSeverity): boolean {
184
+ const count = this.logCount.get(severity) || 0;
185
+ const limit = this.rateLimits.get(severity) || 10;
186
+
187
+ const newCount = count + 1;
188
+ this.logCount.set(severity, newCount);
189
+
190
+ return newCount > limit;
191
+ }
192
+ }
@@ -0,0 +1,32 @@
1
+ import { valueof } from '../utils';
2
+
3
+ export const ErrorCodes = {
4
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR',
5
+ UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION',
6
+ IDENTITY_REQUEST: 'IDENTITY_REQUEST',
7
+ } as const;
8
+
9
+ export type ErrorCodes = valueof<typeof ErrorCodes>;
10
+
11
+ export type ErrorCode = ErrorCodes | string;
12
+
13
+ export const WSDKErrorSeverity = {
14
+ ERROR: 'ERROR',
15
+ INFO: 'INFO',
16
+ WARNING: 'WARNING',
17
+ } as const;
18
+
19
+ export type WSDKErrorSeverity = (typeof WSDKErrorSeverity)[keyof typeof WSDKErrorSeverity];
20
+
21
+ export type ErrorsRequestBody = {
22
+ additionalInformation?: Record<string, string>;
23
+ code: ErrorCode;
24
+ severity: WSDKErrorSeverity;
25
+ stackTrace?: string;
26
+ deviceInfo?: string;
27
+ integration?: string;
28
+ reporter?: string;
29
+ url?: string;
30
+ };
31
+
32
+ export type LogRequestBody = ErrorsRequestBody;