From 466431fbd2a6312809b4417d814d99a29ece9ae9 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Mon, 26 Jan 2026 14:11:13 -0500 Subject: [PATCH] Initial POC of Identity Failed implementation --- src/identityApiClient.ts | 6 +++ src/mp-instance.ts | 41 +++++++++++++-- src/pre-init-utils.ts | 1 + src/roktManager.ts | 18 ++++++- src/store.ts | 2 + test/jest/roktManager.spec.ts | 82 +++++++++++++++++++++++++++-- test/src/tests-identityApiClient.ts | 23 +++++++- 7 files changed, 163 insertions(+), 10 deletions(-) diff --git a/src/identityApiClient.ts b/src/identityApiClient.ts index 2b36a88de..37d3bcd0f 100644 --- a/src/identityApiClient.ts +++ b/src/identityApiClient.ts @@ -288,6 +288,7 @@ export default function IdentityAPIClient( } mpInstance._Store.identityCallInFlight = false; + mpInstance._Store.identityCallFailed = false; verbose(message); parseIdentityResponse( @@ -301,9 +302,14 @@ export default function IdentityAPIClient( ); } catch (err) { mpInstance._Store.identityCallInFlight = false; + + // Marking identity call failed - ready() will fire callbacks if + // RoktManager is ready, allowing Rokt methods to execute even if identity fails + mpInstance._Store.identityCallFailed = true; const errorMessage = (err as Error).message || err.toString(); + debugger; error('Error sending identity request to servers' + ' - ' + errorMessage); invokeCallback( callback, diff --git a/src/mp-instance.ts b/src/mp-instance.ts index 661e4a634..9a0d7e1ee 100644 --- a/src/mp-instance.ts +++ b/src/mp-instance.ts @@ -130,7 +130,9 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan integrationDelays: {}, forwarderConstructors: [], }; - this._RoktManager = new RoktManager(); + + // TODO: Refactor into a preinit class or ready queue manager + this._RoktManager = new RoktManager(this._preInit); // required for forwarders once they reference the mparticle instance this.IdentityType = IdentityType; @@ -150,12 +152,17 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan if (typeof window !== 'undefined') { if (window.mParticle && window.mParticle.config) { + // TODO: Set up test cases for this as well if (window.mParticle.config.hasOwnProperty('rq')) { + console.warn('ready queue from config', window.mParticle.config.rq); + console.warn('typeof ready queue from config', typeof window.mParticle.config.rq); + // debugger; this._preInit.readyQueue = window.mParticle.config.rq; } } } this.init = function(apiKey, config) { + console.warn('local dev'); if (!config) { console.warn( 'You did not pass a config object to init(). mParticle will not initialize properly' @@ -251,7 +258,18 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan * @param {Function} function A function to be called after mParticle is initialized */ this.ready = function(f) { - if (self.isInitialized() && typeof f === 'function') { + // debugger; + console.warn('ready', f); + console.warn('typeof f', typeof f); + console.warn('isInitialized', self.isInitialized()); + console.warn('identityCallFailed', self._Store?.identityCallFailed); + const shouldExecute = typeof f === 'function' && ( + self._Store?.isInitialized || + (self._Store?.identityCallFailed && self._RoktManager.isReady()) + ); + + console.warn('shouldExecute', shouldExecute); + if (shouldExecute) { f(); } else { self._preInit.readyQueue.push(f); @@ -1460,10 +1478,15 @@ function completeSDKInitialization(apiKey, config, mpInstance) { // We will continue to clear out the ready queue as part of the initial init flow // if an identify request is unnecessary, such as if there is an existing session + console.warn('Checking if store can be initialized', mpInstance._Store.mpid, mpInstance._Store.identifyCalled); + + console.warn('ready queue on init complete', mpInstance._preInit.readyQueue); + if ( (mpInstance._Store.mpid && !mpInstance._Store.identifyCalled) || mpInstance._Store.webviewBridgeEnabled ) { + // TODO: We may need to extract this out of the identified check above mpInstance._Store.isInitialized = true; mpInstance._preInit.readyQueue = processReadyQueue( @@ -1612,9 +1635,17 @@ function processIdentityCallback( } function queueIfNotInitialized(func, self) { - if (!self.isInitialized()) { - self.ready(function() { - func(); + // Core SDK methods must wait for Store initialization + // We queue directly to readyQueue (not via ready()) because: + // - ready() fires when either Store OR RoktManager is ready + // - Core SDK methods specifically need Store to be initialized + if (!self._Store?.isInitialized) { + self._preInit.readyQueue.push(function() { + // Double-check Store before executing - prevents execution + // when only RoktManager is ready + if (self._Store?.isInitialized) { + func(); + } }); return true; } diff --git a/src/pre-init-utils.ts b/src/pre-init-utils.ts index c734457e1..cb182b776 100644 --- a/src/pre-init-utils.ts +++ b/src/pre-init-utils.ts @@ -17,6 +17,7 @@ export const processReadyQueue = (readyQueue): Function[] => { if (isFunction(readyQueueItem)) { readyQueueItem(); } else if (Array.isArray(readyQueueItem)) { + // TODO:L Can we add test coverage for this? processPreloadedItem(readyQueueItem); } }); diff --git a/src/roktManager.ts b/src/roktManager.ts index c04aefa56..00d197f18 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -15,6 +15,7 @@ import { SDKLoggerApi } from "./sdkRuntimeModels"; import { IStore, LocalSessionAttributes } from "./store"; import { UserIdentities } from "@mparticle/web-sdk"; import { IdentityType } from "./types"; +import { IPreInit, processReadyQueue } from "./pre-init-utils"; // https://docs.rokt.com/developers/integration-guides/web/library/attributes export interface IRoktPartnerAttributes { @@ -100,6 +101,12 @@ export default class RoktManager { private logger: SDKLoggerApi; private domain?: string; private mappedEmailShaIdentityType?: string | null; + + // TODO: Refactor into a preinit class or ready queue manager + constructor(private _preInit: IPreInit) { + this._preInit = _preInit; + } + /** * Initializes the RoktManager with configuration settings and user data. * @@ -155,8 +162,15 @@ export default class RoktManager { } public attachKit(kit: IRoktKit): void { + console.warn('RoktManager: attaching kit'); this.kit = kit; this.processMessageQueue(); + + // Process ready queue only if identity call failed + // This ensures queued SDK methods execute even when identity initialization fails + if (this.store.identityCallFailed) { + this._preInit.readyQueue = processReadyQueue(this._preInit.readyQueue); + } } /** @@ -180,6 +194,7 @@ export default class RoktManager { } try { + console.warn('RoktManager: selectPlacements'); const { attributes } = options; const sandboxValue = attributes?.sandbox || null; const mappedAttributes = this.mapPlacementAttributes(attributes, this.placementAttributesMapping); @@ -357,7 +372,7 @@ export default class RoktManager { this.store.setLocalSessionAttribute(key, value); } - private isReady(): boolean { + public isReady(): boolean { // The Rokt Manager is ready when a kit is attached and has a launcher return Boolean(this.kit && this.kit.launcher); } @@ -430,6 +445,7 @@ export default class RoktManager { } private deferredCall(methodName: string, payload: any): Promise { + console.warn('deferredCall', methodName, payload); return new Promise((resolve, reject) => { const messageId = `${methodName}_${generateUniqueId()}`; diff --git a/src/store.ts b/src/store.ts index 329f8e24d..7db26dcd4 100644 --- a/src/store.ts +++ b/src/store.ts @@ -182,6 +182,7 @@ export interface IStore { context: Context | null; configurationLoaded: boolean; identityCallInFlight: boolean; + identityCallFailed: boolean; SDKConfig: Partial; nonCurrentUserMPIDs: Record; identifyCalled: boolean; @@ -266,6 +267,7 @@ export default function Store( context: null, configurationLoaded: false, identityCallInFlight: false, + identityCallFailed: false, SDKConfig: {}, nonCurrentUserMPIDs: {}, identifyCalled: false, diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts index 00e74c792..5069eb2b6 100644 --- a/test/jest/roktManager.spec.ts +++ b/test/jest/roktManager.spec.ts @@ -2,6 +2,7 @@ import { IKitConfigs } from "../../src/configAPIClient"; import { IMParticleUser } from "../../src/identity-user-interfaces"; import { SDKIdentityApi } from "../../src/identity.interfaces"; import { IMParticleWebSDKInstance } from "../../src/mp-instance"; +import { IPreInit } from "../../src/pre-init-utils"; import RoktManager, { IRoktKit, IRoktSelectPlacementsOptions } from "../../src/roktManager"; import { testMPID } from '../src/config/constants'; @@ -10,6 +11,7 @@ const resolvePromise = () => new Promise(resolve => setTimeout(resolve, 0)); describe('RoktManager', () => { let roktManager: RoktManager; let currentUser: IMParticleUser; + let preInit: IPreInit; const mockMPInstance = ({ Identity: { @@ -42,7 +44,12 @@ describe('RoktManager', () => { } as unknown) as IMParticleWebSDKInstance; beforeEach(() => { - roktManager = new RoktManager(); + preInit = { + readyQueue: [], + integrationDelays: {}, + forwarderConstructors: [], + }; + roktManager = new RoktManager(preInit); currentUser = { setUserAttributes: jest.fn(), getUserIdentities: jest.fn().mockReturnValue({ @@ -440,8 +447,10 @@ describe('RoktManager', () => { }); describe('#attachKit', () => { - it('should attach a kit', () => { - const kit: IRoktKit = { + let kit: IRoktKit; + + beforeEach(() => { + kit = { launcher: { selectPlacements: jest.fn(), hashAttributes: jest.fn(), @@ -455,10 +464,77 @@ describe('RoktManager', () => { setExtensionData: jest.fn(), use: jest.fn(), }; + }); + it('should attach a kit', () => { roktManager.attachKit(kit); expect(roktManager['kit']).not.toBeNull(); }); + + it('should call processMessageQueue when kit is attached', () => { + const processMessageQueueSpy = jest.spyOn(roktManager as any, 'processMessageQueue'); + + roktManager.attachKit(kit); + + expect(processMessageQueueSpy).toHaveBeenCalledTimes(1); + processMessageQueueSpy.mockRestore(); + }); + + it('should process ready queue when kit is attached and identity call failed', () => { + const readyQueueItem1 = jest.fn(); + const readyQueueItem2 = jest.fn(); + preInit.readyQueue = [readyQueueItem1, readyQueueItem2]; + + // Set identityCallFailed to true + roktManager['store'].identityCallFailed = true; + + roktManager.attachKit(kit); + + expect(readyQueueItem1).toHaveBeenCalledTimes(1); + expect(readyQueueItem2).toHaveBeenCalledTimes(1); + expect(preInit.readyQueue).toEqual([]); + }); + + it('should NOT process ready queue when identity call did not fail', () => { + const readyQueueItem1 = jest.fn(); + const readyQueueItem2 = jest.fn(); + preInit.readyQueue = [readyQueueItem1, readyQueueItem2]; + + // Set identityCallFailed to false + roktManager['store'].identityCallFailed = false; + + roktManager.attachKit(kit); + + // Ready queue items should NOT be called + expect(readyQueueItem1).not.toHaveBeenCalled(); + expect(readyQueueItem2).not.toHaveBeenCalled(); + // Ready queue should remain unchanged + expect(preInit.readyQueue).toEqual([readyQueueItem1, readyQueueItem2]); + }); + + it('should process message queue before processing ready queue when identity call failed', () => { + const processMessageQueueSpy = jest.spyOn(roktManager as any, 'processMessageQueue'); + const processReadyQueueSpy = jest.spyOn(require('../../src/pre-init-utils'), 'processReadyQueue'); + + // Set identityCallFailed to true so ready queue is processed + roktManager['store'].identityCallFailed = true; + + roktManager.attachKit(kit); + + // Verify both were called + expect(processMessageQueueSpy).toHaveBeenCalled(); + expect(processReadyQueueSpy).toHaveBeenCalled(); + + // Verify processMessageQueue was called before processReadyQueue + // by checking the order of calls + const messageQueueCallIndex = processMessageQueueSpy.mock.invocationCallOrder[0]; + const readyQueueCallIndex = processReadyQueueSpy.mock.invocationCallOrder[0]; + + expect(messageQueueCallIndex).toBeLessThan(readyQueueCallIndex); + + processMessageQueueSpy.mockRestore(); + processReadyQueueSpy.mockRestore(); + }); }); describe('#processMessageQueue', () => { diff --git a/test/src/tests-identityApiClient.ts b/test/src/tests-identityApiClient.ts index 64e4e886c..1e71eeda9 100644 --- a/test/src/tests-identityApiClient.ts +++ b/test/src/tests-identityApiClient.ts @@ -93,6 +93,7 @@ describe('Identity Api Client', () => { SDKConfig: { identityUrl: '', }, + identityCallFailed: false, }, _Persistence: {}, } as unknown) as IMParticleWebSDKInstance; @@ -121,6 +122,8 @@ describe('Identity Api Client', () => { expect(parseIdentityResponseSpy.args[0][4]).to.equal('identify'); expect(parseIdentityResponseSpy.args[0][5]).to.deep.equal(identityRequest.known_identities); expect(parseIdentityResponseSpy.args[0][6]).to.equal(false); + // Verify identityCallFailed is set to false when identity call succeeds + expect(mpInstance._Store.identityCallFailed, 'identityCallFailed should be false').to.eq(false); }); it('should return early without calling parseIdentityResponse if the identity call is in flight', async () => { @@ -202,6 +205,7 @@ describe('Identity Api Client', () => { identityUrl: '', }, identityCallInFlight: false, + identityCallFailed: false, }, _Persistence: {}, } as unknown) as IMParticleWebSDKInstance; @@ -227,6 +231,8 @@ describe('Identity Api Client', () => { expect(invokeCallbackSpy.args[0][0]).to.equal(callbackSpy); expect(invokeCallbackSpy.args[0][1]).to.equal(-1); expect(invokeCallbackSpy.args[0][2]).to.equal('server error'); + // Verify identityCallFailed is set to true when identity call fails + expect(mpInstance._Store.identityCallFailed, 'identityCallFailed should be true').to.eq(true); }); it('should use XHR if fetch is not available', async () => { @@ -395,6 +401,7 @@ describe('Identity Api Client', () => { SDKConfig: { identityUrl: '', }, + identityCallFailed: false, }, _Persistence: {}, } as unknown) as IMParticleWebSDKInstance; @@ -435,6 +442,8 @@ describe('Identity Api Client', () => { expect(parseIdentityResponseSpy.args[0][4]).to.equal('identify'); expect(parseIdentityResponseSpy.args[0][5]).to.deep.equal(identityRequest.known_identities); expect(parseIdentityResponseSpy.args[0][6]).to.equal(false); + // A 400 is handled in the success path, so identityCallFailed should be false + expect(mpInstance._Store.identityCallFailed, 'identityCallFailed should be false for 400').to.eq(false); }); it('should include a detailed error message if the fetch returns a 401 (Unauthorized)', async () => { @@ -450,7 +459,7 @@ describe('Identity Api Client', () => { const verboseSpy = sinon.spy(); const errorSpy = sinon.spy(); - const mpInstance: IMParticleWebSDKInstance = ({ + const mpInstance: IMParticleWebSDKInstance = ({ Logger: { verbose: (message) => verboseSpy(message), error: (message) => errorSpy(message), @@ -467,6 +476,7 @@ describe('Identity Api Client', () => { SDKConfig: { identityUrl: '', }, + identityCallFailed: false, }, _Persistence: {}, } as unknown) as IMParticleWebSDKInstance; @@ -497,6 +507,8 @@ describe('Identity Api Client', () => { // A 401 should not call parseIdentityResponse expect(parseIdentityResponseSpy.calledOnce, 'parseIdentityResponseSpy').to.eq(false); + // Verify identityCallFailed is set to false when identity call fails + expect(mpInstance._Store.identityCallFailed, 'identityCallFailed should be true').to.eq(true); }); it('should include a detailed error message if the fetch returns a 403 (Forbidden)', async () => { @@ -528,6 +540,7 @@ describe('Identity Api Client', () => { SDKConfig: { identityUrl: '', }, + identityCallFailed: false, }, _Persistence: {}, } as unknown) as IMParticleWebSDKInstance; @@ -558,6 +571,8 @@ describe('Identity Api Client', () => { // A 403 should not call parseIdentityResponse expect(parseIdentityResponseSpy.calledOnce, 'parseIdentityResponseSpy').to.eq(false); + // Verify identityCallFailed is set to true when identity call fails (403 throws error, caught in catch block) + expect(mpInstance._Store.identityCallFailed, 'identityCallFailed should be true').to.eq(true); }); @@ -590,6 +605,7 @@ describe('Identity Api Client', () => { SDKConfig: { identityUrl: '', }, + identityCallFailed: false, }, _Persistence: {}, } as unknown) as IMParticleWebSDKInstance; @@ -620,6 +636,8 @@ describe('Identity Api Client', () => { // A 404 should not call parseIdentityResponse expect(parseIdentityResponseSpy.calledOnce, 'parseIdentityResponseSpy').to.eq(false); + // Verify identityCallFailed is set to true when identity call fails (404 throws error, caught in catch block) + expect(mpInstance._Store.identityCallFailed, 'identityCallFailed should be true').to.eq(true); }); it('should include a detailed error message if the fetch returns a 500 (Server Error)', async () => { @@ -660,6 +678,7 @@ describe('Identity Api Client', () => { SDKConfig: { identityUrl: '', }, + identityCallFailed: false, }, _Persistence: {}, } as unknown) as IMParticleWebSDKInstance; @@ -683,6 +702,8 @@ describe('Identity Api Client', () => { expect(errorSpy.calledOnce, 'errorSpy called').to.eq(true); expect(errorSpy.args[0][0]).to.equal('Error sending identity request to servers - Received HTTP Code of 500'); + // Verify identityCallFailed is set to true when identity call fails + expect(mpInstance._Store.identityCallFailed, 'identityCallFailed should be true').to.eq(true); }); });