Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e74dedb
block mp cookies when nofunctional is true
khushi1033 Feb 24, 2026
add2e74
clean up comments
khushi1033 Feb 25, 2026
6e5f562
addressed pr comments
khushi1033 Feb 25, 2026
9f9d3c0
Update src/identity-utils.ts
khushi1033 Feb 25, 2026
892cc5a
use disabled vault
khushi1033 Feb 25, 2026
c73bb71
disable cookie sync when nofunctional is true
khushi1033 Feb 26, 2026
e67fb76
changes from comments
khushi1033 Mar 2, 2026
adb5938
added additional tests
khushi1033 Mar 2, 2026
d8564ce
verify events still sent without identity when noFuntional
khushi1033 Mar 2, 2026
0086eb2
testing of all forwarder methods
khushi1033 Mar 3, 2026
17796db
tests for when no identity provided
khushi1033 Mar 3, 2026
431937b
test mparticle intialized and logevent
khushi1033 Mar 3, 2026
fd9ab0f
chore: add codeowners (#1193)
nickolas-dimitrakas Mar 6, 2026
5bcbecb
Merge branch 'master' into feat/SDKE-972-block-cookies-based-on-rokt-…
khushi1033 Mar 6, 2026
a2fbf2c
added flag so events not sent to forwarders twice
khushi1033 Mar 9, 2026
e41b195
clean up whitespaces
khushi1033 Mar 9, 2026
560b2c5
Merge branch 'development' into feat/SDKE-972-block-cookies-based-on-…
khushi1033 Mar 9, 2026
9359ac0
add additional tests for event forwarding behavior
khushi1033 Mar 9, 2026
7373c3a
Merge branch 'feat/SDKE-972-block-cookies-based-on-rokt-privacy-flags…
khushi1033 Mar 9, 2026
3f9bfd8
make seperate function for event sending under noFunctional
khushi1033 Mar 9, 2026
3ce0e1d
cleanup based on code anlysis suggestions
khushi1033 Mar 9, 2026
0d7ad3c
Apply suggestions from code review
khushi1033 Mar 10, 2026
6c930f8
remove isSystemEvent check
khushi1033 Mar 10, 2026
f9382d1
added comments to test for context on events queued
khushi1033 Mar 10, 2026
db5b930
Apply suggestion from @rmi22186
khushi1033 Mar 10, 2026
fd99966
Update src/apiClient.ts
khushi1033 Mar 10, 2026
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @mParticle/sdk-team
46 changes: 42 additions & 4 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 @@ -88,6 +89,35 @@ export default function APIClient(
});
};

// When noFunctional is set and there are no identities passed, the SDK will not fully initialize.
// In this case, there will be no MPID, but we still want kits to initialize and forward the event to kits.
// The original event is queued for the MP server upload path so it can be sent once an MPID is returned.
// Returns true if the event was handled by this path (caller should return early).
const handleNoFunctionalPreMpidEvent = (event: SDKEvent, mpid: string | undefined): boolean => {
const noFunctionalWithoutId =
mpInstance._CookieConsentManager?.getNoFunctional() &&
!hasExplicitIdentifier(mpInstance._Store);

if (!noFunctionalWithoutId || mpid || !mpInstance._Store.configurationLoaded || mpInstance._Store.requireDelay) {
return false;
}

let forwarderEvent = event;
if (kitBlocker?.kitBlockingEnabled) {
forwarderEvent = kitBlocker.createBlockedEvent(event);
}
if (forwarderEvent) {
mpInstance._Forwarders.sendEventToForwarders(forwarderEvent);
event._forwardersAlreadySent = true;
}
mpInstance.Logger.verbose(
'noFunctional event forwarded to kits and queued for MP server upload when MPID is available.'
);
mpInstance._Store.eventQueue.push(event);

return true;
};

this.sendEventToServer = function(event, _options) {
const defaultOptions = {
shouldUploadEvent: true,
Expand All @@ -112,8 +142,14 @@ export default function APIClient(
mpInstance._Store.integrationDelayTimeoutStart,
Date.now()
);
// 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
// need to be set, or if we are still fetching the config (self hosted only), and so require delaying events

if (handleNoFunctionalPreMpidEvent(event, mpid)) {
return;
}

// We queue events if there is no MPID (MPID is null, or === 0), or there are integrations that
// require this to stall because integration attributes need to be set, or if we are still
// fetching the config (self hosted only), and so require delaying events
if (
!mpid ||
mpInstance._Store.requireDelay ||
Expand Down Expand Up @@ -146,8 +182,10 @@ export default function APIClient(
}

// We need to check event again, because kitblocking
// can nullify the event
if (event) {
// can nullify the event.
// Skip if forwarders were already called in the noFunctional pre-MPID path
// to prevent double-sending when the event queue is later flushed.
if (event && !event._forwardersAlreadySent) {
mpInstance._Forwarders.sendEventToForwarders(event);
}
}
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
8 changes: 5 additions & 3 deletions src/foregroundTimeTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
public startTime: number = 0;
public totalTime: number = 0;

constructor(timerKey: string) {
constructor(timerKey: string, private noFunctional: boolean = false) {

Check warning on line 11 in src/foregroundTimeTracker.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'noFunctional: boolean' is never reassigned; mark it as `readonly`.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-web-sdk&issues=AZzSvoyyM_7fIvlLNbuL&open=AZzSvoyyM_7fIvlLNbuL&pullRequest=1167
this.localStorageName = `mprtcl-tos-${timerKey}`;
this.timerVault = new LocalStorageVault<number>(this.localStorageName);
this.loadTimeFromStorage();
if (!this.noFunctional) {
this.loadTimeFromStorage();
}
this.addHandlers();
if (document.hidden === false) {
this.startTracking();
Expand Down Expand Up @@ -63,7 +65,7 @@
}

public updateTimeInPersistence(): void {
if (this.isTrackerActive) {
if (this.isTrackerActive && !this.noFunctional) {
this.timerVault.store(Math.round(this.totalTime));
}
}
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;
};
33 changes: 27 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 @@ -1135,11 +1135,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 @@ -1584,6 +1589,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 Expand Up @@ -1666,6 +1678,15 @@ function processIdentityCallback(
function queueIfNotInitialized(func, self) {
// Core SDK methods must wait for Store initialization
if (!self._Store?.isInitialized) {
// When noFunctional is true with no explicit identifier, the SDK will never
// receive an MPID. Let these calls through so events can still reach forwarders immediately.
// sendEventToServer handles queuing for the MP server upload path separately.
const noFunctionalWithoutId =
self._CookieConsentManager?.getNoFunctional() &&
!hasExplicitIdentifier(self._Store);
if (noFunctionalWithoutId) {
return false;
}
self._preInit.readyQueue.push(function() {
if (self._Store?.isInitialized) {
func();
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
1 change: 1 addition & 0 deletions src/sdkRuntimeModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface SDKEvent {
ExpandedEventCount: number;
ActiveTimeOnSite: number;
IsBackgroundAST?: boolean;
_forwardersAlreadySent?: boolean;
}

export interface SDKGeoLocation {
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
3 changes: 2 additions & 1 deletion src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,8 @@ export default function Store(

if (workspaceToken) {
this.SDKConfig.workspaceToken = workspaceToken;
mpInstance._timeOnSiteTimer = new ForegroundTimer(workspaceToken);
const noFunctional = config?.launcherOptions?.noFunctional === true;
mpInstance._timeOnSiteTimer = new ForegroundTimer(workspaceToken, noFunctional);
} else {
mpInstance.Logger.warning(
'You should have a workspaceToken on your config object for security purposes.'
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 @@
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);

Check warning on line 110 in src/vault.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=mParticle_mparticle-web-sdk&issues=AZyXASmLc0UegRwBg17U&open=AZyXASmLc0UegRwBg17U&pullRequest=1167
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;
}
}
Loading
Loading