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
107 changes: 89 additions & 18 deletions kits/rokt/src/Rokt-Kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,20 @@ import {
clearUtmParams,
} from './pageViewStorage';
import { isLocalStorageAvailable } from './storage';

import { isObject, isString, isEmpty, isFunction, sanitizeUrl } from './utils';
import { buildSetterDiagnosticLogEntry, buildSelectPlacementsDiagnosticLogEntry } from './diagnosticTiming';
import {
createPreselectState,
maybeFirePreselect as maybeFirePreselectExternal,
flushPendingPreselectDispatches as flushPendingPreselectDispatchesExternal,
type PreselectState,
type PreselectHost,
} from './preselection';
import { findPreselectionConfigByIdentifier, isPreselectAttributeKey } from './preselectionConfig';

import { isObject, isString, isEmpty, isFunction, sanitizeUrl, djb2 } from './utils';
import {
buildSetterDiagnosticLogEntry,
buildSelectPlacementsDiagnosticLogEntry,
} from './diagnosticTiming';
import {
createLauncherAttachState,
markLauncherAttached,
Expand Down Expand Up @@ -102,6 +113,7 @@ interface RoktLauncher {
hashAttributes(attributes: Record<string, unknown>): Promise<Record<string, unknown>>;
use(extensionName: string): Promise<unknown>;
terminate(): Promise<void>;
enablePreselection?: boolean;
}

interface RoktGlobal {
Expand Down Expand Up @@ -341,7 +353,7 @@ function generateThankYouElementScript(domain: string | undefined) {
}

function generateBaseUrl(domain: string | undefined) {
const resolvedDomain = typeof domain !== 'undefined' ? domain : DEFAULT_ROKT_DOMAIN;
const resolvedDomain = domain !== undefined ? domain : DEFAULT_ROKT_DOMAIN;

if (resolvedDomain.includes('://')) {
return resolvedDomain.replace(/\/+$/, '');
Expand Down Expand Up @@ -488,15 +500,6 @@ function generateIntegrationName(customIntegrationName?: string): string {
return integrationName;
}

function djb2(str: string): number {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) + hash + str.charCodeAt(i);
hash = hash & hash;
}
return hash;
}

function createAutoRemovedIframe(src: string): void {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
Expand Down Expand Up @@ -833,6 +836,9 @@ class RoktKit implements KitInterface {

private _launcherAttachState: LauncherAttachState = createLauncherAttachState();

private accountId: string | null = null;
private _preselectState: PreselectState = createPreselectState();

// ---- Private helpers ----

private getEventAttributeValue(event: SDKEvent, eventAttributeKey: string): unknown {
Expand All @@ -841,7 +847,7 @@ class RoktKit implements KitInterface {
return null;
}

if (typeof attributes[eventAttributeKey] === 'undefined') {
if (attributes[eventAttributeKey] === undefined) {
return null;
}

Expand Down Expand Up @@ -958,6 +964,41 @@ class RoktKit implements KitInterface {
}
}

private isPreselectionEnabled(): boolean {
return this.launcher?.enablePreselection === true;
}

private buildCacheMatchKeys(identifier: string | undefined): string[] | undefined {
if (!this.isPreselectionEnabled()) {
return undefined;
}

const configEntry = findPreselectionConfigByIdentifier(this.accountId, identifier);
if (!configEntry) {
return undefined;
}

return configEntry.attributeKeys;
}

private buildPreselectHost(): PreselectHost {
return {
accountId: this.accountId,
filteredUser: this.filters.filteredUser,
userAttributes: this.userAttributes,
isKitReady: () => this.isKitReady(),
isPreselectionEnabled: () => this.isPreselectionEnabled(),
getEventAttributeValue: (event, key) => this.getEventAttributeValue(event, key),
logPlacementDiagnostic: (entry) => this.loggingService?.logPlacementDiagnostic(entry),
log: (entry) => this.loggingService?.log(entry),
selectPlacements: (options) => this.selectPlacements(options),
};
Comment thread
alexs-mparticle marked this conversation as resolved.
}

private flushPendingPreselectDispatches(): void {
flushPendingPreselectDispatchesExternal(this._preselectState, this.buildPreselectHost());
}

private isLauncherReadyToAttach(): boolean {
return !!window.Rokt && isFunction(window.Rokt.createLauncher);
}
Expand Down Expand Up @@ -1115,6 +1156,8 @@ class RoktKit implements KitInterface {

// Attaches the kit to the Rokt manager
mp().Rokt.attachKit(this);

this.flushPendingPreselectDispatches();
}

private fetchOptimizely(): Record<string, unknown> {
Expand Down Expand Up @@ -1181,6 +1224,7 @@ class RoktKit implements KitInterface {
): string {
const kitSettings = settings as unknown as RoktKitSettings;
const accountId = kitSettings.accountId;
this.accountId = accountId || null;
this.userAttributes = removeSelectPlacementsAttributePersistenceDeniedAttributes(filteredUserAttributes);
this._onboardingExpProvider = kitSettings.onboardingExpProvider;

Expand Down Expand Up @@ -1326,6 +1370,7 @@ class RoktKit implements KitInterface {
if (event.EventDataType === MESSAGE_TYPE_PAGE_VIEW) {
captureUtmParams(this.loggingService);
this.capturePageView(event);
maybeFirePreselectExternal(this._preselectState, this.buildPreselectHost(), event);
}

if (event.EventDataType === MESSAGE_TYPE_SESSION_END) {
Expand Down Expand Up @@ -1370,6 +1415,9 @@ class RoktKit implements KitInterface {
if (!isSelectPlacementsAttributePersistenceDenied(key)) {
this.userAttributes[key] = value;
}
if (isPreselectAttributeKey(this.accountId, key)) {
this.flushPendingPreselectDispatches();
}
return 'Successfully set user attribute for forwarder: ' + name;
}

Expand All @@ -1389,7 +1437,13 @@ class RoktKit implements KitInterface {
const filteredUser = user as FilteredUser;
this.filters.filteredUser = filteredUser;
this._workspaceSearchInFlightPromise = this.search(filteredUser);
return this.handleIdentityComplete(user, 'onUserIdentified');
const result = this.handleIdentityComplete(user, 'onUserIdentified');
// onUserIdentified fires for every identity op (identify/login/logout/modify)
// with the fresh current user, so this is the single place to retry a preselect
// that was requeued for missing identity; maybeFirePreselect re-checks
// hasValidIdentity itself, so an anonymous user here is a no-op re-queue.
this.flushPendingPreselectDispatches();
return result;
}

private search(filteredUser: FilteredUser): Promise<void> {
Expand Down Expand Up @@ -1584,19 +1638,36 @@ class RoktKit implements KitInterface {
mpid,
};

const selectPlacementsOptions: Record<string, unknown> = { ...options, attributes: selectPlacementsAttributes };
const cacheMatchKeys = this.buildCacheMatchKeys(typeof options.identifier === 'string' ? options.identifier : undefined);

const selectPlacementsOptions: Record<string, unknown> = {
...options,
attributes: selectPlacementsAttributes,
...(cacheMatchKeys !== undefined ? { cacheMatchKeys } : {}),
};

this.loggingService?.logPlacementDiagnostic(
buildSelectPlacementsDiagnosticLogEntry(Object.keys(selectPlacementsAttributes)),
);

const selection = this.launcher!.selectPlacements(selectPlacementsOptions);

const isPreselect = options.preselect === true;

// After selection resolves, sync the Rokt session ID back to mParticle, then log
const logSelection = () => this.logSelectPlacementsEvent(selectPlacementsAttributes);
const logSelection = () => {
if (!isPreselect) {
this.logSelectPlacementsEvent(selectPlacementsAttributes);
}
};

void Promise.resolve(selection)
.then((sel) => sel?.context?.sessionId?.then((sessionId) => this.setRoktSessionId(sessionId)))
.then((sel) => {
if (isPreselect) {
return;
}
return sel?.context?.sessionId?.then((sessionId) => this.setRoktSessionId(sessionId));
})
.catch(() => undefined)
.finally(logSelection);

Expand Down
29 changes: 29 additions & 0 deletions kits/rokt/src/activePreselectStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { readNamespacedField, writeNamespacedField, LS_NAMESPACE_KEY } from './storage';
import { isObject } from './utils';

export const ACTIVE_PRESELECT_TTL_MS = 60_000;

export interface ActivePreselectRecord {
expiresAt: number;
attributes: Record<string, unknown>;
}

function isActivePreselectRecord(value: unknown): value is ActivePreselectRecord {
return isObject(value) && typeof value.expiresAt === 'number' && isObject(value.attributes);
}

export function buildActivePreselectFieldKey(accountId: string, pathname: string): string {
return `activePreselect:${accountId}:${pathname}`;
}

export function getActivePreselect(fieldKey: string): ActivePreselectRecord | null {
const stored = readNamespacedField(LS_NAMESPACE_KEY, fieldKey);
return isActivePreselectRecord(stored) ? stored : null;
}

export function setActivePreselect(fieldKey: string, attributes: Record<string, unknown>): void {
writeNamespacedField(LS_NAMESPACE_KEY, fieldKey, {
expiresAt: Date.now() + ACTIVE_PRESELECT_TTL_MS,
attributes,
});
}
18 changes: 18 additions & 0 deletions kits/rokt/src/diagnosticTiming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,21 @@ export function buildSelectPlacementsDiagnosticLogEntry(placementAttributeKeys:
code: 'SELECT_PLACEMENTS_DISPATCHED',
};
}

export type PreselectDiagnosticOutcome = 'fired' | 'missed' | 'queued' | 'skipped';

export function buildPreselectDiagnosticLogEntry(
outcome: PreselectDiagnosticOutcome,
reason: string,
): DiagnosticLogEntry {
const code: Record<PreselectDiagnosticOutcome, string> = {
fired: 'PRESELECT_FIRED',
missed: 'PRESELECT_MISSED',
queued: 'PRESELECT_QUEUED',
skipped: 'PRESELECT_SKIPPED',
};
return {
message: `Rokt Kit: preselect ${outcome} [reason=${reason}]`,
code: code[outcome],
};
}
9 changes: 7 additions & 2 deletions kits/rokt/src/pageViewStorage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { LoggingService } from './Rokt-Kit';
import { readNamespacedField, writeNamespacedField, removeNamespacedField, isLocalStorageAvailable } from './storage';
import {
readNamespacedField,
writeNamespacedField,
removeNamespacedField,
isLocalStorageAvailable,
LS_NAMESPACE_KEY,
} from './storage';
import { sanitizeUrl, isObject } from './utils';

const LS_NAMESPACE_KEY = 'mp-rokt-kit';
const LS_PAGE_VIEWS_FIELD = 'pageViews';
const LS_UTM_PARAMS_FIELD = 'utmParams';
export const PAGE_VIEWS_MAX_COUNT = 25;
Expand Down
Loading
Loading