Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 91 additions & 31 deletions src/Rokt-Kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ interface RoktGlobal {
currentLauncher?: RoktLauncher;
__batch_stream__?(batch: Batch): void;
setExtensionData(data: Record<string, unknown>): void;
use(value: string): void;
}

// TODO: getMPID and getUserIdentities exist on the User base type but are not re-exported from
Expand Down Expand Up @@ -137,7 +138,8 @@ interface MParticleExtended {

interface TestHelpers {
generateLauncherScript: (domain: string | undefined, extensions: string[]) => string;
extractRoktExtensions: (settingsString?: string) => string[];
generateThankYouElementScript: (domain: string | undefined) => string;
extractRoktExtensionConfig: (settingsString?: string) => RoktExtensionConfig;
hashEventMessage: (messageType: number, eventType: number, eventName: string) => string | number;
parseSettingsString: <T>(settingsString?: string) => T[];
generateMappedEventLookup: (placementEventMapping: PlacementEventMappingEntry[]) => Record<string, string>;
Expand Down Expand Up @@ -178,6 +180,12 @@ interface LogEntry {
code?: string;
}

interface RoktExtensionConfig {
roktExtensionsQueryParams: string[];
legacyRoktExtensions: string[];
loadThankYouElement: boolean;
}

declare global {
interface Window {
Rokt?: RoktGlobal;
Expand Down Expand Up @@ -206,6 +214,9 @@ const ROKT_IDENTITY_EVENT_TYPE = {
MODIFY_USER: 'modify_user',
IDENTIFY: 'identify',
} as const;
const ROKT_THANK_YOU_JOURNEY_EXTENSION = 'ThankYouJourney';
const ROKT_INTEGRATION_SCRIPT_ID = 'rokt-launcher';
const ROKT_THANK_YOU_ELEMENT_SCRIPT_ID = 'rokt-thank-you-element';

type RoktIdentityEventType = (typeof ROKT_IDENTITY_EVENT_TYPE)[keyof typeof ROKT_IDENTITY_EVENT_TYPE];

Expand Down Expand Up @@ -245,17 +256,47 @@ function mp(): MParticleExtended {
// ============================================================

function generateLauncherScript(domain: string | undefined, extensions: string[]): string {
const resolvedDomain = typeof domain !== 'undefined' ? domain : 'apps.rokt.com';
const protocol = 'https://';
const launcherPath = '/wsdk/integrations/launcher.js';
const baseUrl = [protocol, resolvedDomain, launcherPath].join('');
const baseUrl = [generateBaseUrl(domain), launcherPath].join('');

if (!extensions || extensions.length === 0) {
return baseUrl;
}
return baseUrl + '?extensions=' + extensions.join(',');
}

function generateThankYouElementScript(domain: string | undefined) {
const thankYouElementPath = '/rokt-elements/rokt-element-thank-you.js';
return [generateBaseUrl(domain), thankYouElementPath].join('');
}

function generateBaseUrl(domain: string | undefined) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

link

const resolvedDomain = typeof domain !== 'undefined' ? domain : 'apps.rokt-api.com';
const protocol = 'https://';

return [protocol, resolvedDomain].join('');
}

function loadRoktScript(
scriptId: string,
source: string,
handlers?: { onLoad?: () => void; onError?: (e: Event | string) => void },
): void {
if (document.getElementById(scriptId)) return; // resolves the preexisting script issue

const target = document.head || document.body;
const script = document.createElement('script');
script.id = scriptId;
script.type = 'text/javascript';
script.src = source;
script.async = true;
script.crossOrigin = 'anonymous';
(script as HTMLScriptElement & { fetchPriority: string }).fetchPriority = 'high';
if (handlers?.onLoad) script.onload = handlers.onLoad;
if (handlers?.onError) script.onerror = handlers.onError;
target.appendChild(script);
}

function isObject(val: unknown): val is Record<string, unknown> {
return val != null && typeof val === 'object' && Array.isArray(val) === false;
}
Expand All @@ -272,15 +313,33 @@ function parseSettingsString<T>(settingsString?: string): T[] {
return [];
}

function extractRoktExtensions(settingsString?: string): string[] {
function extractRoktExtensionConfig(settingsString?: string): RoktExtensionConfig {
const settings = settingsString ? parseSettingsString<RoktExtensionEntry>(settingsString) : [];
const roktExtensions: string[] = [];
const roktExtensionsQueryParams: string[] = [];
const legacyRoktExtensions: string[] = [];
let loadThankYouElement = false;

for (let i = 0; i < settings.length; i++) {
roktExtensions.push(settings[i].value);
const extensionName = settings[i].value;
if (extensionName === 'thank-you-journey') {
loadThankYouElement = true;
legacyRoktExtensions.push(ROKT_THANK_YOU_JOURNEY_EXTENSION);
} else {
roktExtensionsQueryParams.push(extensionName);
}
}

return roktExtensions;
return {
roktExtensionsQueryParams,
legacyRoktExtensions,
loadThankYouElement,
};
}

function registerLegacyExtensions(legacyExtensions: string[]) {
for (const extension of legacyExtensions) {
window.Rokt?.use(extension);
}
}

function generateMappedEventLookup(placementEventMapping: PlacementEventMappingEntry[]): Record<string, string> {
Expand Down Expand Up @@ -954,7 +1013,6 @@ class RoktKit implements KitInterface {
): string {
const kitSettings = settings as unknown as RoktKitSettings;
const accountId = kitSettings.accountId;
const roktExtensions = extractRoktExtensions(kitSettings.roktExtensions);
this.userAttributes = filteredUserAttributes || {};
this._onboardingExpProvider = kitSettings.onboardingExpProvider;

Expand All @@ -972,6 +1030,9 @@ class RoktKit implements KitInterface {
}

const domain = mp().Rokt?.domain;
const { roktExtensionsQueryParams, legacyRoktExtensions, loadThankYouElement } = extractRoktExtensionConfig(
kitSettings.roktExtensions,
);
const launcherOptions: Record<string, unknown> = {
...((mp().Rokt?.launcherOptions as Record<string, unknown>) || {}),
};
Expand Down Expand Up @@ -1012,7 +1073,8 @@ class RoktKit implements KitInterface {
if (testMode) {
this.testHelpers = {
generateLauncherScript: generateLauncherScript,
extractRoktExtensions: extractRoktExtensions,
generateThankYouElementScript: generateThankYouElementScript,
extractRoktExtensionConfig: extractRoktExtensionConfig,
hashEventMessage: hashEventMessage,
parseSettingsString: parseSettingsString,
generateMappedEventLookup: generateMappedEventLookup,
Expand All @@ -1034,31 +1096,27 @@ class RoktKit implements KitInterface {
return 'Successfully initialized: ' + name;
}

if (loadThankYouElement) {
loadRoktScript(ROKT_THANK_YOU_ELEMENT_SCRIPT_ID, generateThankYouElementScript(domain));
}

if (this.isLauncherReadyToAttach()) {
this.attachLauncher(accountId, launcherOptions);
} else {
const target = document.head || document.body;
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = generateLauncherScript(domain, roktExtensions);
script.async = true;
script.crossOrigin = 'anonymous';
(script as HTMLScriptElement & { fetchPriority: string }).fetchPriority = 'high';
script.id = 'rokt-launcher';

script.onload = () => {
if (this.isLauncherReadyToAttach()) {
this.attachLauncher(accountId, launcherOptions);
} else {
console.error('Rokt object is not available after script load.');
}
};

script.onerror = (error) => {
console.error('Error loading Rokt launcher script:', error);
};
loadRoktScript(ROKT_INTEGRATION_SCRIPT_ID, generateLauncherScript(domain, roktExtensionsQueryParams), {
onLoad: () => {
if (this.isLauncherReadyToAttach()) {
this.attachLauncher(accountId, launcherOptions);
registerLegacyExtensions(legacyRoktExtensions);
} else {
console.error('Rokt object is not available after script load.');
}
},
onError: (error) => {
console.error('Error loading Rokt launcher script:', error);
},
});

target.appendChild(script);
this.captureTiming(RoktKit.PERFORMANCE_MARKS.RoktScriptAppended);
}

Expand Down Expand Up @@ -1200,6 +1258,8 @@ class RoktKit implements KitInterface {

/**
* Enables optional Integration Launcher extensions before selecting placements.
*
* @deprecated This functionality has been internalized and will be removed in a future release.
*/
public use(extensionName: string): Promise<unknown> {
if (!this.isKitReady()) {
Expand Down
140 changes: 121 additions & 19 deletions test/src/tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3217,7 +3217,7 @@ describe('Rokt Forwarder', () => {
});

describe('#generateLauncherScript', () => {
const baseUrl = 'https://apps.rokt.com/wsdk/integrations/launcher.js';
const baseUrl = 'https://apps.rokt-api.com/wsdk/integrations/launcher.js';

beforeEach(() => {
(window as any).mParticle.forwarder.init(
Expand Down Expand Up @@ -3272,38 +3272,140 @@ describe('Rokt Forwarder', () => {
});
});

describe('#generateThankYouElementScript', () => {
const baseUrl = 'https://apps.rokt-api.com/rokt-elements/rokt-element-thank-you.js';

beforeEach(() => {
(window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true);
});

it('should return base URL when no domain is passed', () => {
const url = (window as any).mParticle.forwarder.testHelpers.generateThankYouElementScript(undefined);
expect(url).toBe(baseUrl);
});

it('should return an updated base URL with CNAME when domain is passed', () => {
const url = (window as any).mParticle.forwarder.testHelpers.generateThankYouElementScript('cname.rokt.com');
expect(url).toBe('https://cname.rokt.com/rokt-elements/rokt-element-thank-you.js');
});
});

describe('#roktExtensions', () => {
beforeEach(async () => {
(window as any).Rokt = new (MockRoktForwarder as any)();
(window as any).mParticle.Rokt = (window as any).Rokt;
beforeEach(() => {
(window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true);
});

describe('extractRoktExtensionConfig', () => {
it('should correctly map known extension names to their query parameters', () => {
const settingsString =
'[{&quot;jsmap&quot;:null,&quot;map&quot;:null,&quot;maptype&quot;:&quot;StaticList&quot;,&quot;value&quot;:&quot;cos-extension-detection&quot;},{&quot;jsmap&quot;:null,&quot;map&quot;:null,&quot;maptype&quot;:&quot;StaticList&quot;,&quot;value&quot;:&quot;experiment-monitoring&quot;}]';

const result = (window as any).mParticle.forwarder.testHelpers.extractRoktExtensionConfig(settingsString);
expect(result.roktExtensionsQueryParams).toEqual(['cos-extension-detection', 'experiment-monitoring']);
expect(result.legacyRoktExtensions).toEqual([]);
expect(result.loadThankYouElement).toBe(false);
});

it('should separate thank-you-journey into legacyRoktExtensions and set loadThankYouElement', () => {
const settingsString =
'[{"jsmap":null,"map":null,"maptype":"LegacyExtension","value":"thank-you-journey"},{"jsmap":null,"map":null,"maptype":"StaticList","value":"instant-purchase"}]';

const result = (window as any).mParticle.forwarder.testHelpers.extractRoktExtensionConfig(settingsString);
expect(result.roktExtensionsQueryParams).toEqual(['instant-purchase']);
expect(result.legacyRoktExtensions).toEqual(['ThankYouJourney']);
expect(result.loadThankYouElement).toBe(true);
});
});

it('should fetch thank you element resource when thank you element extension is provided', async () => {
document.getElementById('rokt-thank-you-element')?.remove();
document.getElementById('rokt-launcher')?.remove();

(window as any).Rokt = undefined;
(window as any).mParticle.Rokt = {
attachKit: async (kit: any) => {
(window as any).mParticle.Rokt.kit = kit;
},
filters: {
userAttributesFilters: [],
filterUserAttributes: (attrs: any) => attrs,
filteredUser: { getMPID: () => '123' },
},
use: () => Promise.resolve(),
};

await (window as any).mParticle.forwarder.init(
{
accountId: '123456',
roktExtensions: '[{"jsmap":null,"map":null,"maptype":"LegacyExtension","value":"thank-you-journey"}]',
},
reportService.cb,
true,
false,
);

const tyeScript = document.getElementById('rokt-thank-you-element') as HTMLScriptElement;
expect(tyeScript).not.toBeNull();
expect(tyeScript.src).toContain('/rokt-elements/rokt-element-thank-you.js');
});

describe('extractRoktExtensions', () => {
it('should correctly map known extension names to their query parameters', async () => {
const settingsString =
'[{&quot;jsmap&quot;:null,&quot;map&quot;:null,&quot;maptype&quot;:&quot;StaticList&quot;,&quot;value&quot;:&quot;cos-extension-detection&quot;},{&quot;jsmap&quot;:null,&quot;map&quot;:null,&quot;maptype&quot;:&quot;StaticList&quot;,&quot;value&quot;:&quot;experiment-monitoring&quot;}]';
const expectedExtensions = ['cos-extension-detection', 'experiment-monitoring'];
it('should call window.Rokt.use with ThankYouJourney when thank-you-journey extension is provided', async () => {
document.getElementById('rokt-thank-you-element')?.remove();
document.getElementById('rokt-launcher')?.remove();

expect((window as any).mParticle.forwarder.testHelpers.extractRoktExtensions(settingsString)).toEqual(
expectedExtensions,
);
});
});
const useCalls: string[] = [];

it('should handle invalid setting strings', () => {
expect((window as any).mParticle.forwarder.testHelpers.extractRoktExtensions('NONE')).toEqual([]);
(window as any).Rokt = undefined;
(window as any).mParticle.Rokt = {
attachKit: async (kit: any) => {
(window as any).mParticle.Rokt.kit = kit;
},
filters: {
userAttributesFilters: [],
filterUserAttributes: (attrs: any) => attrs,
filteredUser: { getMPID: () => '123' },
},
};

expect((window as any).mParticle.forwarder.testHelpers.extractRoktExtensions(undefined)).toEqual([]);
await (window as any).mParticle.forwarder.init(
{
accountId: '123456',
roktExtensions: '[{"jsmap":null,"map":null,"maptype":"LegacyExtension","value":"thank-you-journey"}]',
},
reportService.cb,
false,
);

(window as any).Rokt = new (MockRoktForwarder as any)();
(window as any).Rokt.use = (name: string) => {
useCalls.push(name);
};
(window as any).Rokt.createLauncher = async () =>
Promise.resolve({ selectPlacements: () => {}, hashAttributes: () => {}, use: () => Promise.resolve() });

expect((window as any).mParticle.forwarder.testHelpers.extractRoktExtensions(null)).toEqual([]);
const launcherScript = document.getElementById('rokt-launcher') as HTMLScriptElement;
launcherScript.onload!(new Event('load'));

await waitForCondition(() => useCalls.length > 0);

expect(useCalls).toContain('ThankYouJourney');
});

it('should handle invalid setting strings', () => {
expect((window as any).mParticle.forwarder.testHelpers.extractRoktExtensionConfig('NONE')).toEqual({
roktExtensionsQueryParams: [],
legacyRoktExtensions: [],
loadThankYouElement: false,
});
expect((window as any).mParticle.forwarder.testHelpers.extractRoktExtensionConfig(undefined)).toEqual({
roktExtensionsQueryParams: [],
legacyRoktExtensions: [],
loadThankYouElement: false,
});
expect((window as any).mParticle.forwarder.testHelpers.extractRoktExtensionConfig(null)).toEqual({
roktExtensionsQueryParams: [],
legacyRoktExtensions: [],
loadThankYouElement: false,
});
});
});

Expand Down
Loading