diff --git a/package-lock.json b/package-lock.json index 7e07008..983f9bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,12 @@ "version": "1.23.0", "license": "Apache-2.0", "dependencies": { - "@mparticle/web-sdk": "^2.56.0" + "@mparticle/web-sdk": "^2.62.0" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.23.0", + "@mparticle/event-models": "^1.1.9", "@semantic-release/changelog": "^6.0.3", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", @@ -1266,13 +1267,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/@mparticle/event-models": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@mparticle/event-models/-/event-models-1.1.9.tgz", + "integrity": "sha512-2ucTwTKKA4xZjcSMlkim3rvr6d8uyi9yWDqOW8b9RDah2ak+dI0PkWANHkMJqteQprEg9mOkSvxc17kv5adIIA==", + "license": "Apache-2.0" + }, "node_modules/@mparticle/web-sdk": { - "version": "2.56.0", - "resolved": "https://registry.npmjs.org/@mparticle/web-sdk/-/web-sdk-2.56.0.tgz", - "integrity": "sha512-HytxaOUYEIHei5Y6OPQoQl7mmM6H2EBI1CDJ5VVB3otsvO/CQqSzyd6T1VC4hBcJ8qVJ9h7p6hIxIteVsYyXeg==", + "version": "2.62.0", + "resolved": "https://registry.npmjs.org/@mparticle/web-sdk/-/web-sdk-2.62.0.tgz", + "integrity": "sha512-JJV61ValoRNnS4hq3hMe1U9SVkI6EQC+b8Jg/11mq/yMO7xoqvfC274BjNd8I2sYtr5LgD3zGnHOz8gcBn7+Xw==", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.23.2" + }, + "peerDependencies": { + "@mparticle/event-models": "^1.1.9" } }, "node_modules/@octokit/auth-token": { diff --git a/package.json b/package.json index e576866..b1ec5d2 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "devDependencies": { "@eslint/js": "^9.23.0", "@eslint/eslintrc": "^3.3.1", + "@mparticle/event-models": "^1.1.9", "@semantic-release/changelog": "^6.0.3", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", @@ -56,7 +57,7 @@ "vitest": "^4.0.0" }, "dependencies": { - "@mparticle/web-sdk": "^2.56.0" + "@mparticle/web-sdk": "^2.62.0" }, "license": "Apache-2.0" } diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index 5b0786d..6970ca9 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -16,6 +16,10 @@ // Types // ============================================================ +import { Batch, KitInterface, IMParticleUser, SDKEvent } from '@mparticle/web-sdk/internal'; +// BaseEvent not re-exported from @mparticle/web-sdk/internal, so we import directly from @mparticle/event-models. +import { BaseEvent } from '@mparticle/event-models'; + interface RoktKitSettings { accountId: string; roktExtensions?: string; @@ -71,13 +75,14 @@ interface RoktGlobal { createLauncher(options: Record): Promise; createLocalLauncher(options: Record): RoktLauncher; currentLauncher?: RoktLauncher; - __event_stream__?(event: Record): void; + __batch_stream__?(batch: Batch): void; setExtensionData(data: Record): void; } -interface FilteredUser { +// TODO: getMPID and getUserIdentities exist on the User base type but are not re-exported from +// @mparticle/web-sdk/internal, so we redeclare them here until the internal types expose them. +interface FilteredUser extends IMParticleUser { getMPID(): string; - getAllUserAttributes(): Record; getUserIdentities?: () => { userIdentities: Record }; } @@ -155,14 +160,6 @@ interface ForwarderRegistration { getId: () => number; } -interface MParticleEvent { - EventDataType: number; - EventCategory: number; - EventName?: string; - EventAttributes?: Record; - [key: string]: unknown; -} - interface ReportingConfig { loggingUrl?: string; errorUrl?: string; @@ -584,7 +581,7 @@ class LoggingService { // RoktKit class // ============================================================ -class RoktKit { +class RoktKit implements KitInterface { // Static field for allowed origin hashes (mutable by testHelpers) public static _allowedOriginHashes: number[] = [-553112570, 549508659]; @@ -596,6 +593,7 @@ class RoktKit { // Public fields (accessed by tests and the mParticle framework) public name = name; + public id = moduleId; public moduleId = moduleId; public isInitialized = false; public launcher: RoktLauncher | null = null; @@ -604,8 +602,9 @@ class RoktKit { public testHelpers: TestHelpers | null = null; public placementEventMappingLookup: Record = {}; public placementEventAttributeMappingLookup: Record = {}; - public eventQueue: MParticleEvent[] = []; - public eventStreamQueue: MParticleEvent[] = []; + public batchQueue: Batch[] = []; + public batchStreamQueue: Batch[] = []; + public pendingIdentityEvents: BaseEvent[] = []; public integrationName: string | null = null; public domain?: string; public errorReportingService: ErrorReportingService | null = null; @@ -617,7 +616,7 @@ class RoktKit { // ---- Private helpers ---- - private getEventAttributeValue(event: MParticleEvent, eventAttributeKey: string): unknown { + private getEventAttributeValue(event: SDKEvent, eventAttributeKey: string): unknown { const attributes = event && event.EventAttributes; if (!attributes) { return null; @@ -657,7 +656,7 @@ class RoktKit { return false; } - private doesEventMatchRule(event: MParticleEvent, rule: PlacementEventRule): boolean { + private doesEventMatchRule(event: SDKEvent, rule: PlacementEventRule): boolean { if (!rule || !isString(rule.eventAttributeKey)) { return false; } @@ -681,7 +680,7 @@ class RoktKit { return true; } - private applyPlacementEventAttributeMapping(event: MParticleEvent): void { + private applyPlacementEventAttributeMapping(event: SDKEvent): void { const mappedAttributeKeys = Object.keys(this.placementEventAttributeMappingLookup); for (let i = 0; i < mappedAttributeKeys.length; i++) { const mappedAttributeKey = mappedAttributeKeys[i]; @@ -760,29 +759,68 @@ class RoktKit { mp().logEvent(EVENT_NAME_SELECT_PLACEMENTS, EVENT_TYPE_OTHER, attributes as Record); } - private processEventQueue(): void { - this.eventQueue.forEach((event) => { - this.process(event); + private buildIdentityEvent(eventName: string, filteredUser: FilteredUser): BaseEvent { + const mpid = filteredUser.getMPID(); + const sessionId = + mp() && mp().sessionManager && typeof mp().sessionManager!.getSession === 'function' + ? mp().sessionManager!.getSession() + : null; + const userIdentities = + filteredUser.getUserIdentities && typeof filteredUser.getUserIdentities === 'function' + ? filteredUser.getUserIdentities().userIdentities + : null; + + return { + EventName: eventName, + EventDataType: MESSAGE_TYPE_PROFILE, + EventCategory: 0, + Timestamp: Date.now(), + MPID: mpid, + SessionId: sessionId, + UserIdentities: userIdentities, + } 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.eventQueue = []; + this.batchQueue = []; } - private enrichEvent(event: MParticleEvent): Record { - return { ...(event as Record), UserAttributes: this.userAttributes }; + public processBatch(batch: Batch): string { + if (!this.isKitReady()) { + this.batchQueue.push(batch); + return 'Batch queued for forwarder: ' + name; + } + this.sendBatchStream(this.mergePendingIdentityEvents(batch)); + return 'Successfully sent batch to forwarder: ' + name; } - private sendEventStream(event: MParticleEvent): void { - if (window.Rokt && typeof window.Rokt.__event_stream__ === 'function') { - if (this.eventStreamQueue.length) { - const queuedEvents = this.eventStreamQueue; - this.eventStreamQueue = []; - for (let i = 0; i < queuedEvents.length; i++) { - window.Rokt.__event_stream__(this.enrichEvent(queuedEvents[i])); + 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.__event_stream__(this.enrichEvent(event)); + window.Rokt.__batch_stream__(batch); } else { - this.eventStreamQueue.push(event); + this.batchStreamQueue.push(batch); } } @@ -802,28 +840,6 @@ class RoktKit { } } - private buildIdentityEvent(eventName: string, filteredUser: FilteredUser): MParticleEvent { - const mpid = filteredUser.getMPID && typeof filteredUser.getMPID === 'function' ? filteredUser.getMPID() : null; - const sessionId = - mp() && mp().sessionManager && typeof mp().sessionManager!.getSession === 'function' - ? mp().sessionManager!.getSession() - : null; - const userIdentities = - filteredUser.getUserIdentities && typeof filteredUser.getUserIdentities === 'function' - ? filteredUser.getUserIdentities().userIdentities - : null; - - return { - EventName: eventName, - EventDataType: MESSAGE_TYPE_PROFILE, - EventCategory: 0, - Timestamp: Date.now(), - MPID: mpid, - SessionId: sessionId, - UserIdentities: userIdentities, - }; - } - private attachLauncher(accountId: string, launcherOptions: Record): void { const mpSessionId = mp() && mp().sessionManager && typeof mp().sessionManager!.getSession === 'function' @@ -875,7 +891,7 @@ class RoktKit { // Attaches the kit to the Rokt manager mp().Rokt.attachKit(this); - this.processEventQueue(); + this.drainBatchQueue(); } private fetchOptimizely(): Record { @@ -928,28 +944,29 @@ class RoktKit { * Initializes the Rokt forwarder with settings from the mParticle server. */ public init( - settings: RoktKitSettings, + settings: Record, _service: unknown, testMode: boolean, _trackerId: unknown, filteredUserAttributes: Record, - ): void { - const accountId = settings.accountId; - const roktExtensions = extractRoktExtensions(settings.roktExtensions); + ): string { + const kitSettings = settings as unknown as RoktKitSettings; + const accountId = kitSettings.accountId; + const roktExtensions = extractRoktExtensions(kitSettings.roktExtensions); this.userAttributes = filteredUserAttributes || {}; - this._onboardingExpProvider = settings.onboardingExpProvider; + this._onboardingExpProvider = kitSettings.onboardingExpProvider; - const placementEventMapping = parseSettingsString(settings.placementEventMapping); + const placementEventMapping = parseSettingsString(kitSettings.placementEventMapping); this.placementEventMappingLookup = generateMappedEventLookup(placementEventMapping); const placementEventAttributeMapping = parseSettingsString( - settings.placementEventAttributeMapping, + kitSettings.placementEventAttributeMapping, ); this.placementEventAttributeMappingLookup = generateMappedEventAttributeLookup(placementEventAttributeMapping); // Set dynamic OTHER_IDENTITY based on server settings - if (settings.hashedEmailUserIdentityType) { - this._mappedEmailSha256Key = settings.hashedEmailUserIdentityType.toLowerCase(); + if (kitSettings.hashedEmailUserIdentityType) { + this._mappedEmailSha256Key = kitSettings.hashedEmailUserIdentityType.toLowerCase(); } const domain = mp().Rokt?.domain; @@ -962,22 +979,22 @@ class RoktKit { this.domain = domain; const reportingConfig: ReportingConfig = { - loggingUrl: settings.loggingUrl, - errorUrl: settings.errorUrl, - isLoggingEnabled: settings.isLoggingEnabled === 'true' || settings.isLoggingEnabled === true, + loggingUrl: kitSettings.loggingUrl, + errorUrl: kitSettings.errorUrl, + isLoggingEnabled: kitSettings.isLoggingEnabled === 'true' || kitSettings.isLoggingEnabled === true, }; const errorReportingService = new ErrorReportingService( reportingConfig, this.integrationName, window.__rokt_li_guid__, - settings.accountId, + kitSettings.accountId, ); const loggingService = new LoggingService( reportingConfig, errorReportingService, this.integrationName, window.__rokt_li_guid__, - settings.accountId, + kitSettings.accountId, ); this.errorReportingService = errorReportingService; @@ -1012,7 +1029,7 @@ class RoktKit { WSDKErrorSeverity: WSDKErrorSeverity, }; this.attachLauncher(accountId, launcherOptions); - return; + return 'Successfully initialized: ' + name; } if (this.isLauncherReadyToAttach()) { @@ -1042,34 +1059,28 @@ class RoktKit { target.appendChild(script); this.captureTiming(RoktKit.PERFORMANCE_MARKS.RoktScriptAppended); } + + return 'Successfully initialized: ' + name; } - public process(event: MParticleEvent): void { + public process(event: SDKEvent): string { if (!this.isKitReady()) { - this.eventQueue.push(event); - return; - } - - this.sendEventStream(event); - - if (typeof mp().Rokt?.setLocalSessionAttribute !== 'function') { - return; - } - - if (!isEmpty(this.placementEventAttributeMappingLookup)) { - this.applyPlacementEventAttributeMapping(event); + return 'Kit not ready for forwarder: ' + name; } + if (typeof mp().Rokt?.setLocalSessionAttribute === 'function') { + if (!isEmpty(this.placementEventAttributeMappingLookup)) { + this.applyPlacementEventAttributeMapping(event); + } - if (isEmpty(this.placementEventMappingLookup)) { - return; + if (!isEmpty(this.placementEventMappingLookup)) { + const hashedEvent = hashEventMessage(event.EventDataType, event.EventCategory, event.EventName ?? ''); + if (this.placementEventMappingLookup[String(hashedEvent)]) { + mp().Rokt.setLocalSessionAttribute?.(this.placementEventMappingLookup[String(hashedEvent)], true); + } + } } - const hashedEvent = hashEventMessage(event.EventDataType, event.EventCategory, event.EventName ?? ''); - - if (this.placementEventMappingLookup[String(hashedEvent)]) { - const mappedValue = this.placementEventMappingLookup[String(hashedEvent)]; - mp().Rokt.setLocalSessionAttribute?.(mappedValue, true); - } + return 'Successfully sent to forwarder: ' + name; } public setExtensionData(partnerExtensionData: Record): void { @@ -1081,36 +1092,43 @@ class RoktKit { window.Rokt!.setExtensionData(partnerExtensionData); } - public setUserAttribute(key: string, value: unknown): void { + public setUserAttribute(key: string, value: unknown): string { this.userAttributes[key] = value; - this.sendEventStream( - this.buildIdentityEvent('set_user_attributes', this.filters.filteredUser ?? ({} as FilteredUser)), - ); + return 'Successfully set user attribute for forwarder: ' + name; } - public removeUserAttribute(key: string): void { + public removeUserAttribute(key: string): string { delete this.userAttributes[key]; + return 'Successfully removed user attribute for forwarder: ' + name; } - public onUserIdentified(filteredUser: FilteredUser): void { + public onUserIdentified(user: IMParticleUser): string { + const filteredUser = user as FilteredUser; this.filters.filteredUser = filteredUser; - this.userAttributes = filteredUser.getAllUserAttributes(); - this.sendEventStream(this.buildIdentityEvent('identify', filteredUser)); + this.userAttributes = user.getAllUserAttributes(); + this.pendingIdentityEvents.push(this.buildIdentityEvent('identify', filteredUser)); + return 'Successfully called onUserIdentified for forwarder: ' + name; } - public onLoginComplete(filteredUser: FilteredUser): void { - this.userAttributes = filteredUser.getAllUserAttributes(); - this.sendEventStream(this.buildIdentityEvent('login', filteredUser)); + public onLoginComplete(user: IMParticleUser, _filteredIdentityRequest: unknown): string { + const filteredUser = user as FilteredUser; + this.userAttributes = user.getAllUserAttributes(); + this.pendingIdentityEvents.push(this.buildIdentityEvent('login', filteredUser)); + return 'Successfully called onLoginComplete for forwarder: ' + name; } - public onLogoutComplete(filteredUser: FilteredUser): void { - this.userAttributes = filteredUser.getAllUserAttributes(); - this.sendEventStream(this.buildIdentityEvent('logout', filteredUser)); + public onLogoutComplete(user: IMParticleUser, _filteredIdentityRequest: unknown): string { + const filteredUser = user as FilteredUser; + this.userAttributes = user.getAllUserAttributes(); + this.pendingIdentityEvents.push(this.buildIdentityEvent('logout', filteredUser)); + return 'Successfully called onLogoutComplete for forwarder: ' + name; } - public onModifyComplete(filteredUser: FilteredUser): void { - this.userAttributes = filteredUser.getAllUserAttributes(); - this.sendEventStream(this.buildIdentityEvent('modify_user', filteredUser)); + public onModifyComplete(user: IMParticleUser, _filteredIdentityRequest: unknown): string { + const filteredUser = user as FilteredUser; + this.userAttributes = user.getAllUserAttributes(); + this.pendingIdentityEvents.push(this.buildIdentityEvent('modify', filteredUser)); + return 'Successfully called onModifyComplete for forwarder: ' + name; } /** @@ -1123,10 +1141,7 @@ class RoktKit { const filters = this.filters || {}; const userAttributeFilters = (filters.userAttributeFilters as string[]) || []; const filteredUser = filters.filteredUser || null; - const mpid = - filteredUser && filteredUser.getMPID && typeof filteredUser.getMPID === 'function' - ? filteredUser.getMPID() - : null; + const mpid = filteredUser ? filteredUser.getMPID() : null; let filteredAttributes: Record; diff --git a/test/src/tests.spec.ts b/test/src/tests.spec.ts index 58569a5..379285d 100644 --- a/test/src/tests.spec.ts +++ b/test/src/tests.spec.ts @@ -1,6 +1,7 @@ import packageJson from '../../package.json'; const packageVersion = packageJson.version; import '../../src/Rokt-Kit'; +import { Batch } from '@mparticle/web-sdk/internal'; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -2931,11 +2932,6 @@ describe('Rokt Forwarder', () => { }, }; }); - afterEach(() => { - delete (window as any).Rokt.__event_stream__; - (window as any).mParticle.forwarder.eventStreamQueue = []; - }); - it('should set the user attribute', async () => { (window as any).mParticle.forwarder.setUserAttribute('test-attribute', 'test-value'); @@ -2943,42 +2939,6 @@ describe('Rokt Forwarder', () => { 'test-attribute': 'test-value', }); }); - - it('should send a set_user_attributes event to window.Rokt.__event_stream__', () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); - }; - - (window as any).mParticle.forwarder.onUserIdentified({ - getAllUserAttributes: function () { - return { 'user-attr': 'user-value' }; - }, - getMPID: function () { - return 'test-mpid'; - }, - getUserIdentities: function () { - return { userIdentities: { email: 'test@example.com' } }; - }, - }); - - receivedEvents.length = 0; // clear the identify event - - (window as any).mParticle.forwarder.setUserAttribute('new-attr', 'new-value'); - - expect(receivedEvents.length).toBe(1); - expect(receivedEvents[0].EventName).toBe('set_user_attributes'); - expect(receivedEvents[0].EventDataType).toBe(14); - expect(receivedEvents[0].MPID).toBe('test-mpid'); - expect(receivedEvents[0].SessionId).toBe('test-mp-session-id'); - }); - - it('should queue event when window.Rokt.__event_stream__ is not available', () => { - (window as any).mParticle.forwarder.setUserAttribute('queued-attr', 'queued-value'); - - expect((window as any).mParticle.forwarder.eventStreamQueue.length).toBe(1); - expect((window as any).mParticle.forwarder.eventStreamQueue[0].EventName).toBe('set_user_attributes'); - }); }); describe('#removeUserAttribute', () => { @@ -2992,18 +2952,6 @@ describe('Rokt Forwarder', () => { }); describe('#onUserIdentified', () => { - beforeEach(() => { - (window as any).mParticle.sessionManager = { - getSession: function () { - return 'test-mp-session-id'; - }, - }; - }); - afterEach(() => { - delete (window as any).Rokt.__event_stream__; - (window as any).mParticle.forwarder.eventStreamQueue = []; - }); - it('should set the filtered user and userAttributes', () => { (window as any).mParticle.forwarder.onUserIdentified({ getAllUserAttributes: function () { @@ -3022,283 +2970,43 @@ describe('Rokt Forwarder', () => { }); expect((window as any).mParticle.forwarder.filters.filteredUser.getMPID()).toBe('123'); }); - - it('should send a User Identified event to window.Rokt.__event_stream__', () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); - }; - - (window as any).mParticle.forwarder.onUserIdentified({ - getAllUserAttributes: function () { - return { 'user-attr': 'user-value' }; - }, - getMPID: function () { - return 'identified-mpid-123'; - }, - getUserIdentities: function () { - return { userIdentities: { email: 'jenny@example.com' } }; - }, - }); - - expect(receivedEvents.length).toBe(1); - expect(receivedEvents[0].EventName).toBe('identify'); - expect(receivedEvents[0].EventDataType).toBe(14); - expect(receivedEvents[0].MPID).toBe('identified-mpid-123'); - expect(receivedEvents[0].SessionId).toBe('test-mp-session-id'); - expect(receivedEvents[0].UserAttributes).toEqual({ - 'user-attr': 'user-value', - }); - expect(receivedEvents[0].UserIdentities).toEqual({ - email: 'jenny@example.com', - }); - }); - - it('should queue event when window.Rokt.__event_stream__ is not available', () => { - (window as any).mParticle.forwarder.onUserIdentified({ - getAllUserAttributes: function () { - return {}; - }, - getMPID: function () { - return '123'; - }, - getUserIdentities: function () { - return { userIdentities: {} }; - }, - }); - - expect((window as any).mParticle.forwarder.eventStreamQueue.length).toBe(1); - expect((window as any).mParticle.forwarder.eventStreamQueue[0].EventName).toBe('identify'); - }); }); describe('#onLoginComplete', () => { - beforeEach(() => { - (window as any).mParticle.sessionManager = { - getSession: function () { - return 'test-mp-session-id'; - }, - }; - }); - afterEach(() => { - delete (window as any).Rokt.__event_stream__; - (window as any).mParticle.forwarder.eventStreamQueue = []; - }); - it('should update userAttributes from the filtered user', () => { - (window as any).mParticle.forwarder.onLoginComplete({ - getAllUserAttributes: function () { - return { 'user-attr': 'user-value' }; - }, - }); - - expect((window as any).mParticle.forwarder.userAttributes).toEqual({ - 'user-attr': 'user-value', - }); - }); - - it('should send a User Login event to window.Rokt.__event_stream__', () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); - }; - (window as any).mParticle.forwarder.onLoginComplete({ getAllUserAttributes: function () { return { 'user-attr': 'user-value' }; }, getMPID: function () { - return 'login-mpid-123'; - }, - getUserIdentities: function () { - return { - userIdentities: { email: 'jenny@example.com' }, - }; + return '123'; }, }); - expect(receivedEvents.length).toBe(1); - expect(receivedEvents[0].EventName).toBe('login'); - expect(receivedEvents[0].EventDataType).toBe(14); - expect(receivedEvents[0].UserAttributes).toEqual({ + expect((window as any).mParticle.forwarder.userAttributes).toEqual({ 'user-attr': 'user-value', }); - expect(receivedEvents[0].MPID).toBe('login-mpid-123'); - expect(receivedEvents[0].SessionId).toBe('test-mp-session-id'); - expect(receivedEvents[0].UserIdentities).toEqual({ - email: 'jenny@example.com', - }); - }); - - it('should include null MPID and null UserIdentities when filteredUser has no getMPID or getUserIdentities', () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); - }; - - (window as any).mParticle.forwarder.onLoginComplete({ - getAllUserAttributes: function () { - return {}; - }, - }); - - expect(receivedEvents.length).toBe(1); - expect(receivedEvents[0].MPID).toBeNull(); - expect(receivedEvents[0].UserIdentities).toBeNull(); - }); - - it('should include null SessionId when sessionManager is unavailable', () => { - delete (window as any).mParticle.sessionManager; - - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); - }; - - (window as any).mParticle.forwarder.onLoginComplete({ - getAllUserAttributes: function () { - return {}; - }, - getMPID: function () { - return 'some-mpid'; - }, - }); - - expect(receivedEvents.length).toBe(1); - expect(receivedEvents[0].SessionId).toBeNull(); - }); - - it('should queue event when window.Rokt.__event_stream__ is not available', () => { - (window as any).mParticle.forwarder.onLoginComplete({ - getAllUserAttributes: function () { - return {}; - }, - }); - - expect((window as any).mParticle.forwarder.eventStreamQueue.length).toBe(1); - expect((window as any).mParticle.forwarder.eventStreamQueue[0].EventName).toBe('login'); - expect((window as any).mParticle.forwarder.eventStreamQueue[0].EventDataType).toBe(14); - }); - - it('should queue event when window.Rokt is undefined', () => { - const savedRokt = (window as any).Rokt; - (window as any).Rokt = undefined; - - expect(() => { - (window as any).mParticle.forwarder.onLoginComplete({ - getAllUserAttributes: function () { - return {}; - }, - }); - }).not.toThrow(); - - expect((window as any).mParticle.forwarder.eventStreamQueue.length).toBe(1); - expect((window as any).mParticle.forwarder.eventStreamQueue[0].EventName).toBe('login'); - - (window as any).Rokt = savedRokt; }); }); describe('#onLogoutComplete', () => { - beforeEach(() => { - (window as any).mParticle.sessionManager = { - getSession: function () { - return 'test-mp-session-id'; - }, - }; - }); - afterEach(() => { - delete (window as any).Rokt.__event_stream__; - (window as any).mParticle.forwarder.eventStreamQueue = []; - }); - it('should update userAttributes from the filtered user', () => { (window as any).mParticle.forwarder.onLogoutComplete({ getAllUserAttributes: function () { return { 'remaining-attr': 'some-value' }; }, - }); - - expect((window as any).mParticle.forwarder.userAttributes).toEqual({ - 'remaining-attr': 'some-value', - }); - }); - - it('should send a User Logout event to window.Rokt.__event_stream__', () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); - }; - - (window as any).mParticle.forwarder.onLogoutComplete({ - getAllUserAttributes: function () { - return {}; - }, getMPID: function () { - return 'logout-mpid-456'; - }, - getUserIdentities: function () { - return { - userIdentities: { customerid: 'cust-789' }, - }; + return '123'; }, }); - expect(receivedEvents.length).toBe(1); - expect(receivedEvents[0].EventName).toBe('logout'); - expect(receivedEvents[0].EventDataType).toBe(14); - expect(receivedEvents[0].UserAttributes).toEqual({}); - expect(receivedEvents[0].MPID).toBe('logout-mpid-456'); - expect(receivedEvents[0].SessionId).toBe('test-mp-session-id'); - expect(receivedEvents[0].UserIdentities).toEqual({ - customerid: 'cust-789', - }); - }); - - it('should queue event when window.Rokt.__event_stream__ is not available', () => { - (window as any).mParticle.forwarder.onLogoutComplete({ - getAllUserAttributes: function () { - return {}; - }, + expect((window as any).mParticle.forwarder.userAttributes).toEqual({ + 'remaining-attr': 'some-value', }); - - expect((window as any).mParticle.forwarder.eventStreamQueue.length).toBe(1); - expect((window as any).mParticle.forwarder.eventStreamQueue[0].EventName).toBe('logout'); - expect((window as any).mParticle.forwarder.eventStreamQueue[0].EventDataType).toBe(14); - }); - - it('should queue event when window.Rokt is undefined', () => { - const savedRokt = (window as any).Rokt; - (window as any).Rokt = undefined; - - expect(() => { - (window as any).mParticle.forwarder.onLogoutComplete({ - getAllUserAttributes: function () { - return {}; - }, - }); - }).not.toThrow(); - - expect((window as any).mParticle.forwarder.eventStreamQueue.length).toBe(1); - expect((window as any).mParticle.forwarder.eventStreamQueue[0].EventName).toBe('logout'); - - (window as any).Rokt = savedRokt; }); }); describe('#onModifyComplete', () => { - beforeEach(() => { - (window as any).mParticle.sessionManager = { - getSession: function () { - return 'test-mp-session-id'; - }, - }; - }); - afterEach(() => { - delete (window as any).Rokt.__event_stream__; - (window as any).mParticle.forwarder.eventStreamQueue = []; - }); - it('should update userAttributes from the filtered user', () => { (window as any).mParticle.forwarder.onModifyComplete({ getAllUserAttributes: function () { @@ -3316,81 +3024,6 @@ describe('Rokt Forwarder', () => { 'modified-attr': 'modified-value', }); }); - - it('should send a User Modified event to window.Rokt.__event_stream__', () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); - }; - - (window as any).mParticle.forwarder.onModifyComplete({ - getAllUserAttributes: function () { - return { 'modified-attr': 'modified-value' }; - }, - getMPID: function () { - return 'modify-mpid-789'; - }, - getUserIdentities: function () { - return { - userIdentities: { email: 'modified@example.com' }, - }; - }, - }); - - expect(receivedEvents.length).toBe(1); - expect(receivedEvents[0].EventName).toBe('modify_user'); - expect(receivedEvents[0].EventDataType).toBe(14); - expect(receivedEvents[0].MPID).toBe('modify-mpid-789'); - expect(receivedEvents[0].SessionId).toBe('test-mp-session-id'); - expect(receivedEvents[0].UserAttributes).toEqual({ - 'modified-attr': 'modified-value', - }); - expect(receivedEvents[0].UserIdentities).toEqual({ - email: 'modified@example.com', - }); - }); - - it('should queue event when window.Rokt.__event_stream__ is not available', () => { - (window as any).mParticle.forwarder.onModifyComplete({ - getAllUserAttributes: function () { - return {}; - }, - getMPID: function () { - return '123'; - }, - getUserIdentities: function () { - return { userIdentities: {} }; - }, - }); - - expect((window as any).mParticle.forwarder.eventStreamQueue.length).toBe(1); - expect((window as any).mParticle.forwarder.eventStreamQueue[0].EventName).toBe('modify_user'); - expect((window as any).mParticle.forwarder.eventStreamQueue[0].EventDataType).toBe(14); - }); - - it('should queue event when window.Rokt is undefined', () => { - const savedRokt = (window as any).Rokt; - (window as any).Rokt = undefined; - - expect(() => { - (window as any).mParticle.forwarder.onModifyComplete({ - getAllUserAttributes: function () { - return {}; - }, - getMPID: function () { - return '123'; - }, - getUserIdentities: function () { - return { userIdentities: {} }; - }, - }); - }).not.toThrow(); - - expect((window as any).mParticle.forwarder.eventStreamQueue.length).toBe(1); - expect((window as any).mParticle.forwarder.eventStreamQueue[0].EventName).toBe('modify_user'); - - (window as any).Rokt = savedRokt; - }); }); describe('#fetchOptimizely', () => { @@ -4705,67 +4338,15 @@ describe('Rokt Forwarder', () => { 'foo-mapped-flag': true, }); }); - - it('should add the event to the event queue if the kit is not initialized', async () => { - await (window as any).mParticle.forwarder.init( - { - accountId: '123456', - }, - reportService.cb, - true, - null, - {}, - ); - - (window as any).mParticle.forwarder.process({ - EventName: 'Video Watched A', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }); - - expect((window as any).mParticle.forwarder.eventQueue).toEqual([ - { - EventName: 'Video Watched A', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }, - ]); - }); - - it('should process queued events once the kit is ready', async () => { - await (window as any).mParticle.forwarder.init( - { - accountId: '123456', - }, - reportService.cb, - true, - null, - {}, - ); - - (window as any).mParticle.forwarder.process({ - EventName: 'Video Watched B', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }); - - expect((window as any).mParticle.forwarder.eventQueue).toEqual([ - { - EventName: 'Video Watched B', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }, - ]); - - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - - expect((window as any).mParticle.forwarder.eventQueue).toEqual([]); - }); }); - describe('#_sendEventStream', () => { + describe('#processBatch', () => { + let mockBatch: Batch; + beforeEach(() => { - (window as any).mParticle.forwarder.eventStreamQueue = []; + (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({ @@ -4805,347 +4386,266 @@ describe('Rokt Forwarder', () => { }, }, }; + + 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.__event_stream__; - (window as any).mParticle.forwarder.eventQueue = []; - (window as any).mParticle.forwarder.eventStreamQueue = []; + 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 forward event to window.Rokt.__event_stream__ when available', async () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); + 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); - const testEvent = { - EventName: 'Test Event', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }; - - (window as any).mParticle.forwarder.process(testEvent); + (window as any).mParticle.forwarder.processBatch(mockBatch); - expect(receivedEvents.length).toBe(1); - expect(receivedEvents[0].EventName).toBe('Test Event'); - expect(receivedEvents[0].EventCategory).toBe(EventType.Other); - expect(receivedEvents[0].EventDataType).toBe(MessageType.PageEvent); - expect(receivedEvents[0].UserAttributes).toEqual({}); + 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 queue event when window.Rokt.__event_stream__ is not defined', async () => { - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); + 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); - const testEvent = { - EventName: 'Test Event', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }; + expect((window as any).mParticle.forwarder.pendingIdentityEvents.length).toBe(0); - expect(() => { - (window as any).mParticle.forwarder.process(testEvent); - }).not.toThrow(); + (window as any).mParticle.forwarder.processBatch(mockBatch); - expect((window as any).mParticle.forwarder.eventStreamQueue.length).toBe(1); - expect((window as any).mParticle.forwarder.eventStreamQueue[0]).toEqual(testEvent); + 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 event 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; - (window as any).Rokt = undefined; - - const testEvent = { - EventName: 'Test Event', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }; + 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.process(testEvent); + (window as any).mParticle.forwarder.processBatch(mockBatch); }).not.toThrow(); - expect((window as any).mParticle.forwarder.eventStreamQueue.length).toBe(1); - expect((window as any).mParticle.forwarder.eventStreamQueue[0]).toEqual(testEvent); - - (window as any).Rokt = savedRokt; + expect((window as any).mParticle.forwarder.batchQueue.length).toBe(1); + expect((window as any).mParticle.forwarder.batchQueue[0]).toEqual(mockBatch); }); - it('should forward event with attributes to the event stream', async () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); + it('should flush batchQueue when kit becomes 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, {}); + (window as any).mParticle.forwarder.isInitialized = false; + (window as any).mParticle.forwarder.launcher = null; + (window as any).mParticle.forwarder.processBatch(mockBatch); - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); + expect((window as any).mParticle.forwarder.batchQueue.length).toBe(1); - const testEvent = { - EventName: 'Purchase', - EventCategory: EventType.Transaction, - EventDataType: MessageType.PageEvent, - EventAttributes: { - product: 'shoes', - price: '49.99', - }, - }; + await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - (window as any).mParticle.forwarder.process(testEvent); + await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - expect(receivedEvents.length).toBe(1); - expect(receivedEvents[0].EventAttributes).toEqual({ - product: 'shoes', - price: '49.99', - }); + 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 forward multiple events to the event stream', async () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); - }; - + 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); - (window as any).mParticle.forwarder.process({ - EventName: 'Event A', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }); - - (window as any).mParticle.forwarder.process({ - EventName: 'Event B', - EventCategory: EventType.Navigation, - EventDataType: MessageType.PageView, - }); + expect(() => { + (window as any).mParticle.forwarder.processBatch(mockBatch); + }).not.toThrow(); - expect(receivedEvents.length).toBe(2); - expect(receivedEvents[0].EventName).toBe('Event A'); - expect(receivedEvents[1].EventName).toBe('Event B'); + expect((window as any).mParticle.forwarder.batchStreamQueue.length).toBe(1); + expect((window as any).mParticle.forwarder.batchStreamQueue[0]).toEqual(mockBatch); }); - it('should forward queued events to the event stream after init', async () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); - }; + 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, {}); - (window as any).mParticle.forwarder.process({ - EventName: 'Queued Event', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }); + await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); - expect(receivedEvents.length).toBe(0); - expect((window as any).mParticle.forwarder.eventQueue.length).toBe(1); + const savedRokt = (window as any).Rokt; + delete (window as any).Rokt; - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); + expect(() => { + (window as any).mParticle.forwarder.processBatch(mockBatch); + }).not.toThrow(); - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); + expect((window as any).mParticle.forwarder.batchStreamQueue.length).toBe(1); + expect((window as any).mParticle.forwarder.batchStreamQueue[0]).toEqual(mockBatch); - expect(receivedEvents.length).toBe(1); - expect(receivedEvents[0].EventName).toBe('Queued Event'); + (window as any).Rokt = savedRokt; }); - it('should still process placement event mapping alongside event stream', async () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); - }; - - const placementEventMapping = JSON.stringify([ - { - jsmap: 'hashed-<48Video Watched>-value', - map: '123466', - maptype: 'EventClass.Id', - value: 'foo-mapped-flag', - }, - ]); + it('should flush batchStreamQueue before sending the next batch', async () => { + const receivedBatches: any[] = []; - await (window as any).mParticle.forwarder.init( - { - accountId: '123456', - placementEventMapping: placementEventMapping, - }, - reportService.cb, - true, - null, - {}, - ); + 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.process({ - EventName: 'Video Watched', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }); + const batchA = { mpid: 'mpid-A', events: [], user_attributes: {} }; + const batchB = { mpid: 'mpid-B', events: [], user_attributes: {} }; + const batchC = { mpid: 'mpid-C', events: [], user_attributes: {} }; - expect(receivedEvents.length).toBe(1); - expect(receivedEvents[0].EventName).toBe('Video Watched'); + (window as any).mParticle.forwarder.processBatch(batchA); + (window as any).mParticle.forwarder.processBatch(batchB); - expect((window as any).mParticle._Store.localSessionAttributes).toEqual({ - 'foo-mapped-flag': true, - }); - }); + expect((window as any).mParticle.forwarder.batchStreamQueue.length).toBe(2); - it('should enrich event with Kit userAttributes before sending to event stream', async () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); + (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, {}); + (window as any).mParticle.forwarder.processBatch(batchC); - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); + 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); + }); - (window as any).mParticle.forwarder.userAttributes = { - firstName: 'John', - lastName: 'Doe', + it('should add an identity event to pendingIdentityEvents on onLoginComplete', () => { + const mockUser = { + getMPID: () => '123', + getAllUserAttributes: () => ({}), + getUserIdentities: () => ({ userIdentities: {} }), }; - (window as any).mParticle.forwarder.process({ - EventName: 'Test Event', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }); + (window as any).mParticle.forwarder.onLoginComplete(mockUser, null); - expect(receivedEvents.length).toBe(1); - expect(receivedEvents[0].UserAttributes).toEqual({ - firstName: 'John', - lastName: 'Doe', - }); + const pending = (window as any).mParticle.forwarder.pendingIdentityEvents; + expect(pending.length).toBe(1); + expect(pending[0].EventName).toBe('login'); + expect(pending[0].EventDataType).toBe(14); }); - it('should override event UserAttributes with Kit userAttributes', async () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); + it('should add an identity event to pendingIdentityEvents on onLogoutComplete', () => { + const mockUser = { + getMPID: () => '123', + getAllUserAttributes: () => ({}), + getUserIdentities: () => ({ userIdentities: {} }), }; - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); + (window as any).mParticle.forwarder.onLogoutComplete(mockUser, null); - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); + const pending = (window as any).mParticle.forwarder.pendingIdentityEvents; + expect(pending.length).toBe(1); + expect(pending[0].EventName).toBe('logout'); + expect(pending[0].EventDataType).toBe(14); + }); - (window as any).mParticle.forwarder.userAttributes = { - firstName: 'Jane', + it('should add identity events to pendingIdentityEvents on onModifyComplete and onUserIdentified', () => { + const mockUser = { + getMPID: () => '42', + getAllUserAttributes: () => ({}), + getUserIdentities: () => ({ userIdentities: {} }), }; - (window as any).mParticle.forwarder.process({ - EventName: 'Test Event', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - UserAttributes: { - firstName: 'Stale', - obsoleteAttr: 'should-not-appear', - }, - }); + (window as any).mParticle.forwarder.onModifyComplete(mockUser, null); + (window as any).mParticle.forwarder.onUserIdentified(mockUser); - expect(receivedEvents.length).toBe(1); - expect(receivedEvents[0].UserAttributes).toEqual({ - firstName: 'Jane', - }); + const pending = (window as any).mParticle.forwarder.pendingIdentityEvents; + expect(pending.length).toBe(2); + expect(pending[0].EventName).toBe('modify'); + expect(pending[1].EventName).toBe('identify'); }); - it('should not mutate the original event when enriching with userAttributes', async () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); + 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); - (window as any).mParticle.forwarder.userAttributes = { - firstName: 'John', + const mockUser = { + getMPID: () => '123', + getAllUserAttributes: () => ({}), + getUserIdentities: () => ({ userIdentities: {} }), }; - const originalEvent = { - EventName: 'Test Event', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }; + (window as any).mParticle.forwarder.onLoginComplete(mockUser, null); + expect((window as any).mParticle.forwarder.pendingIdentityEvents.length).toBe(1); - (window as any).mParticle.forwarder.process(originalEvent); + (window as any).mParticle.forwarder.processBatch(mockBatch); - expect(originalEvent).not.toHaveProperty('UserAttributes'); - expect(receivedEvents[0].UserAttributes).toEqual({ - firstName: 'John', - }); + 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].EventName).toBe('login'); + expect(receivedBatches[0].events[1].EventDataType).toBe(14); + // Queue should be cleared after flush + expect((window as any).mParticle.forwarder.pendingIdentityEvents.length).toBe(0); }); - it('should send empty UserAttributes when Kit has no userAttributes', async () => { - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); + 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); }; - await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {}); - - await waitForCondition(() => (window as any).mParticle.Rokt.attachKitCalled); + // Queue a batch before the kit initialises + (window as any).mParticle.forwarder.isInitialized = false; + (window as any).mParticle.forwarder.launcher = null; - (window as any).mParticle.forwarder.userAttributes = {}; + const mockUser = { + getMPID: () => '123', + getAllUserAttributes: () => ({}), + getUserIdentities: () => ({ userIdentities: {} }), + }; - (window as any).mParticle.forwarder.process({ - EventName: 'Test Event', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }); + // Identity callback fires before kit is ready + (window as any).mParticle.forwarder.onLoginComplete(mockUser, null); + (window as any).mParticle.forwarder.processBatch(mockBatch); - expect(receivedEvents.length).toBe(1); - expect(receivedEvents[0].UserAttributes).toEqual({}); - }); + expect((window as any).mParticle.forwarder.batchQueue.length).toBe(1); + expect((window as any).mParticle.forwarder.pendingIdentityEvents.length).toBe(1); - it('should flush queued events in FIFO order when __event_stream__ becomes available', async () => { 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.process({ - EventName: 'Event A', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }); - (window as any).mParticle.forwarder.process({ - EventName: 'Event B', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }); - - expect((window as any).mParticle.forwarder.eventStreamQueue.length).toBe(2); - - const receivedEvents: any[] = []; - (window as any).Rokt.__event_stream__ = function (event: any) { - receivedEvents.push(event); - }; - - (window as any).mParticle.forwarder.process({ - EventName: 'Event C', - EventCategory: EventType.Other, - EventDataType: MessageType.PageEvent, - }); - - expect(receivedEvents.length).toBe(3); - expect(receivedEvents[0].EventName).toBe('Event A'); - expect(receivedEvents[1].EventName).toBe('Event B'); - expect(receivedEvents[2].EventName).toBe('Event C'); - expect((window as any).mParticle.forwarder.eventStreamQueue.length).toBe(0); + // 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].EventDataType).toBe(14); + expect((window as any).mParticle.forwarder.pendingIdentityEvents.length).toBe(0); + expect((window as any).mParticle.forwarder.batchQueue.length).toBe(0); }); });