Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 22 additions & 17 deletions src/batchUploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Constants from './constants';
import { SDKEvent, SDKEventCustomFlags, SDKLoggerApi } from './sdkRuntimeModels';
import { convertEvents } from './sdkToEventsApiConverter';
import { MessageType, EventType } from './types';
import { getRampNumber, isEmpty } from './utils';
import { getRampNumber, isEmpty, obfuscateData } from './utils';
import { SessionStorageVault, LocalStorageVault } from './vault';
import {
AsyncUploader,
Expand Down Expand Up @@ -76,17 +76,11 @@ export class BatchUploader {

if (this.offlineStorageEnabled) {
this.eventVault = new SessionStorageVault<SDKEvent[]>(
`${mpInstance._Store.storageName}-events`,
{
logger: mpInstance.Logger,
}
`${mpInstance._Store.storageName}-events`
);

this.batchVault = new LocalStorageVault<Batch[]>(
`${mpInstance._Store.storageName}-batches`,
{
logger: mpInstance.Logger,
}
`${mpInstance._Store.storageName}-batches`
);

// Load Events from Session Storage in case we have any in storage
Expand Down Expand Up @@ -261,10 +255,14 @@ export class BatchUploader {

this.eventsQueuedForProcessing.push(event);
if (this.offlineStorageEnabled && this.eventVault) {
this.eventVault.store(this.eventsQueuedForProcessing);
try {
this.eventVault.store(this.eventsQueuedForProcessing);
} catch (error) {
Logger.error('Failed to store events to offline storage. Events will remain in memory queue.');
}
}

Logger.verbose(`Queuing event: ${JSON.stringify(event)}`);
Logger.verbose(`Queuing event: ${JSON.stringify(obfuscateData(event))}`);
Logger.verbose(`Queued event count: ${this.eventsQueuedForProcessing.length}`);

if (this.shouldTriggerImmediateUpload(event.EventDataType)) {
Expand Down Expand Up @@ -377,7 +375,11 @@ export class BatchUploader {

this.eventsQueuedForProcessing = [];
if (this.offlineStorageEnabled && this.eventVault) {
this.eventVault.store([]);
try {
this.eventVault.store([]);
} catch (error) {
this.mpInstance.Logger.error('Failed to clear events from offline storage.');
}
}

let newBatches: Batch[] = [];
Expand Down Expand Up @@ -429,10 +431,13 @@ export class BatchUploader {
// therefore NOT overwrite Offline Storage when beacon returns, so that we can retry
// uploading saved batches at a later time. Batches should only be removed from
// Local Storage once we can confirm they are successfully uploaded.
this.batchVault.store(this.batchesQueuedForProcessing);

// Clear batch queue since everything should be in Offline Storage
this.batchesQueuedForProcessing = [];
try {
this.batchVault.store(this.batchesQueuedForProcessing);
// Clear batch queue since everything should be in Offline Storage
this.batchesQueuedForProcessing = [];
} catch (error) {
this.mpInstance.Logger.error('Failed to store batches to offline storage. Batches will remain in memory queue.');
}
}

if (triggerFuture) {
Expand All @@ -454,7 +459,7 @@ export class BatchUploader {
return null;
}

logger.verbose(`Uploading batches: ${JSON.stringify(uploads)}`);
logger.verbose(`Uploading batches: ${JSON.stringify(obfuscateData(uploads))}`);
logger.verbose(`Batch count: ${uploads.length}`);

for (let i = 0; i < uploads.length; i++) {
Expand Down
6 changes: 5 additions & 1 deletion src/foregroundTimeTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ export default class ForegroundTimeTracker {

public updateTimeInPersistence(): void {
if (this.isTrackerActive) {
this.timerVault.store(Math.round(this.totalTime));
try {
this.timerVault.store(Math.round(this.totalTime));
} catch (error) {
// Silently fail - time tracking persistence is not critical for SDK functionality
}
}
}

Expand Down
12 changes: 10 additions & 2 deletions src/identity-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ export const cacheIdentityRequest = (
status,
expireTimestamp,
};
idCache.store(cache);
try {
idCache.store(cache);
} catch (error) {
// Silently fail - identity caching is an optimization, not critical for functionality
}
};

// We need to ensure that identities are concatenated in a deterministic way, so
Expand Down Expand Up @@ -233,7 +237,11 @@ export const removeExpiredIdentityCacheDates = (
}
}

idCache.store(cache);
try {
idCache.store(cache);
} catch (error) {
// Silently fail - identity caching is an optimization, not critical for functionality
}
};

export const tryCacheIdentity = (
Expand Down
14 changes: 11 additions & 3 deletions src/identityApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
IFetchPayload,
} from './uploaders';
import { CACHE_HEADER } from './identity-utils';
import { parseNumber, valueof } from './utils';
import { obfuscateData, parseNumber, valueof } from './utils';
import {
IAliasCallback,
IAliasRequest,
Expand Down Expand Up @@ -277,8 +277,16 @@ export default function IdentityAPIClient(
}

} else {
message = 'Received Identity Response from server: ';
message += JSON.stringify(identityResponse.responseText);
const responseText = identityResponse.responseText;
const { matched_identities, ...rest } = responseText || {};
const obfuscatedMatchedIdentities = obfuscateData(matched_identities);

Comment on lines +280 to +283
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions obfuscating the known_identities payload, but this change obfuscates matched_identities in the response log. Please confirm the intended field(s) to obfuscate and update either the implementation or the PR description for consistency.

Copilot uses AI. Check for mistakes.
// Obfuscate matched_identities to prevent PII exposure
const responseToLog = matched_identities
? { ...rest, matched_identities: obfuscatedMatchedIdentities }
: responseText;

message = 'Received Identity Response from server: ' + JSON.stringify(responseToLog);
}

break;
Expand Down
6 changes: 2 additions & 4 deletions src/mp-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1154,7 +1154,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
* @method setOptOut
* @param {Boolean} isOptingOut boolean to opt out or not. When set to true, opt out of logging.
*/
this.setOptOut = function(isOptingOut) {
this.setOptOut = function(isOptingOut: boolean) {
const queued = queueIfNotInitialized(function() {
self.setOptOut(isOptingOut);
}, self);
Expand Down Expand Up @@ -1547,9 +1547,7 @@ function createKitBlocker(config, mpInstance) {
}

function createIdentityCache(mpInstance) {
return new LocalStorageVault(`${mpInstance._Store.storageName}-id-cache`, {
logger: mpInstance.Logger,
});
return new LocalStorageVault(`${mpInstance._Store.storageName}-id-cache`);
}

function runPreConfigFetchInitialization(mpInstance, apiKey, config) {
Expand Down
10 changes: 6 additions & 4 deletions src/roktManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
isFunction,
AttributeValue,
isEmpty,
obfuscateData,
} from "./utils";
import { SDKIdentityApi } from "./identity.interfaces";
import { SDKLoggerApi } from "./sdkRuntimeModels";
Expand Down Expand Up @@ -202,7 +203,7 @@ export default class RoktManager {
const { attributes } = options;
const sandboxValue = attributes?.sandbox || null;
const mappedAttributes = this.mapPlacementAttributes(attributes, this.placementAttributesMapping);
this.logger?.verbose(`mParticle.Rokt selectPlacements called with attributes:\n${JSON.stringify(attributes, null, 2)}`);
this.logger?.verbose(`mParticle.Rokt selectPlacements called with attributes:\n${JSON.stringify(obfuscateData(attributes), null, 2)}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was added for our support staff to be able to easily see what a customer initially passed to selectPlacements prior to us enriching the attributes with other MP related items. We may need to remove this one temporarily, or provide another logLevel, like debug, but that will require an additional audit to see what would be better for debug vs verbose


this.currentUser = this.identityService.getCurrentUser();
const currentUserIdentities = this.currentUser?.getUserIdentities()?.userIdentities || {};
Expand Down Expand Up @@ -241,7 +242,7 @@ export default class RoktManager {
newIdentities[this.mappedEmailShaIdentityType] = newHashedEmail;
this.logger.warning(`emailsha256 mismatch detected. Current mParticle hashedEmail differs from hashedEmail passed to selectPlacements call. Proceeding to call identify with hashedEmail from selectPlacements call. Please verify your implementation.`);
}

if (!isEmpty(newIdentities)) {
// Call identify with the new user identities
try {
Expand All @@ -256,7 +257,8 @@ export default class RoktManager {
});
});
} catch (error) {
this.logger.error('Failed to identify user with new email: ' + JSON.stringify(error));
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
this.logger.error('Failed to identify user with updated identities: ' + errorMessage);
}
}

Expand Down Expand Up @@ -472,7 +474,7 @@ export default class RoktManager {
return;
}

this.logger?.verbose(`RoktManager: Processing queued message: ${message.methodName} with payload: ${JSON.stringify(message.payload)}`);
this.logger?.verbose(`RoktManager: Processing queued message: ${message.methodName} with payload: ${JSON.stringify(obfuscateData(message.payload))}`);

// Capture resolve/reject functions before async processing
const resolve = message.resolve;
Expand Down
46 changes: 44 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,12 +426,53 @@ const filterDictionaryWithHash = <T>(
}

const parseConfig = (config: SDKInitConfig, moduleName: string, moduleId: number): IKitConfigs | null => {
return config.kitConfigs?.find((kitConfig: IKitConfigs) =>
kitConfig.name === moduleName &&
return config.kitConfigs?.find((kitConfig: IKitConfigs) =>
kitConfig.name === moduleName &&
kitConfig.moduleId === moduleId
) || null;
}

/**
* Obfuscates an object by replacing all primitive values with their type names,
* while preserving the structure of nested objects and arrays.
* This is useful for logging data structures without exposing PII.
*
* @param value - The value to obfuscate
* @returns The obfuscated value with types instead of actual values
*
* @example
* obfuscateData({ email: 'user@email.com', age: 30 })
* // Returns: { email: 'string', age: 'number' }
*
* obfuscateData({ tags: ['premium', 'verified'] })
* // Returns: { tags: ['string', 'string'] }
*/
const obfuscateData = (value: any): any => {
// Preserve null and undefined
if (value === null || value === undefined) {
return value;
}

// Handle arrays - recursively obfuscate each element
if (Array.isArray(value)) {
return value.map(item => obfuscateData(item));
}

// Handle objects - recursively obfuscate each property
if (isObject(value)) {
const obfuscated: Dictionary<any> = {};
for (const key in value) {
if (value.hasOwnProperty(key)) {
obfuscated[key] = obfuscateData(value[key]);
}
}
return obfuscated;
}

// For primitives and other types, return the type as a string
return typeof value;
};

export {
createCookieString,
revertCookieString,
Expand All @@ -449,6 +490,7 @@ export {
inArray,
isObject,
isStringOrNumber,
obfuscateData,
parseConfig,
parseNumber,
parseSettingsString,
Expand Down
38 changes: 7 additions & 31 deletions src/vault.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,22 @@
import { Logger } from '@mparticle/web-sdk';
import { isEmpty, isNumber } from './utils';

export interface IVaultOptions {
logger?: Logger;
offlineStorageEnabled?: boolean;
}

export abstract class BaseVault<StorableItem> {
public contents: StorableItem;
protected readonly _storageKey: string;
protected logger?: Logger;
protected storageObject: Storage;

/**
*
* @param {string} storageKey the local storage key string
* @param {Storage} Web API Storage object that is being used
* @param {IVaultOptions} options A Dictionary of IVaultOptions
* @param {Storage} storageObject Web API Storage object that is being used
*/
constructor(
storageKey: string,
storageObject: Storage,
options?: IVaultOptions
storageObject: Storage
) {
this._storageKey = storageKey;
this.storageObject = storageObject;

// Add a fake logger in case one is not provided or needed
this.logger = options?.logger || {
verbose: () => {},
warning: () => {},
error: () => {},
};

this.contents = this.retrieve();
}
Comment on lines 17 to 21
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

options is still accepted by BaseVault/LocalStorageVault/SessionStorageVault, but it’s no longer used anywhere in the constructor. If the repo’s TS/GTS rules enforce unused-parameter checks, this may fail lint/typecheck. Consider removing the parameter/interface or renaming to _options until it’s needed.

Copilot uses AI. Check for mistakes.

Expand All @@ -53,13 +37,8 @@ export abstract class BaseVault<StorableItem> {

try {
this.storageObject.setItem(this._storageKey, stringifiedItem);

this.logger.verbose(`Saving item to Storage: ${stringifiedItem}`);
} catch (error) {
this.logger.error(
`Cannot Save items to Storage: ${stringifiedItem}`
);
this.logger.error(error as string);
throw new Error('Cannot Save items to Storage');
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseVault.store now throws on setItem failures. Callers (e.g., event/batch persistence) don’t catch this, so quota/security errors will bubble up and can break normal SDK flows (queueing events, uploading, etc.). Previously this was non-fatal; consider restoring a non-throwing behavior (or returning a success flag) and logging a sanitized message (avoid including the stored payload).

Suggested change
throw new Error('Cannot Save items to Storage');
// Swallow storage errors to avoid breaking normal SDK flows.
// Log a sanitized message without including the stored payload.
try {
const errorMessage =
error && typeof (error as Error).message === 'string'
? (error as Error).message
: String(error);
if (typeof console !== 'undefined' && typeof console.error === 'function') {
console.error(
`BaseVault.store: failed to persist data for key "${this._storageKey}".`,
errorMessage
);
}
} catch {
// If logging itself fails, do nothing to maintain non-throwing behavior.
}

Copilot uses AI. Check for mistakes.
}
}

Expand All @@ -75,8 +54,6 @@ export abstract class BaseVault<StorableItem> {

this.contents = item ? JSON.parse(item) : null;

this.logger.verbose(`Retrieving item from Storage: ${item}`);

return this.contents;
}

Expand All @@ -86,20 +63,19 @@ export abstract class BaseVault<StorableItem> {
* @method purge
*/
public purge(): void {
this.logger.verbose('Purging Storage');
this.contents = null;
this.storageObject.removeItem(this._storageKey);
}
}

export class LocalStorageVault<StorableItem> extends BaseVault<StorableItem> {
constructor(storageKey: string, options?: IVaultOptions) {
super(storageKey, window.localStorage, options);
constructor(storageKey: string) {
super(storageKey, window.localStorage);
}
}

export class SessionStorageVault<StorableItem> extends BaseVault<StorableItem> {
constructor(storageKey: string, options?: IVaultOptions) {
super(storageKey, window.sessionStorage, options);
constructor(storageKey: string) {
super(storageKey, window.sessionStorage);
}
}
2 changes: 1 addition & 1 deletion test/jest/roktManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2389,7 +2389,7 @@ describe('RoktManager', () => {
await roktManager.selectPlacements(options);

expect(mockMPInstance.Logger.verbose).toHaveBeenCalledWith(
`mParticle.Rokt selectPlacements called with attributes:\n${JSON.stringify({ email: 'test@example.com', customAttr: 'value' }, null, 2)}`
`mParticle.Rokt selectPlacements called with attributes:\n${JSON.stringify({ email: 'string', customAttr: 'string' }, null, 2)}`
);
});
});
Expand Down
Loading
Loading