Skip to content

feat: Block mp cookies when noFunctional is true#1167

Open
khushi1033 wants to merge 12 commits intodevelopmentfrom
feat/SDKE-972-block-cookies-based-on-rokt-privacy-flags
Open

feat: Block mp cookies when noFunctional is true#1167
khushi1033 wants to merge 12 commits intodevelopmentfrom
feat/SDKE-972-block-cookies-based-on-rokt-privacy-flags

Conversation

@khushi1033
Copy link
Contributor

@khushi1033 khushi1033 commented Feb 24, 2026

Background

Rokt supports disabling cookies via client-side flags (noFunctional, noTargeting). With the introduction of the joint Rokt/mParticle SDK, mP cookies are treated as strictly necessary — meaning they could not previously be disabled for Rokt-only clients. This created friction for clients who want a clear opt-out path for users or interpret mP cookies as non-essential when using the SDK solely for Rokt use cases (e.g., post-checkout ads).

However, simply blocking all mP cookies introduces a risk: without the mprtcl-v4-{workspace_token} cookie, every page load looks like a new user, causing identify to be called continuously and flooding the system with new profile creation.

What Has Changed

  • Blocked all mP cookies/persistence when noFunctional: true: localStorage, cookies, and session storage writes are no-ops in persistence.js, batchUploader.ts, and mp-instance.ts.
  • Created a no-op identity cache vault when noFunctional: true to prevent mprtcl-v4-id-cache from being written to localStorage.
  • Suppressed automatic identify calls when noFunctional: true AND no explicit identifiers (device ID or user identities like email) are provided by the partner — preventing runaway
    profile creation.
  • Added hasExplicitIdentifier utility in identity-utils.ts to check whether a partner has passed a device ID or user identities via config.
  • Prevented auto-population of identifyRequest from persistence when noFunctional: true during SDK initialization.
  • Disables cookie sync when noFunctional = true, even if noTargeting = false

Screenshots/Video

  • N/A

Checklist

  • I have performed a self-review of my own code.
  • I have made corresponding changes to the documentation.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have tested this locally.

Additional Notes

  • Tested locally in browser using a mock page to turn on/off noFunctional flag and checking that identify calls, cookie/local storage, and kit initialization behaves as expected

Reference Issue (For employees only. Ignore if you are an outside contributor)

@khushi1033 khushi1033 marked this pull request as ready for review February 25, 2026 14:53
Copy link
Collaborator

@alexs-mparticle alexs-mparticle left a comment

Choose a reason for hiding this comment

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

@khushi1033 Great PR and I'm happy to see this coming together. I have a few comments but this looks pretty solid.

data['newMPID'].csd.should.have.property(roktModuleId);
});

it('should not block Rokt cookie sync when only noFunctional is true', async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why was this removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With noFunctional blocking cookies now, this test fails since theres no mPId being saved. Planning to add a new test once we have confirmation of how we want cookie sync to behave under noFunctional

Comment on lines +1579 to +1598
// When noFunctional is true, we must still return a vault-shaped object because
// Identity expects mpInstance._Identity.idCache to always exist. A no-op vault
// ensures no identity response data is written to localStorage
if (mpInstance._CookieConsentManager?.getNoFunctional()) {
const vault = new LocalStorageVault(`${mpInstance._Store.storageName}-id-cache`, {
logger: mpInstance.Logger,
});
vault.store = function() {
this.contents = arguments[0];
mpInstance.Logger.verbose(`Storage disabled: not storing id-cache when noFunctional is true`);
};
vault.retrieve = function() {
this.contents = null;
return null;
};
vault.purge = function() {
this.contents = null;
};
return vault;
}
Copy link
Member

Choose a reason for hiding this comment

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

I asked AI to instead of creating a no-op, to make idCache optional. Here are the pros/cons of optional idCache:

1. Explicit “no cache” instead of a fake object
optional idCache: The type is BaseVault<...> | null. “We are not caching” is visible in the type and in control flow.
No-op: You have an object that looks like a cache but isn’t. “No cache” is hidden behind an implementation that only exists to satisfy “something is always there.”
2. One concept instead of two
optional idCache: There’s a single idea: either we have a real cache or we don’t.
No-op: You maintain two concepts — “real cache” and “fake cache that implements the interface.” The no-op is there only to avoid null checks, which pushes the “no functional” concern into a dummy implementation.
3. Less code and no dummy behavior
optional idCache: No extra type, no store/retrieve/purge stubs, no allocation when noFunctional is true.
No-op: You have to implement and maintain those no-op methods and keep them in sync with the real vault behavior (e.g. logging) over time.
4. Honest API
optional idCache: The API says “identity cache may be absent.” Callers (or shared utils) handle that once at the boundary.
No-op: The API says “you always have a cache,” but one of the implementations is a lie. That can be surprising for anyone reading or debugging.

Copy link
Member

Choose a reason for hiding this comment

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

I'd prefer the optional no cache

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to use DisabledVault instead similar to the approach in this PR

expect(hasExplicitIdentifier(store as IStore)).toBe(false);
});

it('should return false when userIdentities object is empty', () => {
Copy link
Member

Choose a reason for hiding this comment

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

can you add one more test for if useIdentities object has an invalid user identity, like userIdentities = {email: ''} and also userIdentities = {email: null}?

Copy link
Member

@rmi22186 rmi22186 left a comment

Choose a reason for hiding this comment

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

Can you add tests to the forwarders test file that shows an integration test when we add noFunctional = true, and shows that forwarders are still being initialized, and the forwarder still receives an event even if MP SDK does not initialize?

});

describe('when MP SDK is not fully initialized', () => {
it('should allow Rokt Kit to initialize and selectPlacements', async () => {
Copy link
Member

Choose a reason for hiding this comment

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

What about this tests is showing that the MP SDK is not fully initialized? It seems to just be setting up the rokt manager manually.

Similar to the forwarders test, it'd be helpful to have a test (probably there), specifically for seeing if rokt is fully initialized even if the MP SDK isn't.

Something like:
call mParticle init with noFunctional = true
assert that the MP SDK is not initialized
assert that no identity calls were made
call mParticle.rokt.selectPlacements and assert that the kit called selectPlacements internally

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep you're right, I updated the test to better represent the full flow.

Copy link
Collaborator

@alexs-mparticle alexs-mparticle left a comment

Choose a reason for hiding this comment

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

LGTM. Once you resolve comments from @rmi22186, I think we can ship.

delete window.mParticle.config.identifyRequest;
});

it('should still initialize forwarders when noFunctional is true (even when SDK does not complete initialization)', () => {
Copy link
Member

Choose a reason for hiding this comment

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

Can we add a test that says the event is still sent to the forwarder here also?

It should be work both in this case when there is no identity passed, as well as the bottom case where an identity is passed,

Copy link
Contributor Author

@khushi1033 khushi1033 Mar 2, 2026

Choose a reason for hiding this comment

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

Done, added verification that events are still sent even without identity passed in

src/apiClient.ts Outdated
);
mpInstance._Store.eventQueue.push(event);
// When noFunctional is true, mpid may never come (no identity), so still send to forwarders so kits receive the event.
if (mpInstance._CookieConsentManager?.getNoFunctional() && !isEmpty(event) && (event.EventName as unknown as number) !== Types.MessageType.AppStateTransition) {
Copy link
Member

Choose a reason for hiding this comment

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

It's actually not just events. You may want to ask claude or cursor to do an audit of where we call anything on the forwarder inside of the forwarders.js module, as an example forwarder.setUserAttribute. These will needs tests too.

Copy link
Member

Choose a reason for hiding this comment

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

Is it ever possible for an event to be sent twice to a forwarder in this case?

If a partner passes an identity, and a customer is on a slow connection, it's possible an MPID hasn't come back yet. THe event gets queued and also immediately forwarded to the forwarder.

Then the MPID comes back, the queue gets processed, and in that code block, would it also then get sent to the server? Perhaps doing a check like you did elsewhere for hasExplicitIdentifier may help here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it, yeah I added a check for hasExplicitIdentifier to ensure the events aren't sent twice.

Also added tests for the following behavior under noFunctional = true:

  • forwarder.setSessionAttribute still works whether an identity is provided or isn't.
  • forwarder.setUserAttribute and forwarder.removeUserAttribute only work when an identity is provided, and are no-ops when it isn't since getCurrentUser is null
  • forwarder.onUserIdentified and forwarder.onIdentifyComplete are only called when an identity is provided

});

expect(identifySpy).toHaveBeenCalledWith({
userIdentities: { email: 'user@example.com' },
Copy link
Member

Choose a reason for hiding this comment

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

One of the requirements is that once identify is called, mparticle should be fully initialized. I'd add a test for this. Also, have the SDK queue a few events before calling identify, confirm the queue is a certain length (if that is possible), then call identify, wait for it to return, and confirm logevent was called with the queued events.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added testing to tests-forwarders.ts to make sure mparticle is being intialized after identify is called and added a test to check logEvent is being called on queued events.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 3, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
19.5% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants