Skip to content
16 changes: 16 additions & 0 deletions src/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { IMParticleUser, ISDKUserAttributes } from './identity-user-interfaces';
import { AsyncUploader, FetchUploader, XHRUploader } from './uploaders';
import { IMParticleWebSDKInstance } from './mp-instance';
import { appendUserInfo } from './user-utils';
import { hasExplicitIdentifier } from './identity-utils';

export interface IAPIClient {
uploader: BatchUploader | null;
Expand Down Expand Up @@ -123,6 +124,21 @@ export default function APIClient(
'Event was added to eventQueue. eventQueue will be processed once a valid MPID is returned or there is no more integration imposed delay.'
);
mpInstance._Store.eventQueue.push(event);
// When noFunctional is true and no explicit identity was provided, mpid may never come, so send to forwarders now.
if (
mpInstance._CookieConsentManager?.getNoFunctional() &&
!hasExplicitIdentifier(mpInstance._Store) &&
!isEmpty(event) &&
(event.EventName as unknown as number) !== Types.MessageType.AppStateTransition
) {
let eventForForwarders = event;
if (kitBlocker && kitBlocker.kitBlockingEnabled) {
eventForForwarders = kitBlocker.createBlockedEvent(event);
}
if (eventForForwarders) {
mpInstance._Forwarders.sendEventToForwarders(eventForForwarders);
}
}
return;
}

Expand Down
5 changes: 4 additions & 1 deletion src/batchUploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ export class BatchUploader {
// so that we don't have to check it every time
this.offlineStorageEnabled = this.isOfflineStorageAvailable();

if (this.offlineStorageEnabled) {
// When noFunctional is true, prevent events/batches storage
const noFunctional = mpInstance._CookieConsentManager?.getNoFunctional();

if (this.offlineStorageEnabled && !noFunctional) {
this.eventVault = new SessionStorageVault<SDKEvent[]>(
`${mpInstance._Store.storageName}-events`,
{
Expand Down
6 changes: 6 additions & 0 deletions src/cookieSyncManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ export default function CookieSyncManager(
return;
}

// When noFunctional is true, persistence is not saved, so we cannot track cookie sync
// dates. Skip cookie sync to avoid running it on every page load.
if (mpInstance._CookieConsentManager.getNoFunctional()) {
return;
}

const persistence = mpInstance._Persistence.getPersistence();

if (isEmpty(persistence)) {
Expand Down
25 changes: 24 additions & 1 deletion src/identity-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Constants, { ONE_DAY_IN_SECONDS, MILLIS_IN_ONE_SEC } from './constants';
import { Dictionary, parseNumber, isObject, generateHash } from './utils';
import { Dictionary, parseNumber, isObject, generateHash, isEmpty } from './utils';
import { BaseVault } from './vault';
import Types from './types';
import {
Expand All @@ -13,6 +13,7 @@ import {
IIdentityResponse,
IMParticleUser,
} from './identity-user-interfaces';
import { IStore } from './store';

const { Identify, Modify, Login, Logout } = Constants.IdentityMethods;
export const CACHE_HEADER = 'x-mp-max-age' as const;
Expand Down Expand Up @@ -298,3 +299,25 @@ export const hasIdentityRequestChanged = (
JSON.stringify(currentUserIdentities) !== JSON.stringify(newIdentities)
);
};

/**
* Checks if deviceId or other user identifiers (like email) were explicitly provided
* by the partner via config.deviceId or config.identifyRequest.userIdentities.
* When noFunctional is true, then cookies are blocked, so the partner must explicitly
* pass deviceId or other identifiers to prevent new users from being created on each page load.
*
* @param store - The SDK store (provides SDKConfig.deviceId and SDKConfig.identifyRequest.userIdentities)
* @returns true if deviceId or other identifiers were explicitly provided in config, false otherwise
*/
export const hasExplicitIdentifier = (store: IStore | undefined | null): boolean => {
const userIdentities = store?.SDKConfig?.identifyRequest?.userIdentities;
if (
userIdentities &&
isObject(userIdentities) &&
!isEmpty(userIdentities) &&
Object.values(userIdentities).some(Boolean)
) {
return true;
}
return !!store?.SDKConfig?.deviceId;
};
24 changes: 18 additions & 6 deletions src/mp-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ import KitBlocker from './kitBlocking';
import ConfigAPIClient, { IKitConfigs } from './configAPIClient';
import IdentityAPIClient from './identityApiClient';
import { isFunction, parseConfig, valueof, generateDeprecationMessage } from './utils';
import { LocalStorageVault } from './vault';
import { removeExpiredIdentityCacheDates } from './identity-utils';
import { DisabledVault, LocalStorageVault } from './vault';
import { removeExpiredIdentityCacheDates, hasExplicitIdentifier } from './identity-utils';
import IntegrationCapture from './integrationCapture';
import { IPreInit, processReadyQueue } from './pre-init-utils';
import { BaseEvent, MParticleWebSDK, SDKHelpersApi } from './sdkRuntimeModels';
Expand Down Expand Up @@ -1130,11 +1130,16 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
* @param {String or Number} value value for session attribute
*/
this.setSessionAttribute = function(key, value) {
const queued = queueIfNotInitialized(function() {
self.setSessionAttribute(key, value);
}, self);
const skipQueue =
self._CookieConsentManager?.getNoFunctional() &&
!hasExplicitIdentifier(self._Store);

if (queued) return;
if (!skipQueue) {
const queued = queueIfNotInitialized(function() {
self.setSessionAttribute(key, value);
}, self);
if (queued) return;
}

// Logs to cookie
// And logs to in-memory object
Expand Down Expand Up @@ -1573,6 +1578,13 @@ function createKitBlocker(config, mpInstance) {
}

function createIdentityCache(mpInstance) {
// Identity expects mpInstance._Identity.idCache to always exist. DisabledVault
// ensures no identity response data is written to localStorage when noFunctional is true
if (mpInstance._CookieConsentManager?.getNoFunctional()) {
return new DisabledVault(`${mpInstance._Store.storageName}-id-cache`, {
logger: mpInstance.Logger,
});
}
return new LocalStorageVault(`${mpInstance._Store.storageName}-id-cache`, {
logger: mpInstance.Logger,
});
Expand Down
15 changes: 15 additions & 0 deletions src/persistence.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@ export default function _Persistence(mpInstance) {
return;
}

// Block mprtcl-v4 localStorage when noFunctional is true
if (mpInstance._CookieConsentManager?.getNoFunctional()) {
return;
}

var key = mpInstance._Store.storageName,
localStorageData = self.getLocalStorage() || {},
currentUser = mpInstance.Identity.getCurrentUser(),
Expand Down Expand Up @@ -398,6 +403,11 @@ export default function _Persistence(mpInstance) {
// https://go.mparticle.com/work/SQDSDKS-5022
// https://go.mparticle.com/work/SQDSDKS-6021
this.setCookie = function() {
// Block mprtcl-v4 cookies when noFunctional is true
if (mpInstance._CookieConsentManager?.getNoFunctional()) {
return;
}

var mpid,
currentUser = mpInstance.Identity.getCurrentUser();
if (currentUser) {
Expand Down Expand Up @@ -803,6 +813,11 @@ export default function _Persistence(mpInstance) {

// https://go.mparticle.com/work/SQDSDKS-6021
this.savePersistence = function(persistence) {
// Block mprtcl-v4 persistence when noFunctional is true
if (mpInstance._CookieConsentManager?.getNoFunctional()) {
return;
}

var encodedPersistence = self.encodePersistence(
JSON.stringify(persistence)
),
Expand Down
13 changes: 11 additions & 2 deletions src/sessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Types from './types';
import { generateDeprecationMessage } from './utils';
import { IMParticleUser } from './identity-user-interfaces';
import { IMParticleWebSDKInstance } from './mp-instance';
import { hasIdentityRequestChanged } from './identity-utils';
import { hasIdentityRequestChanged, hasExplicitIdentifier } from './identity-utils';

const { Messages } = Constants;

Expand Down Expand Up @@ -45,7 +45,12 @@ export default function SessionManager(
const currentUser = mpInstance.Identity.getCurrentUser();
const sdkIdentityRequest = SDKConfig.identifyRequest;

const shouldSuppressIdentify =
mpInstance._CookieConsentManager?.getNoFunctional() &&
!hasExplicitIdentifier(mpInstance._Store);

if (
!shouldSuppressIdentify &&
hasIdentityRequestChanged(currentUser, sdkIdentityRequest)
) {
mpInstance.Identity.identify(
Expand Down Expand Up @@ -102,7 +107,11 @@ export default function SessionManager(

self.setSessionTimer();

if (!mpInstance._Store.identifyCalled) {
const shouldSuppressIdentify =
mpInstance._CookieConsentManager?.getNoFunctional() &&
!hasExplicitIdentifier(mpInstance._Store);

if (!mpInstance._Store.identifyCalled && !shouldSuppressIdentify) {
mpInstance.Identity.identify(
mpInstance._Store.SDKConfig.identifyRequest,
mpInstance._Store.SDKConfig.identityCallback
Expand Down
21 changes: 21 additions & 0 deletions src/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,25 @@ export class SessionStorageVault<StorableItem> extends BaseVault<StorableItem> {
constructor(storageKey: string, options?: IVaultOptions) {
super(storageKey, window.sessionStorage, options);
}
}

// DisabledVault is used when persistence is disabled by privacy flags.
export class DisabledVault<StorableItem> extends BaseVault<StorableItem> {
constructor(storageKey: string, options?: IVaultOptions) {
super(storageKey, window.localStorage, options);
this.contents = null;
this.storageObject.removeItem(this._storageKey);
}

public store(_item: StorableItem): void {
this.contents = null;
}

public retrieve(): StorableItem | null {
return this.contents;
}

public purge(): void {
this.contents = null;
}
}
19 changes: 19 additions & 0 deletions test/jest/cookieSyncManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [pixelSettings],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {
csd: {}
Expand Down Expand Up @@ -76,6 +77,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [pixelSettingsWithoutPixelUrl],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {
csd: {}
Expand Down Expand Up @@ -105,6 +107,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [pixelSettings],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {
csd: {}
Expand All @@ -126,6 +129,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: true,
pixelConfigurations: [pixelSettings],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {
csd: {}
Expand Down Expand Up @@ -156,6 +160,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [myPixelSettings],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {
csd: {}
Expand Down Expand Up @@ -195,6 +200,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [{...pixelSettings, ...myPixelSettings}],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {
csd: {}
Expand Down Expand Up @@ -229,6 +235,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [pixelSettings],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {}}),
},
Expand Down Expand Up @@ -266,6 +273,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [pixelSettings],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {
csd: { 5: cookieSyncDateInPast }
Expand Down Expand Up @@ -302,6 +310,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [pixelSettings],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {}}),
},
Expand Down Expand Up @@ -337,6 +346,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [pixelSettings],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({}),
},
Expand Down Expand Up @@ -365,6 +375,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [myPixelSettings],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {
csd: {}
Expand Down Expand Up @@ -408,6 +419,7 @@ describe('CookieSyncManager', () => {
isEnabledForUserConsent: jest.fn().mockReturnValue(true),
},
_CookieConsentManager: {
getNoFunctional: jest.fn().mockReturnValue(false),
getNoTargeting: jest.fn().mockReturnValue(true),
},
Identity: {
Expand Down Expand Up @@ -437,6 +449,7 @@ describe('CookieSyncManager', () => {
isEnabledForUserConsent: jest.fn().mockReturnValue(true),
},
_CookieConsentManager: {
getNoFunctional: jest.fn().mockReturnValue(false),
getNoTargeting: jest.fn().mockReturnValue(false),
},
Identity: {
Expand Down Expand Up @@ -496,6 +509,7 @@ describe('CookieSyncManager', () => {
isEnabledForUserConsent: jest.fn().mockReturnValue(true),
},
_CookieConsentManager: {
getNoFunctional: jest.fn().mockReturnValue(false),
getNoTargeting: jest.fn().mockReturnValue(true),
},
Identity: {
Expand Down Expand Up @@ -529,6 +543,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [myPixelSettings], // empty values will make require consent to be true
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {
csd: {}
Expand Down Expand Up @@ -584,6 +599,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [tradeDeskPixelSettings],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {
csd: {}
Expand Down Expand Up @@ -625,6 +641,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [nonTradeDeskPixelSettings],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {
csd: {}
Expand Down Expand Up @@ -673,6 +690,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [tradeDeskPixelSettings, appNexusPixelSettings],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {
csd: {}
Expand Down Expand Up @@ -729,6 +747,7 @@ describe('CookieSyncManager', () => {
webviewBridgeEnabled: false,
pixelConfigurations: [tradeDeskPixelSettings],
},
_CookieConsentManager: { getNoFunctional: jest.fn().mockReturnValue(false) },
_Persistence: {
getPersistence: () => ({testMPID: {
csd: {}
Expand Down
Loading
Loading