diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index b1574fd..882f2f3 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -412,10 +412,40 @@ var constructor = function () { attributes: selectPlacementsAttributes, }); - // Log custom event for selectPlacements call - logSelectPlacementsEvent(selectPlacementsAttributes); + var selection = self.launcher.selectPlacements(selectPlacementsOptions); + + // After selection resolves, sync the Rokt session ID back to mParticle + // as an integration attribute so server-side integrations can link events. + // We log the custom event AFTER setting the attribute because + // setIntegrationAttribute alone doesn't fire a network request — + // if the user closes the page before another event fires, the server + // would never receive the session ID. + if (selection && typeof selection.then === 'function') { + selection + .then(function (sel) { + if (sel && sel.context && sel.context.sessionId) { + sel.context.sessionId + .then(function (sessionId) { + _setRoktSessionId(sessionId); + logSelectPlacementsEvent( + selectPlacementsAttributes + ); + }) + .catch(function () { + logSelectPlacementsEvent( + selectPlacementsAttributes + ); + }); + } else { + logSelectPlacementsEvent(selectPlacementsAttributes); + } + }) + .catch(function () { + logSelectPlacementsEvent(selectPlacementsAttributes); + }); + } - return self.launcher.selectPlacements(selectPlacementsOptions); + return selection; } /** @@ -525,6 +555,25 @@ var constructor = function () { } } + function _setRoktSessionId(sessionId) { + if (!sessionId || typeof sessionId !== 'string') { + return; + } + try { + var mpInstance = window.mParticle.getInstance(); + if ( + mpInstance && + typeof mpInstance.setIntegrationAttribute === 'function' + ) { + mpInstance.setIntegrationAttribute(moduleId, { + roktSessionId: sessionId, + }); + } + } catch (e) { + // Best effort — never let this break the partner page + } + } + function onUserIdentified(filteredUser) { self.filters.filteredUser = filteredUser; self.userAttributes = filteredUser.getAllUserAttributes(); diff --git a/test/src/tests.js b/test/src/tests.js index 2d05873..03f5780 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -2617,6 +2617,19 @@ describe('Rokt Forwarder', () => { describe('#logSelectPlacementsEvent', () => { it('should log a custom event', async () => { + window.Rokt.createLauncher = async function () { + return Promise.resolve({ + selectPlacements: function () { + return Promise.resolve({ + context: { + sessionId: + Promise.resolve('rokt-session-abc'), + }, + }); + }, + }); + }; + await window.mParticle.forwarder.init( { accountId: '123456', @@ -2629,6 +2642,10 @@ describe('Rokt Forwarder', () => { } ); + await waitForCondition( + () => window.mParticle.forwarder.isInitialized + ); + await window.mParticle.forwarder.selectPlacements({ identifier: 'test-placement', attributes: { @@ -2636,6 +2653,8 @@ describe('Rokt Forwarder', () => { }, }); + await waitForCondition(() => mParticle.loggedEvents.length > 0); + mParticle.loggedEvents.length.should.equal(1); mParticle.loggedEvents[0].eventName.should.equal( 'selectPlacements' @@ -2648,6 +2667,19 @@ describe('Rokt Forwarder', () => { }); it('should include merged user attributes, identities, and mpid', async () => { + window.Rokt.createLauncher = async function () { + return Promise.resolve({ + selectPlacements: function () { + return Promise.resolve({ + context: { + sessionId: + Promise.resolve('rokt-session-abc'), + }, + }); + }, + }); + }; + await window.mParticle.forwarder.init( { accountId: '123456', @@ -2660,6 +2692,10 @@ describe('Rokt Forwarder', () => { } ); + await waitForCondition( + () => window.mParticle.forwarder.isInitialized + ); + await window.mParticle.forwarder.selectPlacements({ identifier: 'test-placement', attributes: { @@ -2667,6 +2703,8 @@ describe('Rokt Forwarder', () => { }, }); + await waitForCondition(() => mParticle.loggedEvents.length > 0); + const eventAttributes = mParticle.loggedEvents[0].eventAttributes; @@ -2679,6 +2717,140 @@ describe('Rokt Forwarder', () => { ); }); + it('should log event when sessionId promise rejects', async () => { + window.Rokt.createLauncher = async function () { + return Promise.resolve({ + selectPlacements: function () { + return Promise.resolve({ + context: { + sessionId: Promise.reject( + new Error('session id failed') + ), + }, + }); + }, + }); + }; + + await window.mParticle.forwarder.init( + { + accountId: '123456', + }, + reportService.cb, + true, + null, + { + 'cached-user-attr': 'cached-value', + } + ); + + await waitForCondition( + () => window.mParticle.forwarder.isInitialized + ); + + await window.mParticle.forwarder.selectPlacements({ + identifier: 'test-placement', + attributes: { + 'new-attr': 'new-value', + }, + }); + + await waitForCondition(() => mParticle.loggedEvents.length > 0); + + mParticle.loggedEvents.length.should.equal(1); + mParticle.loggedEvents[0].eventName.should.equal( + 'selectPlacements' + ); + }); + + it('should log event when selection has no sessionId', async () => { + window.Rokt.createLauncher = async function () { + return Promise.resolve({ + selectPlacements: function () { + return Promise.resolve({ + context: {}, + }); + }, + }); + }; + + await window.mParticle.forwarder.init( + { + accountId: '123456', + }, + reportService.cb, + true, + null, + { + 'cached-user-attr': 'cached-value', + } + ); + + await waitForCondition( + () => window.mParticle.forwarder.isInitialized + ); + + await window.mParticle.forwarder.selectPlacements({ + identifier: 'test-placement', + attributes: { + 'new-attr': 'new-value', + }, + }); + + await waitForCondition(() => mParticle.loggedEvents.length > 0); + + mParticle.loggedEvents.length.should.equal(1); + mParticle.loggedEvents[0].eventName.should.equal( + 'selectPlacements' + ); + }); + + it('should log event when selectPlacements promise rejects', async () => { + window.Rokt.createLauncher = async function () { + return Promise.resolve({ + selectPlacements: function () { + return Promise.reject( + new Error('selection failed') + ); + }, + }); + }; + + await window.mParticle.forwarder.init( + { + accountId: '123456', + }, + reportService.cb, + true, + null, + { + 'cached-user-attr': 'cached-value', + } + ); + + await waitForCondition( + () => window.mParticle.forwarder.isInitialized + ); + + try { + await window.mParticle.forwarder.selectPlacements({ + identifier: 'test-placement', + attributes: { + 'new-attr': 'new-value', + }, + }); + } catch (e) { + // Expected rejection from selectPlacements + } + + await waitForCondition(() => mParticle.loggedEvents.length > 0); + + mParticle.loggedEvents.length.should.equal(1); + mParticle.loggedEvents[0].eventName.should.equal( + 'selectPlacements' + ); + }); + it('should skip logging when mParticle.logEvent is not available', async () => { var originalLogEvent = window.mParticle.logEvent; window.mParticle.logEvent = undefined; @@ -4581,6 +4753,190 @@ describe('Rokt Forwarder', () => { }); }); + describe('#_setRoktSessionId', () => { + var setIntegrationAttributeCalls; + + beforeEach(() => { + setIntegrationAttributeCalls = []; + window.Rokt = new MockRoktForwarder(); + window.mParticle.Rokt = window.Rokt; + window.mParticle.Rokt.attachKitCalled = false; + window.mParticle.Rokt.attachKit = async (kit) => { + window.mParticle.Rokt.attachKitCalled = true; + window.mParticle.Rokt.kit = kit; + Promise.resolve(); + }; + window.mParticle.Rokt.setLocalSessionAttribute = function ( + key, + value + ) { + window.mParticle._Store.localSessionAttributes[key] = value; + }; + window.mParticle.Rokt.getLocalSessionAttributes = function () { + return window.mParticle._Store.localSessionAttributes; + }; + window.mParticle.Rokt.filters = { + userAttributeFilters: [], + filterUserAttributes: function (attributes) { + return attributes; + }, + filteredUser: { + getMPID: function () { + return '123'; + }, + }, + }; + window.mParticle.getInstance = function () { + return { + setIntegrationAttribute: function (id, attrs) { + setIntegrationAttributeCalls.push({ + id: id, + attrs: attrs, + }); + }, + }; + }; + }); + + afterEach(() => { + delete window.mParticle.getInstance; + window.mParticle.forwarder.isInitialized = false; + window.mParticle.Rokt.attachKitCalled = false; + }); + + function createMockSelection(sessionId) { + return { + context: { + sessionId: sessionId + ? Promise.resolve(sessionId) + : Promise.resolve(''), + }, + }; + } + + function setupLauncherWithSelection(mockSelection) { + window.Rokt.createLauncher = async function (_options) { + return Promise.resolve({ + selectPlacements: function () { + return Promise.resolve(mockSelection); + }, + }); + }; + } + + it('should set integration attribute when session ID is available via context', async () => { + var mockSelection = createMockSelection('rokt-session-abc'); + setupLauncherWithSelection(mockSelection); + + await window.mParticle.forwarder.init( + { accountId: '123456' }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition( + () => window.mParticle.forwarder.isInitialized + ); + + await window.mParticle.forwarder.selectPlacements({ + identifier: 'test-placement', + attributes: {}, + }); + + await waitForCondition( + () => setIntegrationAttributeCalls.length > 0 + ); + + setIntegrationAttributeCalls.length.should.equal(1); + setIntegrationAttributeCalls[0].id.should.equal(181); + setIntegrationAttributeCalls[0].attrs.should.deepEqual({ + roktSessionId: 'rokt-session-abc', + }); + }); + + it('should not set integration attribute when session ID is empty', async () => { + var mockSelection = createMockSelection(''); + setupLauncherWithSelection(mockSelection); + + await window.mParticle.forwarder.init( + { accountId: '123456' }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition( + () => window.mParticle.forwarder.isInitialized + ); + + await window.mParticle.forwarder.selectPlacements({ + identifier: 'test-placement', + attributes: {}, + }); + + // Give time for any async operations to settle + await new Promise((resolve) => setTimeout(resolve, 50)); + + setIntegrationAttributeCalls.length.should.equal(0); + }); + + it('should not throw when mParticle.getInstance is unavailable', async () => { + var mockSelection = createMockSelection('rokt-session-abc'); + setupLauncherWithSelection(mockSelection); + delete window.mParticle.getInstance; + + await window.mParticle.forwarder.init( + { accountId: '123456' }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition( + () => window.mParticle.forwarder.isInitialized + ); + + // Should not throw + await window.mParticle.forwarder.selectPlacements({ + identifier: 'test-placement', + attributes: {}, + }); + + // Give time for async operations + await new Promise((resolve) => setTimeout(resolve, 50)); + + setIntegrationAttributeCalls.length.should.equal(0); + }); + + it('should return the selection promise to callers', async () => { + var mockSelection = createMockSelection('rokt-session-abc'); + setupLauncherWithSelection(mockSelection); + + await window.mParticle.forwarder.init( + { accountId: '123456' }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition( + () => window.mParticle.forwarder.isInitialized + ); + + var result = await window.mParticle.forwarder.selectPlacements({ + identifier: 'test-placement', + attributes: {}, + }); + + result.should.equal(mockSelection); + }); + }); + describe('#parseSettingsString', () => { it('should parse null values in a settings string appropriately', () => { const settingsString =