@mparticle/web-sdk 2.51.0 → 2.53.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.51.0",
3
+ "version": "2.53.0",
4
4
  "description": "mParticle core SDK for web applications",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -257,15 +257,15 @@ export class BatchUploader {
257
257
  return;
258
258
  }
259
259
 
260
- const { verbose } = this.mpInstance.Logger;
260
+ const { Logger } = this.mpInstance;
261
261
 
262
262
  this.eventsQueuedForProcessing.push(event);
263
263
  if (this.offlineStorageEnabled && this.eventVault) {
264
264
  this.eventVault.store(this.eventsQueuedForProcessing);
265
265
  }
266
266
 
267
- verbose(`Queuing event: ${JSON.stringify(event)}`);
268
- verbose(`Queued event count: ${this.eventsQueuedForProcessing.length}`);
267
+ Logger.verbose(`Queuing event: ${JSON.stringify(event)}`);
268
+ Logger.verbose(`Queued event count: ${this.eventsQueuedForProcessing.length}`);
269
269
 
270
270
  if (this.shouldTriggerImmediateUpload(event.EventDataType)) {
271
271
  this.prepareAndUpload(false, false);
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Cookie consent flags control SDK behavior based on user consent preferences for Rokt integration.
3
+ * @see https://docs.rokt.com/developers/integration-guides/web/cookie-consent-flags/
4
+ */
5
+ export interface ICookieConsentFlags {
6
+ /**
7
+ * When true, indicates the user has opted out of functional cookies/tracking.
8
+ * Default: false (functional tracking is allowed)
9
+ */
10
+ noFunctional: boolean;
11
+
12
+ /**
13
+ * When true, indicates the user has opted out of targeting cookies/tracking.
14
+ * This flag is used to block cookie syncs and other targeting-related features.
15
+ * Default: false (targeting is allowed)
16
+ */
17
+ noTargeting: boolean;
18
+ }
19
+
20
+ export interface ICookieConsentManager {
21
+ /**
22
+ * Targeting is allowed when noTargeting is false (default)
23
+ */
24
+ getNoTargeting: () => boolean;
25
+
26
+ /**
27
+ * Functional tracking is allowed when noFunctional is false (default)
28
+ */
29
+ getNoFunctional: () => boolean;
30
+ }
31
+
32
+ /**
33
+ * CookieConsentManager handles storage and access of consent flags (noFunctional, noTargeting)
34
+ * that are passed via launcherOptions during SDK initialization.
35
+ *
36
+ * These flags allow Rokt integration to respect user privacy choices.
37
+ *
38
+ * Default behavior: Both flags default to false, meaning all features are allowed
39
+ * unless explicitly opted out by the user.
40
+ */
41
+ export default class CookieConsentManager implements ICookieConsentManager {
42
+ constructor(private flags: ICookieConsentFlags) {
43
+ this.flags = {
44
+ noFunctional: flags.noFunctional === true,
45
+ noTargeting: flags.noTargeting === true,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Returns true if third-party targeting is disabled.
51
+ * Targeting is allowed when noTargeting is false (default).
52
+ */
53
+ getNoTargeting(): boolean {
54
+ return this.flags.noTargeting;
55
+ }
56
+
57
+ /**
58
+ * Returns true if functional tracking is disabled.
59
+ * Functional tracking is allowed when noFunctional is false (default).
60
+ */
61
+ getNoFunctional(): boolean {
62
+ return this.flags.noFunctional;
63
+ }
64
+ }
@@ -13,6 +13,17 @@ const { InformationMessages } = Messages;
13
13
 
14
14
  export const DAYS_IN_MILLISECONDS = 1000 * 60 * 60 * 24;
15
15
 
16
+ // Partner module IDs for cookie sync configurations
17
+ export const PARTNER_MODULE_IDS = {
18
+ AdobeEventForwarder: 11,
19
+ DoubleclickDFP: 41,
20
+ AppNexus: 50,
21
+ Lotame: 58,
22
+ TradeDesk: 103,
23
+ VerizonMedia: 155,
24
+ Rokt: 1277,
25
+ } as const;
26
+
16
27
  export type CookieSyncDates = Dictionary<number>;
17
28
 
18
29
  export interface IPixelConfiguration {
@@ -98,6 +109,11 @@ export default function CookieSyncManager(
98
109
  return;
99
110
  }
100
111
 
112
+ // For Rokt, block cookie sync when noTargeting privacy flag is true
113
+ if (moduleId === PARTNER_MODULE_IDS.Rokt && mpInstance._CookieConsentManager.getNoTargeting()) {
114
+ return;
115
+ }
116
+
101
117
  const { isEnabledForUserConsent } = mpInstance._Consent;
102
118
 
103
119
  if (!isEnabledForUserConsent(filteringConsentRuleValues, mpInstance.Identity.getCurrentUser())) {
@@ -111,8 +127,12 @@ export default function CookieSyncManager(
111
127
  return;
112
128
  }
113
129
 
114
- // Url for cookie sync pixel
115
- const fullUrl = createCookieSyncUrl(mpid, pixelUrl, redirectUrl)
130
+ // The Trade Desk requires a URL parameter for GDPR enabled users.
131
+ // It is optional but to simplify the code, we add it for all Trade
132
+ // // Desk cookie syncs.
133
+ const domain = moduleId === PARTNER_MODULE_IDS.TradeDesk ? window.location.hostname : undefined;
134
+ // Add domain parameter for Trade Desk
135
+ const fullUrl = createCookieSyncUrl(mpid, pixelUrl, redirectUrl, domain);
116
136
 
117
137
  self.performCookieSync(
118
138
  fullUrl,
@@ -107,12 +107,14 @@ export interface IIdentityRequest {
107
107
  ): IdentityPreProcessResult;
108
108
  }
109
109
 
110
+ export type AliasRequestScope = 'device' | 'mpid';
111
+
110
112
  export interface IAliasRequest {
111
113
  destinationMpid: MPID;
112
114
  sourceMpid: MPID;
113
115
  startTime: number;
114
116
  endTime: number;
115
- scope?: string;
117
+ scope?: AliasRequestScope;
116
118
  }
117
119
 
118
120
  export interface IAliasCallback {
@@ -151,7 +153,8 @@ export interface SDKIdentityApi {
151
153
  ): void;
152
154
  createAliasRequest?(
153
155
  sourceUser: IMParticleUser,
154
- destinationUser: IMParticleUser
156
+ destinationUser: IMParticleUser,
157
+ scope?: AliasRequestScope
155
158
  ): IAliasRequest;
156
159
  }
157
160
 
package/src/identity.js CHANGED
@@ -195,6 +195,7 @@ export default function Identity(mpInstance) {
195
195
  SourceMpid: aliasRequest.sourceMpid,
196
196
  StartUnixtimeMs: aliasRequest.startTime,
197
197
  EndUnixtimeMs: aliasRequest.endTime,
198
+ Scope: aliasRequest.scope,
198
199
  };
199
200
  },
200
201
 
@@ -744,7 +745,7 @@ export default function Identity(mpInstance) {
744
745
  after applying this adjustment it will be impossible to create an aliasRequest passes the aliasUsers()
745
746
  validation that the startTime must be less than the endTime
746
747
  */
747
- createAliasRequest: function(sourceUser, destinationUser) {
748
+ createAliasRequest: function(sourceUser, destinationUser, scope) {
748
749
  try {
749
750
  if (!destinationUser || !sourceUser) {
750
751
  mpInstance.Logger.error(
@@ -788,6 +789,7 @@ export default function Identity(mpInstance) {
788
789
  sourceMpid: sourceUser.getMPID(),
789
790
  startTime: startTime,
790
791
  endTime: endTime,
792
+ scope: scope || 'device',
791
793
  };
792
794
  } catch (e) {
793
795
  mpInstance.Logger.error(
@@ -88,12 +88,12 @@ export default function IdentityAPIClient(
88
88
  aliasRequest: IAliasRequest,
89
89
  aliasCallback: IAliasCallback
90
90
  ) {
91
- const { verbose, error } = mpInstance.Logger;
91
+ const { Logger } = mpInstance;
92
92
  const { invokeAliasCallback } = mpInstance._Helpers;
93
93
  const { aliasUrl } = mpInstance._Store.SDKConfig;
94
94
  const { devToken: apiKey } = mpInstance._Store;
95
95
 
96
- verbose(Messages.InformationMessages.SendAliasHttp);
96
+ Logger.verbose(Messages.InformationMessages.SendAliasHttp);
97
97
 
98
98
  // https://go.mparticle.com/work/SQDSDKS-6750
99
99
  const uploadUrl = `https://${aliasUrl}${apiKey}/Alias`;
@@ -136,7 +136,7 @@ export default function IdentityAPIClient(
136
136
  try {
137
137
  aliasResponseBody = await response.json();
138
138
  } catch (e) {
139
- verbose('The request has no response body');
139
+ Logger.verbose('The request has no response body');
140
140
  }
141
141
  } else {
142
142
  // https://go.mparticle.com/work/SQDSDKS-6568
@@ -171,11 +171,11 @@ export default function IdentityAPIClient(
171
171
 
172
172
  }
173
173
 
174
- verbose(message);
174
+ Logger.verbose(message);
175
175
  invokeAliasCallback(aliasCallback, response.status, errorMessage);
176
176
  } catch (e) {
177
177
  const errorMessage = (e as Error).message || e.toString();
178
- error('Error sending alias request to mParticle servers. ' + errorMessage);
178
+ Logger.error('Error sending alias request to mParticle servers. ' + errorMessage);
179
179
  invokeAliasCallback(
180
180
  aliasCallback,
181
181
  HTTPCodes.noHttpCoverage,
@@ -193,15 +193,14 @@ export default function IdentityAPIClient(
193
193
  mpid: MPID,
194
194
  knownIdentities: UserIdentities
195
195
  ) {
196
- const { verbose, error } = mpInstance.Logger;
197
196
  const { invokeCallback } = mpInstance._Helpers;
198
-
199
- verbose(Messages.InformationMessages.SendIdentityBegin);
197
+ const { Logger } = mpInstance;
198
+ Logger.verbose(Messages.InformationMessages.SendIdentityBegin);
200
199
  if (!identityApiRequest) {
201
- error(Messages.ErrorMessages.APIRequestEmpty);
200
+ Logger.error(Messages.ErrorMessages.APIRequestEmpty);
202
201
  return;
203
202
  }
204
- verbose(Messages.InformationMessages.SendIdentityHttp);
203
+ Logger.verbose(Messages.InformationMessages.SendIdentityHttp);
205
204
 
206
205
  if (mpInstance._Store.identityCallInFlight) {
207
206
  invokeCallback(
@@ -289,7 +288,7 @@ export default function IdentityAPIClient(
289
288
 
290
289
  mpInstance._Store.identityCallInFlight = false;
291
290
 
292
- verbose(message);
291
+ Logger.verbose(message);
293
292
  parseIdentityResponse(
294
293
  identityResponse,
295
294
  previousMPID,
@@ -304,7 +303,7 @@ export default function IdentityAPIClient(
304
303
 
305
304
  const errorMessage = (err as Error).message || err.toString();
306
305
 
307
- error('Error sending identity request to servers' + ' - ' + errorMessage);
306
+ Logger.error('Error sending identity request to servers' + ' - ' + errorMessage);
308
307
  invokeCallback(
309
308
  callback,
310
309
  HTTPCodes.noHttpCoverage,
package/src/logger.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { LogLevelType, SDKInitConfig, SDKLoggerApi } from './sdkRuntimeModels';
2
+
3
+ export type ILoggerConfig = Pick<SDKInitConfig, 'logLevel' | 'logger'>;
4
+ export type IConsoleLogger = Partial<Pick<SDKLoggerApi, 'error' | 'warning' | 'verbose'>>;
5
+
6
+ export class Logger {
7
+ private logLevel: LogLevelType;
8
+ private logger: IConsoleLogger;
9
+
10
+ constructor(config: ILoggerConfig) {
11
+ this.logLevel = config.logLevel ?? LogLevelType.Warning;
12
+ this.logger = config.logger ?? new ConsoleLogger();
13
+ }
14
+
15
+ public verbose(msg: string): void {
16
+ if(this.logLevel === LogLevelType.None)
17
+ return;
18
+
19
+ if (this.logger.verbose && this.logLevel === LogLevelType.Verbose) {
20
+ this.logger.verbose(msg);
21
+ }
22
+ }
23
+
24
+ public warning(msg: string): void {
25
+ if(this.logLevel === LogLevelType.None)
26
+ return;
27
+
28
+ if (this.logger.warning &&
29
+ (this.logLevel === LogLevelType.Verbose || this.logLevel === LogLevelType.Warning)) {
30
+ this.logger.warning(msg);
31
+ }
32
+ }
33
+
34
+ public error(msg: string): void {
35
+ if(this.logLevel === LogLevelType.None)
36
+ return;
37
+
38
+ if (this.logger.error) {
39
+ this.logger.error(msg);
40
+ }
41
+ }
42
+
43
+ public setLogLevel(newLogLevel: LogLevelType): void {
44
+ this.logLevel = newLogLevel;
45
+ }
46
+ }
47
+
48
+ export class ConsoleLogger implements IConsoleLogger {
49
+ public verbose(msg: string): void {
50
+ if (console && console.info) {
51
+ console.info(msg);
52
+ }
53
+ }
54
+
55
+ public error(msg: string): void {
56
+ if (console && console.error) {
57
+ console.error(msg);
58
+ }
59
+ }
60
+
61
+ public warning(msg: string): void {
62
+ if (console && console.warn) {
63
+ console.warn(msg);
64
+ }
65
+ }
66
+ }
@@ -25,7 +25,7 @@ import CookieSyncManager, { ICookieSyncManager } from './cookieSyncManager';
25
25
  import SessionManager, { ISessionManager } from './sessionManager';
26
26
  import Ecommerce from './ecommerce';
27
27
  import Store, { IStore } from './store';
28
- import Logger from './logger';
28
+ import { Logger } from './logger';
29
29
  import Persistence from './persistence';
30
30
  import Events from './events';
31
31
  import Forwarders from './forwarders';
@@ -51,6 +51,7 @@ import { IPersistence } from './persistence.interfaces';
51
51
  import ForegroundTimer from './foregroundTimeTracker';
52
52
  import RoktManager, { IRoktOptions } from './roktManager';
53
53
  import filteredMparticleUser from './filteredMparticleUser';
54
+ import CookieConsentManager, { ICookieConsentManager } from './cookieConsentManager';
54
55
 
55
56
  export interface IErrorLogMessage {
56
57
  message?: string;
@@ -82,6 +83,7 @@ export interface IMParticleWebSDKInstance extends MParticleWebSDK {
82
83
  _IntegrationCapture: IntegrationCapture;
83
84
  _NativeSdkHelpers: INativeSdkHelpers;
84
85
  _Persistence: IPersistence;
86
+ _CookieConsentManager: ICookieConsentManager;
85
87
  _RoktManager: RoktManager;
86
88
  _SessionManager: ISessionManager;
87
89
  _ServerModel: IServerModel;
@@ -227,6 +229,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
227
229
  if (instance._Store) {
228
230
  delete instance._Store;
229
231
  }
232
+ instance.Logger = new Logger(config);
230
233
  instance._Store = new Store(config, instance);
231
234
  instance._Store.isLocalStorageAvailable = instance._Persistence.determineLocalStorageAvailability(
232
235
  window.localStorage
@@ -1554,6 +1557,10 @@ function runPreConfigFetchInitialization(mpInstance, apiKey, config) {
1554
1557
  window.mParticle.Store = mpInstance._Store;
1555
1558
  mpInstance.Logger.verbose(StartingInitialization);
1556
1559
 
1560
+ // Initialize CookieConsentManager with privacy flags from launcherOptions
1561
+ const { noFunctional, noTargeting } = config?.launcherOptions ?? {};
1562
+ mpInstance._CookieConsentManager = new CookieConsentManager({ noFunctional, noTargeting });
1563
+
1557
1564
  // Check to see if localStorage is available before main configuration runs
1558
1565
  // since we will need this for the current implementation of user persistence
1559
1566
  // TODO: Refactor this when we refactor User Identity Persistence
@@ -368,10 +368,10 @@ function mParticleInstanceManager(this: IMParticleInstanceManager) {
368
368
  aliasUsers: function(aliasRequest, callback) {
369
369
  self.getInstance().Identity.aliasUsers(aliasRequest, callback);
370
370
  },
371
- createAliasRequest: function(sourceUser, destinationUser) {
371
+ createAliasRequest: function(sourceUser, destinationUser, scope) {
372
372
  return self
373
373
  .getInstance()
374
- .Identity.createAliasRequest(sourceUser, destinationUser);
374
+ .Identity.createAliasRequest(sourceUser, destinationUser, scope);
375
375
  },
376
376
  getCurrentUser: function() {
377
377
  return self.getInstance().Identity.getCurrentUser();
@@ -252,15 +252,51 @@ export default class RoktManager {
252
252
  }
253
253
  }
254
254
 
255
- public hashAttributes(attributes: IRoktPartnerAttributes): Promise<IRoktPartnerAttributes> {
256
- if (!this.isReady()) {
257
- return this.deferredCall<IRoktPartnerAttributes>('hashAttributes', attributes);
258
- }
259
-
255
+ /**
256
+ * Hashes attributes and returns both original and hashed versions
257
+ * with Rokt-compatible key names (like emailsha256, mobilesha256)
258
+ *
259
+ *
260
+ * @param {IRoktPartnerAttributes} attributes - Attributes to hash
261
+ * @returns {Promise<IRoktPartnerAttributes>} Object with both original and hashed attributes
262
+ *
263
+ */
264
+ public async hashAttributes(attributes: IRoktPartnerAttributes): Promise<IRoktPartnerAttributes> {
260
265
  try {
261
- return this.kit.hashAttributes(attributes);
266
+ if (!attributes || typeof attributes !== 'object') {
267
+ return {};
268
+ }
269
+
270
+ // Get own property keys only
271
+ const keys = Object.keys(attributes);
272
+
273
+ if (keys.length === 0) {
274
+ return {};
275
+ }
276
+
277
+ // Hash all attributes in parallel
278
+ const hashPromises = keys.map(async (key) => {
279
+ const attributeValue = attributes[key];
280
+ const hashedValue = await this.hashSha256(attributeValue);
281
+ return { key, attributeValue, hashedValue };
282
+ });
283
+
284
+ const results = await Promise.all(hashPromises);
285
+
286
+ // Build the result object
287
+ const hashedAttributes: IRoktPartnerAttributes = {};
288
+ for (const { key, attributeValue, hashedValue } of results) {
289
+ hashedAttributes[key] = attributeValue;
290
+ if (hashedValue) {
291
+ hashedAttributes[`${key}sha256`] = hashedValue;
292
+ }
293
+ }
294
+ return hashedAttributes;
295
+
262
296
  } catch (error) {
263
- return Promise.reject(error instanceof Error ? error : new Error('Unknown error occurred'));
297
+ const errorMessage = error instanceof Error ? error.message : String(error);
298
+ this.logger.error(`Failed to hashAttributes, returning an empty object: ${errorMessage}`);
299
+ return {};
264
300
  }
265
301
  }
266
302
 
@@ -290,6 +326,29 @@ export default class RoktManager {
290
326
  }
291
327
  }
292
328
 
329
+ /**
330
+ * Hashes an attribute using SHA-256
331
+ *
332
+ * @param {string | number | boolean | undefined | null} attribute - The value to hash
333
+ * @returns {Promise<string | undefined | null>} SHA-256 hashed value or undefined/null
334
+ *
335
+ */
336
+ public async hashSha256(attribute: string | number | boolean | undefined | null): Promise<string | undefined | null> {
337
+ if (attribute === null || attribute === undefined) {
338
+ this.logger.warning(`hashSha256 received null/undefined as input`);
339
+ return attribute as null | undefined;
340
+ }
341
+
342
+ try {
343
+ const normalizedValue = String(attribute).trim().toLocaleLowerCase();
344
+ return await this.sha256Hex(normalizedValue);
345
+ } catch (error) {
346
+ const errorMessage = error instanceof Error ? error.message : String(error);
347
+ this.logger.error(`Failed to hashSha256, returning undefined: ${errorMessage}`);
348
+ return undefined;
349
+ }
350
+ }
351
+
293
352
  public getLocalSessionAttributes(): LocalSessionAttributes {
294
353
  return this.store.getLocalSessionAttributes();
295
354
  }
@@ -400,6 +459,37 @@ export default class RoktManager {
400
459
  this.messageQueue.delete(messageId);
401
460
  }
402
461
 
462
+ /**
463
+ * Hashes a string input using SHA-256 and returns the hex digest
464
+ * Uses the Web Crypto API for secure hashing
465
+ *
466
+ * @param {string} input - The string to hash
467
+ * @returns {Promise<string>} The SHA-256 hash as a hexadecimal string
468
+ */
469
+ private async sha256Hex(input: string): Promise<string> {
470
+ const encoder = new TextEncoder();
471
+ const encodedInput = encoder.encode(input);
472
+ const digest = await crypto.subtle.digest('SHA-256', encodedInput);
473
+ return this.arrayBufferToHex(digest);
474
+ }
475
+
476
+ /**
477
+ * Converts an ArrayBuffer to a hexadecimal string representation
478
+ * Each byte is converted to a 2-character hex string with leading zeros
479
+ *
480
+ * @param {ArrayBuffer} buffer - The buffer to convert
481
+ * @returns {string} The hexadecimal string representation
482
+ */
483
+ private arrayBufferToHex(buffer: ArrayBuffer): string {
484
+ const bytes = new Uint8Array(buffer);
485
+ let hexString = '';
486
+ for (let i = 0; i < bytes.length; i++) {
487
+ const hexByte = bytes[i].toString(16).padStart(2, '0');
488
+ hexString += hexByte;
489
+ }
490
+ return hexString;
491
+ }
492
+
403
493
  /**
404
494
  * Checks if an identity value has changed by comparing current and new values
405
495
  *
@@ -40,6 +40,7 @@ import { SDKECommerceAPI } from './ecommerce.interfaces';
40
40
  import { IErrorLogMessage, IMParticleWebSDKInstance, IntegrationDelays } from './mp-instance';
41
41
  import Constants from './constants';
42
42
  import RoktManager, { IRoktLauncherOptions } from './roktManager';
43
+ import { IConsoleLogger } from './logger';
43
44
 
44
45
  // TODO: Resolve this with version in @mparticle/web-sdk
45
46
  export type SDKEventCustomFlags = Dictionary<any>;
@@ -259,7 +260,13 @@ export interface IMParticleInstanceManager extends MParticleWebSDK {
259
260
  export type BooleanStringLowerCase = 'false' | 'true';
260
261
  export type BooleanStringTitleCase = 'False' | 'True';
261
262
 
262
- export type LogLevelType = 'none' | 'verbose' | 'warning' | 'error';
263
+ export type LogLevelType = (typeof LogLevelType)[keyof typeof LogLevelType];
264
+ export const LogLevelType = {
265
+ None: 'none',
266
+ Verbose: 'verbose',
267
+ Warning: 'warning',
268
+ Error: 'error',
269
+ } as const;
263
270
 
264
271
  // TODO: This should eventually be moved into wherever init logic lives
265
272
  // TODO: Replace/Merge this with MPConfiguration in @types/mparticle__web-sdk
@@ -306,6 +313,7 @@ export interface SDKInitConfig
306
313
  launcherOptions?: IRoktLauncherOptions;
307
314
 
308
315
  rq?: Function[] | any[];
316
+ logger?: IConsoleLogger;
309
317
  }
310
318
 
311
319
  export interface DataPlanConfig {