From 5dbfce4dbc2fe264d09a10620242f8d8b447603a Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Tue, 11 Mar 2025 14:34:54 -0400 Subject: [PATCH 1/6] feat: Add Rokt Wrapper --- src/mp-instance.ts | 3 + src/mparticle-instance-manager.ts | 3 + src/roktManager.ts | 83 +++++++++++++ src/sdkRuntimeModels.ts | 2 + test/jest/roktManager.spec.ts | 119 +++++++++++++++++++ test/src/tests-mparticle-instance-manager.ts | 1 + 6 files changed, 211 insertions(+) create mode 100644 src/roktManager.ts create mode 100644 test/jest/roktManager.spec.ts diff --git a/src/mp-instance.ts b/src/mp-instance.ts index a7688542c..ff74a52d9 100644 --- a/src/mp-instance.ts +++ b/src/mp-instance.ts @@ -49,6 +49,7 @@ import { IECommerce } from './ecommerce.interfaces'; import { INativeSdkHelpers } from './nativeSdkHelpers.interfaces'; import { IPersistence } from './persistence.interfaces'; import ForegroundTimer from './foregroundTimeTracker'; +import RoktManager from './roktManager'; export interface IErrorLogMessage { message?: string; @@ -80,6 +81,7 @@ export interface IMParticleWebSDKInstance extends MParticleWebSDK { _IntegrationCapture: IntegrationCapture; _NativeSdkHelpers: INativeSdkHelpers; _Persistence: IPersistence; + _RoktManager: RoktManager; _SessionManager: ISessionManager; _ServerModel: IServerModel; _Store: IStore; @@ -126,6 +128,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan forwarderConstructors: [], }; this._IntegrationCapture = new IntegrationCapture(); + this._RoktManager = new RoktManager(); // required for forwarders once they reference the mparticle instance this.IdentityType = IdentityType; diff --git a/src/mparticle-instance-manager.ts b/src/mparticle-instance-manager.ts index 777baa1eb..d69007ec2 100644 --- a/src/mparticle-instance-manager.ts +++ b/src/mparticle-instance-manager.ts @@ -103,6 +103,9 @@ function mParticleInstanceManager(this: IMParticleInstanceManager) { return client; } }; + + this.Rokt = self.getInstance()._RoktManager; + this.getDeviceId = function() { return self.getInstance().getDeviceId(); }; diff --git a/src/roktManager.ts b/src/roktManager.ts new file mode 100644 index 000000000..db08ab927 --- /dev/null +++ b/src/roktManager.ts @@ -0,0 +1,83 @@ +export interface IRoktPartnerAttributes { + [key: string]: string | number | boolean | undefined | null; +} + +export interface ISelectPlacementsOptions { + attributes: IRoktPartnerAttributes; + identifier?: string; +} + +export interface ISelection { + placementId?: string; + status?: string; + error?: string; +} + +export interface IRoktLauncher { + selectPlacements: (options: ISelectPlacementsOptions) => Promise; +} + +export interface IRoktMessage { + methodName: string; + payload: any; +} + +export default class RoktManager { + private launcher: IRoktLauncher | null = null; + private messageQueue: IRoktMessage[] = []; + + constructor() { + this.launcher = null; + } + + public attachLauncher(launcher: IRoktLauncher): Promise { + return new Promise((resolve) => { + if (!launcher) { + this.queueMessage({ + methodName: 'attachLauncher', + payload: launcher, + }); + } else { + this.setLauncher(launcher); + this.processMessageQueue(); + } + resolve(); + }); + } + + public selectPlacements(options: ISelectPlacementsOptions): Promise { + if (!this.launcher) { + this.queueMessage({ + methodName: 'selectPlacements', + payload: options, + }); + return Promise.resolve({}); + } + + try { + return this.launcher.selectPlacements(options); + } catch (error) { + return Promise.reject(error instanceof Error ? error : new Error('Unknown error occurred')); + } + } + + private processMessageQueue(): void { + if (this.messageQueue.length > 0) { + this.messageQueue.forEach(async (message) => { + if (this.launcher && message.methodName in this.launcher) { + await (this.launcher[message.methodName] as Function)(message.payload); + } + }); + this.messageQueue = []; + } + } + + + private queueMessage(message: IRoktMessage): void { + this.messageQueue.push(message); + } + + private setLauncher(launcher: IRoktLauncher): void { + this.launcher = launcher; + } +} diff --git a/src/sdkRuntimeModels.ts b/src/sdkRuntimeModels.ts index 8145bdc27..822364ed1 100644 --- a/src/sdkRuntimeModels.ts +++ b/src/sdkRuntimeModels.ts @@ -39,6 +39,7 @@ import _BatchValidator from './mockBatchCreator'; import { SDKECommerceAPI } from './ecommerce.interfaces'; import { IErrorLogMessage, IMParticleWebSDKInstance, IntegrationDelays } from './mp-instance'; import Constants from './constants'; +import RoktManager from './roktManager'; // TODO: Resolve this with version in @mparticle/web-sdk export type SDKEventCustomFlags = Dictionary; @@ -242,6 +243,7 @@ export interface IMParticleInstanceManager extends MParticleWebSDK { config: SDKInitConfig; isIOS?: boolean; MPSideloadedKit: typeof MPSideloadedKit; + Rokt: RoktManager; // https://go.mparticle.com/work/SQDSDKS-7060 sessionManager: Pick; Store: IStore; diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts new file mode 100644 index 000000000..caa2ac2b2 --- /dev/null +++ b/test/jest/roktManager.spec.ts @@ -0,0 +1,119 @@ +import RoktManager, { IRoktLauncher, ISelectPlacementsOptions } from "../../src/roktManager"; + +describe('RoktManager', () => { + let roktManager: RoktManager; + + beforeEach(() => { + roktManager = new RoktManager(); + }); + + describe('constructor', () => { + it('should be initialized', () => { + expect(roktManager).toBeDefined(); + }); + it('should have a null launcher', () => { + expect(roktManager['launcher']).toBeNull(); + }); + }); + + describe('#attachLauncher', () => { + it('should attach a launcher', () => { + const launcher = {} as IRoktLauncher; + roktManager.attachLauncher(launcher); + expect(roktManager['launcher']).not.toBeNull(); + }); + + it('should queue the launcher method if no launcher is attached', () => { + roktManager.attachLauncher(null); + expect(roktManager['launcher']).toBeNull(); + expect(roktManager['messageQueue'].length).toBe(1); + expect(roktManager['messageQueue'][0].methodName).toBe('attachLauncher'); + expect(roktManager['messageQueue'][0].payload).toBe(null); + }); + + it('should process the message queue if a launcher is attached', () => { + const launcher = jest.fn() as unknown as IRoktLauncher; + + roktManager.attachLauncher(null); + expect(roktManager['launcher']).toBeNull(); + expect(roktManager['messageQueue'].length).toBe(1); + expect(roktManager['messageQueue'][0].methodName).toBe('attachLauncher'); + expect(roktManager['messageQueue'][0].payload).toBe(null); + + roktManager.attachLauncher(launcher); + expect(roktManager['launcher']).not.toBeNull(); + expect(roktManager['messageQueue'].length).toBe(0); + }); + }); + + describe('#selectPlacements', () => { + it('should call selectPlacements', () => { + const launcher = { + selectPlacements: jest.fn() + } as unknown as IRoktLauncher; + + roktManager.attachLauncher(launcher); + const options = { + attributes: {} + } as ISelectPlacementsOptions; + + roktManager.selectPlacements(options); + expect(launcher.selectPlacements).toHaveBeenCalledWith(options); + }); + + it('should call selectPlacements with any passed in attributes', () => { + const launcher = { + selectPlacements: jest.fn() + } as unknown as IRoktLauncher; + + roktManager.attachLauncher(launcher); + + const options: ISelectPlacementsOptions = { + attributes: { + age: 25, + score: 100.5, + isSubscribed: true, + isActive: false, + interests: 'sports,music,books' + } + }; + + roktManager.selectPlacements(options); + expect(launcher.selectPlacements).toHaveBeenCalledWith(options); + }); + + it('should queue the selectPlacements method if no launcher is attached', () => { + const options = { + attributes: {} + } as ISelectPlacementsOptions; + + roktManager.selectPlacements(options); + + expect(roktManager['launcher']).toBeNull(); + expect(roktManager['messageQueue'].length).toBe(1); + expect(roktManager['messageQueue'][0].methodName).toBe('selectPlacements'); + expect(roktManager['messageQueue'][0].payload).toBe(options); + }); + + it('should process queued selectPlacements calls once the launcher is attached', () => { + const launcher = { + selectPlacements: jest.fn() + } as unknown as IRoktLauncher; + + const options = { + attributes: {} + } as ISelectPlacementsOptions; + + roktManager.selectPlacements(options); + expect(roktManager['launcher']).toBeNull(); + expect(roktManager['messageQueue'].length).toBe(1); + expect(roktManager['messageQueue'][0].methodName).toBe('selectPlacements'); + expect(roktManager['messageQueue'][0].payload).toBe(options); + + roktManager.attachLauncher(launcher); + expect(roktManager['launcher']).not.toBeNull(); + expect(roktManager['messageQueue'].length).toBe(0); + expect(launcher.selectPlacements).toHaveBeenCalledWith(options); + }); + }); +}); \ No newline at end of file diff --git a/test/src/tests-mparticle-instance-manager.ts b/test/src/tests-mparticle-instance-manager.ts index e56dd2e3c..767199596 100644 --- a/test/src/tests-mparticle-instance-manager.ts +++ b/test/src/tests-mparticle-instance-manager.ts @@ -213,6 +213,7 @@ describe('mParticle instance manager', () => { 'isInitialized', 'getEnvironment', 'upload', + 'Rokt', ]); }); From 113cbc9949737b724cdbfac075d29def38fea16c Mon Sep 17 00:00:00 2001 From: Alex S <49695018+alexs-mparticle@users.noreply.github.com> Date: Thu, 13 Mar 2025 16:44:58 -0400 Subject: [PATCH 2/6] Apply suggestions from code review Co-authored-by: Robert Ing --- src/roktManager.ts | 2 ++ test/jest/roktManager.spec.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/roktManager.ts b/src/roktManager.ts index db08ab927..d27767bdd 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -1,7 +1,9 @@ +// https://docs.rokt.com/developers/integration-guides/web/library/attributes export interface IRoktPartnerAttributes { [key: string]: string | number | boolean | undefined | null; } +// https://docs.rokt.com/developers/integration-guides/web/library/select-placements-options export interface ISelectPlacementsOptions { attributes: IRoktPartnerAttributes; identifier?: string; diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts index caa2ac2b2..196536077 100644 --- a/test/jest/roktManager.spec.ts +++ b/test/jest/roktManager.spec.ts @@ -11,6 +11,7 @@ describe('RoktManager', () => { it('should be initialized', () => { expect(roktManager).toBeDefined(); }); + it('should have a null launcher', () => { expect(roktManager['launcher']).toBeNull(); }); From fc08d1dbf46aec0684d76570fc8ab15b10b42a40 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Fri, 14 Mar 2025 10:23:55 -0400 Subject: [PATCH 3/6] Address PR Comments --- src/roktManager.ts | 19 ++++++++++--------- test/jest/roktManager.spec.ts | 10 +++++----- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/roktManager.ts b/src/roktManager.ts index d27767bdd..8ed6cb204 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -4,19 +4,20 @@ export interface IRoktPartnerAttributes { } // https://docs.rokt.com/developers/integration-guides/web/library/select-placements-options -export interface ISelectPlacementsOptions { +export interface IRoktSelectPlacementsOptions { attributes: IRoktPartnerAttributes; identifier?: string; } -export interface ISelection { - placementId?: string; - status?: string; - error?: string; +interface IRoktPlacement {} + +export interface IRoktSelection { + close: () => void; + getPlacements: () => Promise; } export interface IRoktLauncher { - selectPlacements: (options: ISelectPlacementsOptions) => Promise; + selectPlacements: (options: IRoktSelectPlacementsOptions) => Promise; } export interface IRoktMessage { @@ -25,7 +26,7 @@ export interface IRoktMessage { } export default class RoktManager { - private launcher: IRoktLauncher | null = null; + public launcher: IRoktLauncher | null = null; private messageQueue: IRoktMessage[] = []; constructor() { @@ -47,13 +48,13 @@ export default class RoktManager { }); } - public selectPlacements(options: ISelectPlacementsOptions): Promise { + public selectPlacements(options: IRoktSelectPlacementsOptions): Promise { if (!this.launcher) { this.queueMessage({ methodName: 'selectPlacements', payload: options, }); - return Promise.resolve({}); + return Promise.resolve({} as IRoktSelection); } try { diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts index 196536077..51572f48d 100644 --- a/test/jest/roktManager.spec.ts +++ b/test/jest/roktManager.spec.ts @@ -1,4 +1,4 @@ -import RoktManager, { IRoktLauncher, ISelectPlacementsOptions } from "../../src/roktManager"; +import RoktManager, { IRoktLauncher, IRoktSelectPlacementsOptions } from "../../src/roktManager"; describe('RoktManager', () => { let roktManager: RoktManager; @@ -56,7 +56,7 @@ describe('RoktManager', () => { roktManager.attachLauncher(launcher); const options = { attributes: {} - } as ISelectPlacementsOptions; + } as IRoktSelectPlacementsOptions; roktManager.selectPlacements(options); expect(launcher.selectPlacements).toHaveBeenCalledWith(options); @@ -69,7 +69,7 @@ describe('RoktManager', () => { roktManager.attachLauncher(launcher); - const options: ISelectPlacementsOptions = { + const options: IRoktSelectPlacementsOptions = { attributes: { age: 25, score: 100.5, @@ -86,7 +86,7 @@ describe('RoktManager', () => { it('should queue the selectPlacements method if no launcher is attached', () => { const options = { attributes: {} - } as ISelectPlacementsOptions; + } as IRoktSelectPlacementsOptions; roktManager.selectPlacements(options); @@ -103,7 +103,7 @@ describe('RoktManager', () => { const options = { attributes: {} - } as ISelectPlacementsOptions; + } as IRoktSelectPlacementsOptions; roktManager.selectPlacements(options); expect(roktManager['launcher']).toBeNull(); From 50566b443f1e87159aa5946c295120775b1ffd30 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Fri, 14 Mar 2025 11:16:02 -0400 Subject: [PATCH 4/6] Address PR Comments --- test/jest/roktManager.spec.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts index 51572f48d..df3a909a1 100644 --- a/test/jest/roktManager.spec.ts +++ b/test/jest/roktManager.spec.ts @@ -48,10 +48,10 @@ describe('RoktManager', () => { }); describe('#selectPlacements', () => { - it('should call selectPlacements', () => { - const launcher = { + it('should call launcher.selectPlacements with empty attributes', () => { + const launcher: IRoktLauncher = { selectPlacements: jest.fn() - } as unknown as IRoktLauncher; + }; roktManager.attachLauncher(launcher); const options = { @@ -62,10 +62,10 @@ describe('RoktManager', () => { expect(launcher.selectPlacements).toHaveBeenCalledWith(options); }); - it('should call selectPlacements with any passed in attributes', () => { - const launcher = { + it('should call launcher.selectPlacements with passed in attributes', () => { + const launcher: IRoktLauncher = { selectPlacements: jest.fn() - } as unknown as IRoktLauncher; + }; roktManager.attachLauncher(launcher); @@ -97,9 +97,9 @@ describe('RoktManager', () => { }); it('should process queued selectPlacements calls once the launcher is attached', () => { - const launcher = { + const launcher: IRoktLauncher = { selectPlacements: jest.fn() - } as unknown as IRoktLauncher; + }; const options = { attributes: {} From b49cf13ea57e980ade8176e55f1e60fe6e2b4c16 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Fri, 14 Mar 2025 11:53:18 -0400 Subject: [PATCH 5/6] Address PR Comments --- src/roktManager.ts | 16 +++------------- test/jest/roktManager.spec.ts | 23 +++++++++-------------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/src/roktManager.ts b/src/roktManager.ts index 8ed6cb204..a00db28a8 100644 --- a/src/roktManager.ts +++ b/src/roktManager.ts @@ -33,19 +33,9 @@ export default class RoktManager { this.launcher = null; } - public attachLauncher(launcher: IRoktLauncher): Promise { - return new Promise((resolve) => { - if (!launcher) { - this.queueMessage({ - methodName: 'attachLauncher', - payload: launcher, - }); - } else { - this.setLauncher(launcher); - this.processMessageQueue(); - } - resolve(); - }); + public attachLauncher(launcher: IRoktLauncher): void { + this.setLauncher(launcher); + this.processMessageQueue(); } public selectPlacements(options: IRoktSelectPlacementsOptions): Promise { diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts index df3a909a1..fca41ddd1 100644 --- a/test/jest/roktManager.spec.ts +++ b/test/jest/roktManager.spec.ts @@ -24,26 +24,21 @@ describe('RoktManager', () => { expect(roktManager['launcher']).not.toBeNull(); }); - it('should queue the launcher method if no launcher is attached', () => { - roktManager.attachLauncher(null); - expect(roktManager['launcher']).toBeNull(); - expect(roktManager['messageQueue'].length).toBe(1); - expect(roktManager['messageQueue'][0].methodName).toBe('attachLauncher'); - expect(roktManager['messageQueue'][0].payload).toBe(null); - }); - it('should process the message queue if a launcher is attached', () => { - const launcher = jest.fn() as unknown as IRoktLauncher; + const launcher = { + selectPlacements: jest.fn() + } as unknown as IRoktLauncher; + + roktManager.selectPlacements({} as IRoktSelectPlacementsOptions); + roktManager.selectPlacements({} as IRoktSelectPlacementsOptions); + roktManager.selectPlacements({} as IRoktSelectPlacementsOptions); - roktManager.attachLauncher(null); - expect(roktManager['launcher']).toBeNull(); - expect(roktManager['messageQueue'].length).toBe(1); - expect(roktManager['messageQueue'][0].methodName).toBe('attachLauncher'); - expect(roktManager['messageQueue'][0].payload).toBe(null); + expect(roktManager['messageQueue'].length).toBe(3); roktManager.attachLauncher(launcher); expect(roktManager['launcher']).not.toBeNull(); expect(roktManager['messageQueue'].length).toBe(0); + expect(launcher.selectPlacements).toHaveBeenCalledTimes(3); }); }); From 6ba542a3e632c70bb6dc191642a8ad35ec7c7c95 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Fri, 14 Mar 2025 13:22:01 -0400 Subject: [PATCH 6/6] Address PR Comments --- test/jest/roktManager.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts index fca41ddd1..24bbb6d5d 100644 --- a/test/jest/roktManager.spec.ts +++ b/test/jest/roktManager.spec.ts @@ -25,9 +25,9 @@ describe('RoktManager', () => { }); it('should process the message queue if a launcher is attached', () => { - const launcher = { + const launcher: IRoktLauncher = { selectPlacements: jest.fn() - } as unknown as IRoktLauncher; + }; roktManager.selectPlacements({} as IRoktSelectPlacementsOptions); roktManager.selectPlacements({} as IRoktSelectPlacementsOptions);