From 167f364019b626cb3b444aea5efc97667731b7b9 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Wed, 17 Jun 2026 17:14:36 -0400 Subject: [PATCH] chore: remove client-side mParticle batch-stream pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The batch-stream path that forwarded mParticle event batches to the Rokt Transactions Gateway via window.Rokt.__batch_stream__() is now redundant — server-side mParticle integration handles this data independently. Remove: - processBatch / sendBatchStream / drainBatchQueue - batchQueue / batchStreamQueue queuing arrays - enrichCommerceEventTypes commerce override - mergePendingIdentityEvents / buildIdentityEvent / pendingIdentityEvents - ROKT_IDENTITY_EVENT_TYPE enum and RoktIdentityEventType - __batch_stream__ from the RoktGlobal interface IDSync (Workspace search) and per-event placement-flag mapping are untouched. handleIdentityComplete keeps only the userAttributes refresh, so the identity callbacks remain no-op-safe and continue returning their success strings. --- src/Rokt-Kit.ts | 111 +------ test/src/tests.spec.ts | 640 ----------------------------------------- 2 files changed, 6 insertions(+), 745 deletions(-) diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index 06e343d..5f91127 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -16,11 +16,9 @@ // Types // ============================================================ -import { Batch, KitInterface, IMParticleUser, SDKEvent } from '@mparticle/web-sdk/internal'; +import { KitInterface, IMParticleUser, SDKEvent } from '@mparticle/web-sdk/internal'; import type { IUserIdentities } from '@mparticle/web-sdk'; -// BaseEvent not re-exported from @mparticle/web-sdk/internal, so we import directly from @mparticle/event-models. -import { BaseEvent, CommerceEvent } from '@mparticle/event-models'; import { isSelectPlacementsAttributePersistenceDenied, removeSelectPlacementsAttributePersistenceDeniedAttributes, @@ -81,7 +79,6 @@ interface RoktGlobal { createLauncher(options: Record): Promise; createLocalLauncher(options: Record): RoktLauncher; currentLauncher?: RoktLauncher; - __batch_stream__?(batch: Batch): void; setExtensionData(data: Record): void; } @@ -234,12 +231,6 @@ const moduleId = 181; const EVENT_NAME_SELECT_PLACEMENTS = 'selectPlacements'; const ADBLOCK_CONTROL_DOMAIN = 'apps.roktecommerce.com'; const INIT_LOG_SAMPLING_RATE = 0.1; -const ROKT_IDENTITY_EVENT_TYPE = { - LOGIN: 'login', - LOGOUT: 'logout', - MODIFY_USER: 'modify_user', - IDENTIFY: 'identify', -} as const; const ROKT_THANK_YOU_JOURNEY_EXTENSION = 'ThankYouPageJourney'; const ROKT_INTEGRATION_SCRIPT_ID = 'rokt-launcher'; const ROKT_THANK_YOU_ELEMENT_SCRIPT_ID = 'rokt-thank-you-element'; @@ -251,8 +242,6 @@ const USER_IDENTIFIED_IN_WORKSPACE_KEY = 'userIdentifiedInWorkspace'; // stalled search never blocks placement rendering on a thank-you page. const WORKSPACE_SEARCH_SELECT_TIMEOUT_MS = 500; -type RoktIdentityEventType = (typeof ROKT_IDENTITY_EVENT_TYPE)[keyof typeof ROKT_IDENTITY_EVENT_TYPE]; - // ============================================================ // Reporting service constants // ============================================================ @@ -705,9 +694,6 @@ class RoktKit implements KitInterface { public testHelpers: TestHelpers | null = null; public placementEventMappingLookup: Record = {}; public placementEventAttributeMappingLookup: Record = {}; - public batchQueue: Batch[] = []; - public batchStreamQueue: Batch[] = []; - public pendingIdentityEvents: BaseEvent[] = []; public integrationName: string | null = null; public domain?: string; public errorReportingService: ErrorReportingService | null = null; @@ -878,88 +864,6 @@ class RoktKit implements KitInterface { mp().logEvent(EVENT_NAME_SELECT_PLACEMENTS, EVENT_TYPE_OTHER, attributes as Record); } - private buildIdentityEvent(eventType: RoktIdentityEventType, filteredUser: FilteredUser): BaseEvent { - const mpid = filteredUser.getMPID(); - const sessionUuid = - mp() && mp().sessionManager && typeof mp().sessionManager!.getSession === 'function' - ? mp().sessionManager!.getSession() - : undefined; - const userIdentities = this.returnUserIdentities(filteredUser); - - return { - event_type: eventType, - data: { - timestamp_unixtime_ms: Date.now(), - session_uuid: sessionUuid ?? undefined, - mpid, - user_identities: userIdentities, - user_attributes: this.userAttributes, - }, - } as unknown as BaseEvent; - } - - private mergePendingIdentityEvents(batch: Batch): Batch { - if (this.pendingIdentityEvents.length === 0) { - return batch; - } - const merged: Batch = { - ...batch, - events: [...(batch.events ?? []), ...this.pendingIdentityEvents], - }; - this.pendingIdentityEvents = []; - return merged; - } - - private drainBatchQueue(): void { - this.batchQueue.forEach((batch) => { - this.processBatch(batch); - }); - this.batchQueue = []; - } - - public processBatch(batch: Batch): string { - if (!this.isKitReady()) { - this.batchQueue.push(batch); - return 'Batch queued for forwarder: ' + name; - } - const enrichedBatch = this.enrichCommerceEventTypes(this.mergePendingIdentityEvents(batch)); - this.sendBatchStream(enrichedBatch); - return 'Successfully sent batch to forwarder: ' + name; - } - - private enrichCommerceEventTypes(batch: Batch): Batch { - if (!batch.events) { - return batch; - } - for (const event of batch.events) { - if (event.event_type !== 'commerce_event') continue; - - const { data } = event as CommerceEvent; - if (!data) continue; - - const commerceType = data.custom_flags?.['Rokt.CommerceEventType']; - if (commerceType && isObject(data.product_action)) { - (data.product_action as { action: string }).action = commerceType; - } - } - return batch; - } - - private sendBatchStream(batch: Batch): void { - if (window.Rokt && typeof window.Rokt.__batch_stream__ === 'function') { - if (this.batchStreamQueue.length) { - const queuedBatches = this.batchStreamQueue; - this.batchStreamQueue = []; - for (let i = 0; i < queuedBatches.length; i++) { - window.Rokt.__batch_stream__(queuedBatches[i]); - } - } - window.Rokt.__batch_stream__(batch); - } else { - this.batchStreamQueue.push(batch); - } - } - private setRoktSessionId(sessionId: string): void { if (!sessionId || typeof sessionId !== 'string') { return; @@ -1037,7 +941,6 @@ class RoktKit implements KitInterface { // Attaches the kit to the Rokt manager mp().Rokt.attachKit(this); - this.drainBatchQueue(); } private fetchOptimizely(): Record { @@ -1263,10 +1166,8 @@ class RoktKit implements KitInterface { return 'Successfully removed user attribute for forwarder: ' + name; } - private handleIdentityComplete(user: IMParticleUser, eventType: RoktIdentityEventType, callbackName: string): string { - const filteredUser = user as FilteredUser; + private handleIdentityComplete(user: IMParticleUser, callbackName: string): string { this.userAttributes = removeSelectPlacementsAttributePersistenceDeniedAttributes(user.getAllUserAttributes()); - this.pendingIdentityEvents.push(this.buildIdentityEvent(eventType, filteredUser)); return 'Successfully called ' + callbackName + ' for forwarder: ' + name; } @@ -1274,7 +1175,7 @@ class RoktKit implements KitInterface { const filteredUser = user as FilteredUser; this.filters.filteredUser = filteredUser; this._workspaceSearchInFlightPromise = this.search(filteredUser); - return this.handleIdentityComplete(user, ROKT_IDENTITY_EVENT_TYPE.IDENTIFY, 'onUserIdentified'); + return this.handleIdentityComplete(user, 'onUserIdentified'); } private search(filteredUser: FilteredUser): Promise { @@ -1357,7 +1258,7 @@ class RoktKit implements KitInterface { } public onLoginComplete(user: IMParticleUser, _filteredIdentityRequest: unknown): string { - return this.handleIdentityComplete(user, ROKT_IDENTITY_EVENT_TYPE.LOGIN, 'onLoginComplete'); + return this.handleIdentityComplete(user, 'onLoginComplete'); } public onLogoutComplete(user: IMParticleUser, _filteredIdentityRequest: unknown): string { @@ -1368,11 +1269,11 @@ class RoktKit implements KitInterface { this.userIdentifiedInWorkspace = false; this._workspaceSearchInFlightPromise = null; this._workspaceLastSearchedIdentitiesKey = undefined; - return this.handleIdentityComplete(user, ROKT_IDENTITY_EVENT_TYPE.LOGOUT, 'onLogoutComplete'); + return this.handleIdentityComplete(user, 'onLogoutComplete'); } public onModifyComplete(user: IMParticleUser, _filteredIdentityRequest: unknown): string { - return this.handleIdentityComplete(user, ROKT_IDENTITY_EVENT_TYPE.MODIFY_USER, 'onModifyComplete'); + return this.handleIdentityComplete(user, 'onModifyComplete'); } /** diff --git a/test/src/tests.spec.ts b/test/src/tests.spec.ts index 162765c..8fc54b8 100644 --- a/test/src/tests.spec.ts +++ b/test/src/tests.spec.ts @@ -5,7 +5,6 @@ import { isSelectPlacementsAttributePersistenceDenied, removeSelectPlacementsAttributePersistenceDeniedAttributes, } from '../../src/selectPlacementsAttributePersistence'; -import { Batch } from '@mparticle/web-sdk/internal'; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -5597,645 +5596,6 @@ describe('Rokt Forwarder', () => { }); }); - describe('#processBatch', () => { - let mockBatch: Batch; - - beforeEach(() => { - (window as any).mParticle.forwarder.batchQueue = []; - (window as any).mParticle.forwarder.batchStreamQueue = []; - (window as any).mParticle.forwarder.pendingIdentityEvents = []; - (window as any).Rokt = new (MockRoktForwarder as any)(); - (window as any).Rokt.createLauncher = async function () { - return Promise.resolve({ - selectPlacements: function (options: any) { - (window as any).mParticle.Rokt.selectPlacementsOptions = options; - (window as any).mParticle.Rokt.selectPlacementsCalled = true; - }, - }); - }; - (window as any).mParticle.Rokt = (window as any).Rokt; - (window as any).mParticle.Rokt.attachKitCalled = false; - (window as any).mParticle.Rokt.attachKit = async (kit: any) => { - (window as any).mParticle.Rokt.attachKitCalled = true; - (window as any).mParticle.Rokt.kit = kit; - Promise.resolve(); - }; - (window as any).mParticle.Rokt.setLocalSessionAttribute = function (key: any, value: any) { - (window as any).mParticle._Store.localSessionAttributes[key] = value; - }; - (window as any).mParticle.Rokt.getLocalSessionAttributes = function () { - return (window as any).mParticle._Store.localSessionAttributes; - }; - (window as any).mParticle.forwarder.launcher = { - selectPlacements: function (options: any) { - (window as any).mParticle.Rokt.selectPlacementsOptions = options; - (window as any).mParticle.Rokt.selectPlacementsCalled = true; - }, - }; - (window as any).mParticle.Rokt.filters = { - userAttributesFilters: [], - filterUserAttributes: function (attributes: any) { - return attributes; - }, - filteredUser: { - getMPID: function () { - return '123'; - }, - }, - }; - - mockBatch = { - mpid: 'test-mpid-123', - user_attributes: { 'user-attr': 'user-value' }, - user_identities: { email: 'test@example.com' }, - events: [ - { - event_type: 'custom_event', - data: { event_name: 'Test Event', custom_event_type: 'other' }, - }, - ], - }; - }); - - afterEach(() => { - delete (window as any).Rokt.__batch_stream__; - (window as any).mParticle.forwarder.batchQueue = []; - (window as any).mParticle.forwarder.batchStreamQueue = []; - (window as any).mParticle.forwarder.pendingIdentityEvents = []; - (window as any).mParticle.forwarder.isInitialized = false; - (window as any).mParticle.Rokt.attachKitCalled = false; - }); - - it('should send batch to window.Rokt.__batch_stream__ when kit is ready', async () => { - const receivedBatches: any[] = []; - (window as any).Rokt.__batch_stream__ = function (payload: any) { - receivedBatches.push(payload); - }; - - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - (window as any).mParticle.forwarder.processBatch(mockBatch); - - expect(receivedBatches.length).toBe(1); - expect(receivedBatches[0].mpid).toBe('test-mpid-123'); - expect(receivedBatches[0].user_attributes).toEqual({ 'user-attr': 'user-value' }); - expect(receivedBatches[0].user_identities).toEqual({ email: 'test@example.com' }); - expect(receivedBatches[0].events.length).toBe(1); - }); - - it('should not add extra events when pendingIdentityEvents is empty', async () => { - const receivedBatches: any[] = []; - (window as any).Rokt.__batch_stream__ = function (payload: any) { - receivedBatches.push(payload); - }; - - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - expect((window as any).mParticle.forwarder.pendingIdentityEvents.length).toBe(0); - - (window as any).mParticle.forwarder.processBatch(mockBatch); - - expect(receivedBatches.length).toBe(1); - expect(receivedBatches[0].events.length).toBe(1); - expect(receivedBatches[0].events[0].event_type).toBe('custom_event'); - }); - - it('should queue batch in batchQueue when kit is not initialized', () => { - (window as any).mParticle.forwarder.isInitialized = false; - (window as any).mParticle.forwarder.launcher = null; - - expect(() => { - (window as any).mParticle.forwarder.processBatch(mockBatch); - }).not.toThrow(); - - expect((window as any).mParticle.forwarder.batchQueue.length).toBe(1); - expect((window as any).mParticle.forwarder.batchQueue[0]).toEqual(mockBatch); - }); - - it('should flush batchQueue when kit becomes ready', async () => { - const receivedBatches: any[] = []; - (window as any).Rokt.__batch_stream__ = function (payload: any) { - receivedBatches.push(payload); - }; - - (window as any).mParticle.forwarder.isInitialized = false; - (window as any).mParticle.forwarder.launcher = null; - (window as any).mParticle.forwarder.processBatch(mockBatch); - - expect((window as any).mParticle.forwarder.batchQueue.length).toBe(1); - - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - expect(receivedBatches.length).toBe(1); - expect(receivedBatches[0].mpid).toBe('test-mpid-123'); - expect((window as any).mParticle.forwarder.batchQueue.length).toBe(0); - }); - - it('should queue batch in batchStreamQueue when window.Rokt.__batch_stream__ is not defined', async () => { - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - expect(() => { - (window as any).mParticle.forwarder.processBatch(mockBatch); - }).not.toThrow(); - - expect((window as any).mParticle.forwarder.batchStreamQueue.length).toBe(1); - expect((window as any).mParticle.forwarder.batchStreamQueue[0]).toEqual(mockBatch); - }); - - it('should queue batch in batchStreamQueue when window.Rokt is undefined', async () => { - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - const savedRokt = (window as any).Rokt; - delete (window as any).Rokt; - - expect(() => { - (window as any).mParticle.forwarder.processBatch(mockBatch); - }).not.toThrow(); - - expect((window as any).mParticle.forwarder.batchStreamQueue.length).toBe(1); - expect((window as any).mParticle.forwarder.batchStreamQueue[0]).toEqual(mockBatch); - - (window as any).Rokt = savedRokt; - }); - - it('should flush batchStreamQueue before sending the next batch', async () => { - const receivedBatches: any[] = []; - - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - const batchA = { mpid: 'mpid-A', events: [], user_attributes: {} }; - const batchB = { mpid: 'mpid-B', events: [], user_attributes: {} }; - const batchC = { mpid: 'mpid-C', events: [], user_attributes: {} }; - - (window as any).mParticle.forwarder.processBatch(batchA); - (window as any).mParticle.forwarder.processBatch(batchB); - - expect((window as any).mParticle.forwarder.batchStreamQueue.length).toBe(2); - - (window as any).Rokt.__batch_stream__ = function (payload: any) { - receivedBatches.push(payload); - }; - - (window as any).mParticle.forwarder.processBatch(batchC); - - expect(receivedBatches.length).toBe(3); - expect(receivedBatches[0].mpid).toBe('mpid-A'); - expect(receivedBatches[1].mpid).toBe('mpid-B'); - expect(receivedBatches[2].mpid).toBe('mpid-C'); - expect((window as any).mParticle.forwarder.batchStreamQueue.length).toBe(0); - }); - - it('should add an identity event to pendingIdentityEvents on onLoginComplete', () => { - const mockUser = { - getMPID: () => '123', - getAllUserAttributes: () => ({}), - getUserIdentities: () => ({ userIdentities: {} }), - }; - - (window as any).mParticle.forwarder.onLoginComplete(mockUser, null); - - const pending = (window as any).mParticle.forwarder.pendingIdentityEvents; - expect(pending.length).toBe(1); - expect(pending[0].event_type).toBe('login'); - expect(pending[0].data.timestamp_unixtime_ms).toBeTypeOf('number'); - expect(pending[0].data.user_identities).toEqual({}); - expect(pending[0].data.user_attributes).toEqual({}); - }); - - it('should add an identity event to pendingIdentityEvents on onLogoutComplete', () => { - const mockUser = { - getMPID: () => '123', - getAllUserAttributes: () => ({}), - getUserIdentities: () => ({ userIdentities: {} }), - }; - - (window as any).mParticle.forwarder.onLogoutComplete(mockUser, null); - - const pending = (window as any).mParticle.forwarder.pendingIdentityEvents; - expect(pending.length).toBe(1); - expect(pending[0].event_type).toBe('logout'); - expect(pending[0].data.timestamp_unixtime_ms).toBeTypeOf('number'); - expect(pending[0].data.user_identities).toEqual({}); - expect(pending[0].data.user_attributes).toEqual({}); - }); - - it('should add identity events to pendingIdentityEvents on onModifyComplete and onUserIdentified', () => { - const mockUser = { - getMPID: () => '42', - getAllUserAttributes: () => ({}), - getUserIdentities: () => ({ userIdentities: {} }), - }; - - (window as any).mParticle.forwarder.onModifyComplete(mockUser, null); - (window as any).mParticle.forwarder.onUserIdentified(mockUser); - - const pending = (window as any).mParticle.forwarder.pendingIdentityEvents; - expect(pending.length).toBe(2); - expect(pending[0].event_type).toBe('modify_user'); - expect(pending[1].event_type).toBe('identify'); - }); - - it('should include user_identities inside data of the identity event payload', () => { - const mockUser = { - getMPID: () => '123', - getAllUserAttributes: () => ({}), - getUserIdentities: () => ({ - userIdentities: { - email: 'user@example.com', - customerid: 'cust-456', - }, - }), - }; - - (window as any).mParticle.forwarder.onLoginComplete(mockUser, null); - - const pending = (window as any).mParticle.forwarder.pendingIdentityEvents; - expect(pending.length).toBe(1); - expect(pending[0].data.user_identities).toEqual({ - email: 'user@example.com', - customerid: 'cust-456', - }); - }); - - it('should apply emailsha256 mapping to user_identities inside data of the identity event payload', () => { - (window as any).mParticle.forwarder._mappedEmailSha256Key = 'other'; - - const mockUser = { - getMPID: () => '123', - getAllUserAttributes: () => ({}), - getUserIdentities: () => ({ - userIdentities: { - customerid: 'cust-789', - other: 'abc123hashedemail', - }, - }), - }; - - (window as any).mParticle.forwarder.onLoginComplete(mockUser, null); - - const pending = (window as any).mParticle.forwarder.pendingIdentityEvents; - expect(pending.length).toBe(1); - expect(pending[0].data.user_identities).toEqual({ - customerid: 'cust-789', - emailsha256: 'abc123hashedemail', - }); - expect(pending[0].data.user_identities.other).toBeUndefined(); - }); - - it('should include user_attributes inside data of the identity event payload', () => { - const mockUser = { - getMPID: () => '123', - getAllUserAttributes: () => ({ plan: 'premium', region: 'us' }), - getUserIdentities: () => ({ userIdentities: {} }), - }; - - (window as any).mParticle.forwarder.onLoginComplete(mockUser, null); - - const pending = (window as any).mParticle.forwarder.pendingIdentityEvents; - expect(pending.length).toBe(1); - expect(pending[0].data.user_attributes).toEqual({ plan: 'premium', region: 'us' }); - }); - - it('should snapshot user_attributes from the user at event build time', () => { - const mockUserAtLogin = { - getMPID: () => '123', - getAllUserAttributes: () => ({ role: 'admin' }), - getUserIdentities: () => ({ userIdentities: {} }), - }; - const mockUserAtModify = { - getMPID: () => '123', - getAllUserAttributes: () => ({ role: 'viewer' }), - getUserIdentities: () => ({ userIdentities: {} }), - }; - - (window as any).mParticle.forwarder.onLoginComplete(mockUserAtLogin, null); - (window as any).mParticle.forwarder.onModifyComplete(mockUserAtModify, null); - - const pending = (window as any).mParticle.forwarder.pendingIdentityEvents; - expect(pending.length).toBe(2); - expect(pending[0].data.user_attributes).toEqual({ role: 'admin' }); - expect(pending[1].data.user_attributes).toEqual({ role: 'viewer' }); - }); - - it('should include empty user_attributes inside data on logout for an anonymous session', () => { - const anonymousUser = { - getMPID: () => '-1234567890', - getAllUserAttributes: () => ({}), - getUserIdentities: () => ({ userIdentities: {} }), - }; - - (window as any).mParticle.forwarder.onLogoutComplete(anonymousUser, null); - - const pending = (window as any).mParticle.forwarder.pendingIdentityEvents; - expect(pending.length).toBe(1); - expect(pending[0].event_type).toBe('logout'); - expect(pending[0].data.user_identities).toEqual({}); - expect(pending[0].data.user_attributes).toEqual({}); - }); - - it('should merge pendingIdentityEvents into the outgoing batch and clear the queue', async () => { - const receivedBatches: any[] = []; - (window as any).Rokt.__batch_stream__ = function (payload: any) { - receivedBatches.push(payload); - }; - - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - const mockUser = { - getMPID: () => '123', - getAllUserAttributes: () => ({}), - getUserIdentities: () => ({ userIdentities: {} }), - }; - - (window as any).mParticle.forwarder.onLoginComplete(mockUser, null); - expect((window as any).mParticle.forwarder.pendingIdentityEvents.length).toBe(1); - - (window as any).mParticle.forwarder.processBatch(mockBatch); - - expect(receivedBatches.length).toBe(1); - // Original 1 custom_event + 1 identity event from onLoginComplete - expect(receivedBatches[0].events.length).toBe(2); - expect(receivedBatches[0].events[1].event_type).toBe('login'); - expect(receivedBatches[0].events[1].data.timestamp_unixtime_ms).toBeTypeOf('number'); - expect(receivedBatches[0].events[1].data.user_identities).toEqual({}); - expect(receivedBatches[0].events[1].data.user_attributes).toEqual({}); - // Queue should be cleared after flush - expect((window as any).mParticle.forwarder.pendingIdentityEvents.length).toBe(0); - }); - - it('should merge pendingIdentityEvents into the first queued batch when kit becomes ready', async () => { - const receivedBatches: any[] = []; - (window as any).Rokt.__batch_stream__ = function (payload: any) { - receivedBatches.push(payload); - }; - - // Queue a batch before the kit initialises - (window as any).mParticle.forwarder.isInitialized = false; - (window as any).mParticle.forwarder.launcher = null; - - const mockUser = { - getMPID: () => '123', - getAllUserAttributes: () => ({}), - getUserIdentities: () => ({ userIdentities: {} }), - }; - - // Identity callback fires before kit is ready - (window as any).mParticle.forwarder.onLoginComplete(mockUser, null); - (window as any).mParticle.forwarder.processBatch(mockBatch); - - expect((window as any).mParticle.forwarder.batchQueue.length).toBe(1); - expect((window as any).mParticle.forwarder.pendingIdentityEvents.length).toBe(1); - - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - // The queued batch should have the pending identity event merged in - expect(receivedBatches.length).toBe(1); - expect(receivedBatches[0].events.length).toBe(2); - expect(receivedBatches[0].events[1].event_type).toBe('login'); - expect((window as any).mParticle.forwarder.pendingIdentityEvents.length).toBe(0); - expect((window as any).mParticle.forwarder.batchQueue.length).toBe(0); - }); - }); - - describe('#enrichCommerceEventTypes', () => { - let mockCommerceBatch: Batch; - - beforeEach(() => { - (window as any).mParticle.forwarder.batchQueue = []; - (window as any).mParticle.forwarder.batchStreamQueue = []; - (window as any).mParticle.forwarder.pendingIdentityEvents = []; - (window as any).Rokt = new (MockRoktForwarder as any)(); - (window as any).Rokt.createLauncher = async function () { - return Promise.resolve({ - selectPlacements: function (options: any) { - (window as any).mParticle.Rokt.selectPlacementsOptions = options; - (window as any).mParticle.Rokt.selectPlacementsCalled = true; - }, - }); - }; - (window as any).mParticle.Rokt = (window as any).Rokt; - (window as any).mParticle.Rokt.attachKitCalled = false; - (window as any).mParticle.Rokt.attachKit = async (kit: any) => { - (window as any).mParticle.Rokt.attachKitCalled = true; - (window as any).mParticle.Rokt.kit = kit; - Promise.resolve(); - }; - (window as any).mParticle.Rokt.setLocalSessionAttribute = function (key: any, value: any) { - (window as any).mParticle._Store.localSessionAttributes[key] = value; - }; - (window as any).mParticle.Rokt.getLocalSessionAttributes = function () { - return (window as any).mParticle._Store.localSessionAttributes; - }; - (window as any).mParticle.forwarder.launcher = { - selectPlacements: function (options: any) { - (window as any).mParticle.Rokt.selectPlacementsOptions = options; - (window as any).mParticle.Rokt.selectPlacementsCalled = true; - }, - }; - (window as any).mParticle.Rokt.filters = { - userAttributesFilters: [], - filterUserAttributes: function (attributes: any) { - return attributes; - }, - filteredUser: { - getMPID: function () { - return '123'; - }, - }, - }; - - mockCommerceBatch = { - mpid: 'test-mpid-123', - events: [ - { - event_type: 'commerce_event', - data: { - custom_flags: { 'Rokt.CommerceEventType': 'payment_succeeded' }, - product_action: { - action: 'unknown', - products: [{ id: 'SKU-1', name: 'Test Product', price: 50, quantity: 1 }], - }, - }, - }, - ], - }; - }); - - afterEach(() => { - delete (window as any).Rokt.__batch_stream__; - (window as any).mParticle.forwarder.batchQueue = []; - (window as any).mParticle.forwarder.batchStreamQueue = []; - (window as any).mParticle.forwarder.pendingIdentityEvents = []; - (window as any).mParticle.forwarder.isInitialized = false; - (window as any).mParticle.Rokt.attachKitCalled = false; - }); - - it('should replace action with Rokt.CommerceEventType custom flag for commerce events', async () => { - const receivedBatches: any[] = []; - (window as any).Rokt.__batch_stream__ = function (payload: any) { - receivedBatches.push(payload); - }; - - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - (window as any).mParticle.forwarder.processBatch(mockCommerceBatch); - - expect(receivedBatches.length).toBe(1); - expect(receivedBatches[0].events[0].data.product_action.action).toBe('payment_succeeded'); - }); - - it('should not modify action when Rokt.CommerceEventType custom flag is absent', async () => { - const receivedBatches: any[] = []; - (window as any).Rokt.__batch_stream__ = function (payload: any) { - receivedBatches.push(payload); - }; - - mockCommerceBatch.events[0].data.custom_flags = {}; - - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - (window as any).mParticle.forwarder.processBatch(mockCommerceBatch); - - expect(receivedBatches.length).toBe(1); - expect(receivedBatches[0].events[0].data.product_action.action).toBe('unknown'); - }); - - it('should not modify non-commerce events', async () => { - const receivedBatches: any[] = []; - (window as any).Rokt.__batch_stream__ = function (payload: any) { - receivedBatches.push(payload); - }; - - const nonCommerceBatch: Batch = { - mpid: 'test-mpid-123', - events: [ - { - event_type: 'custom_event', - data: { - event_name: 'Test Event', - custom_event_type: 'other', - custom_flags: { 'Rokt.CommerceEventType': 'payment_succeeded' }, - }, - }, - ], - }; - - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - (window as any).mParticle.forwarder.processBatch(nonCommerceBatch); - - expect(receivedBatches.length).toBe(1); - expect(receivedBatches[0].events[0].event_type).toBe('custom_event'); - expect(receivedBatches[0].events[0].data.product_action).toBeUndefined(); - }); - - it('should enrich only commerce events in a mixed batch', async () => { - const receivedBatches: any[] = []; - (window as any).Rokt.__batch_stream__ = function (payload: any) { - receivedBatches.push(payload); - }; - - const mixedBatch: Batch = { - mpid: 'test-mpid-123', - events: [ - { - event_type: 'custom_event', - data: { event_name: 'Page View', custom_event_type: 'other' }, - }, - { - event_type: 'commerce_event', - data: { - custom_flags: { 'Rokt.CommerceEventType': 'refund_initiated' }, - product_action: { action: 'unknown', products: [] }, - }, - }, - { - event_type: 'commerce_event', - data: { - custom_flags: {}, - product_action: { action: 'purchase', products: [] }, - }, - }, - ], - }; - - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - (window as any).mParticle.forwarder.processBatch(mixedBatch); - - expect(receivedBatches.length).toBe(1); - const events = receivedBatches[0].events; - expect(events[0].event_type).toBe('custom_event'); - expect(events[1].data.product_action.action).toBe('refund_initiated'); - expect(events[2].data.product_action.action).toBe('purchase'); - }); - - it('should not throw when product_action is null', async () => { - const receivedBatches: any[] = []; - (window as any).Rokt.__batch_stream__ = function (payload: any) { - receivedBatches.push(payload); - }; - - const promoBatch: Batch = { - mpid: 'test-mpid-123', - events: [ - { - event_type: 'commerce_event', - data: { - custom_flags: {}, - promotion_action: { action: 'view', promotions: [{ id: 'promo-1', name: 'Summer Sale' }] }, - }, - }, - ], - }; - - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - expect(() => { - (window as any).mParticle.forwarder.processBatch(promoBatch); - }).not.toThrow(); - - expect(receivedBatches.length).toBe(1); - expect(receivedBatches[0].events[0].data.promotion_action.action).toBe('view'); - expect(receivedBatches[0].events[0].data.product_action).toBeUndefined(); - }); - - it('should handle batch with no events', async () => { - const receivedBatches: any[] = []; - (window as any).Rokt.__batch_stream__ = function (payload: any) { - receivedBatches.push(payload); - }; - - const emptyBatch: Batch = { mpid: 'test-mpid-123', events: [] }; - - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - (window as any).mParticle.forwarder.processBatch(emptyBatch); - - expect(receivedBatches.length).toBe(1); - expect(receivedBatches[0].events.length).toBe(0); - }); - }); - describe('#_setRoktSessionId', () => { let setIntegrationAttributeCalls: any[];