Skip to content

Commit 5bc5c16

Browse files
rmi22186claude
andcommitted
feat: Add Advertiser IDSync search on user identification
When a kit setting `advertiserIdSyncApiKey` is provided, the kit calls mParticle.Identity.searchAdvertiser(apiKey, { email }, callback) from onUserIdentified. On a 200 response the kit sets the local user attribute `userIdentifiedInAdvertiser = true` so downstream placement selection can target users whose identity is matched in the advertiser workspace. The setting is a string (the advertiser workspace's API key) rather than a boolean — this lets a single SDK send searches against an arbitrary advertiser workspace without needing the SDK's own workspace to be reconfigured. Missing key, missing/invalid email, and a missing mParticle.Identity.searchAdvertiser implementation are all silently inert (no network call, no attribute set). Pairs with the corresponding mParticle Web SDK change that exposes mParticle.Identity.searchAdvertiser. Note: a Fastly CORS allow-list update is required separately for the x-mp-key header on /v1/search. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cd94bed commit 5bc5c16

2 files changed

Lines changed: 223 additions & 1 deletion

File tree

src/Rokt-Kit.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ interface RoktKitSettings {
3030
loggingUrl?: string;
3131
errorUrl?: string;
3232
isLoggingEnabled?: string | boolean;
33+
advertiserIdSyncApiKey?: string;
3334
}
3435

3536
interface EventAttributeCondition {
@@ -87,6 +88,23 @@ interface FilteredUser extends IMParticleUser {
8788
getUserIdentities?: () => { userIdentities: Record<string, string> };
8889
}
8990

91+
interface AdvertiserIdSyncResult {
92+
httpCode: number;
93+
body?: {
94+
context?: string | null;
95+
mpid?: string;
96+
matched_identities?: Record<string, string>;
97+
is_ephemeral?: boolean;
98+
is_logged_in?: boolean;
99+
};
100+
}
101+
102+
type AdvertiserIdSyncSearcher = (
103+
apiKey: string,
104+
knownIdentities: { email: string },
105+
callback: (result: AdvertiserIdSyncResult) => void,
106+
) => void;
107+
90108
interface KitFilters {
91109
userAttributeFilters?: string[];
92110
filterUserAttributes?: (attributes: Record<string, unknown>, filters?: string[]) => Record<string, unknown>;
@@ -134,6 +152,7 @@ interface MParticleExtended {
134152
loggedEvents?: Array<Record<string, unknown>>;
135153
_registerErrorReportingService?(service: ErrorReportingService): void;
136154
_registerLoggingService?(service: LoggingService): void;
155+
Identity?: { searchAdvertiser?: AdvertiserIdSyncSearcher };
137156
}
138157

139158
interface TestHelpers {
@@ -217,6 +236,7 @@ const ROKT_IDENTITY_EVENT_TYPE = {
217236
const ROKT_THANK_YOU_JOURNEY_EXTENSION = 'ThankYouJourney';
218237
const ROKT_INTEGRATION_SCRIPT_ID = 'rokt-launcher';
219238
const ROKT_THANK_YOU_ELEMENT_SCRIPT_ID = 'rokt-thank-you-element';
239+
const USER_IDENTIFIED_IN_ADVERTISER_KEY = 'userIdentifiedInAdvertiser';
220240

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

@@ -679,6 +699,7 @@ class RoktKit implements KitInterface {
679699
// Private fields
680700
private _mappedEmailSha256Key?: string;
681701
private _onboardingExpProvider?: string;
702+
private _advertiserIdSyncApiKey?: string;
682703

683704
// ---- Private helpers ----
684705

@@ -1029,6 +1050,10 @@ class RoktKit implements KitInterface {
10291050
this._mappedEmailSha256Key = kitSettings.hashedEmailUserIdentityType.toLowerCase();
10301051
}
10311052

1053+
this._advertiserIdSyncApiKey = isString(kitSettings.advertiserIdSyncApiKey)
1054+
? kitSettings.advertiserIdSyncApiKey
1055+
: undefined;
1056+
10321057
const domain = mp().Rokt?.domain;
10331058
const { roktExtensionsQueryParams, legacyRoktExtensions, loadThankYouElement } = extractRoktExtensionConfig(
10341059
kitSettings.roktExtensions,
@@ -1170,10 +1195,37 @@ class RoktKit implements KitInterface {
11701195
}
11711196

11721197
public onUserIdentified(user: IMParticleUser): string {
1173-
this.filters.filteredUser = user as FilteredUser;
1198+
const filteredUser = user as FilteredUser;
1199+
this.filters.filteredUser = filteredUser;
1200+
this.searchAdvertiser(filteredUser);
11741201
return this.handleIdentityComplete(user, ROKT_IDENTITY_EVENT_TYPE.IDENTIFY, 'onUserIdentified');
11751202
}
11761203

1204+
private searchAdvertiser(filteredUser: FilteredUser): void {
1205+
const apiKey = this._advertiserIdSyncApiKey;
1206+
if (!apiKey) {
1207+
return;
1208+
}
1209+
const searchAdvertiser = mp().Identity?.searchAdvertiser;
1210+
if (typeof searchAdvertiser !== 'function') {
1211+
return;
1212+
}
1213+
const userIdentities = filteredUser.getUserIdentities ? filteredUser.getUserIdentities().userIdentities : null;
1214+
const email = userIdentities?.email;
1215+
if (!email || !isString(email)) {
1216+
return;
1217+
}
1218+
try {
1219+
searchAdvertiser(apiKey, { email }, (result: AdvertiserIdSyncResult) => {
1220+
if (result?.httpCode === 200) {
1221+
this.userAttributes[USER_IDENTIFIED_IN_ADVERTISER_KEY] = true;
1222+
}
1223+
});
1224+
} catch (err) {
1225+
console.error('Rokt Kit: Advertiser IDSync search failed', err);
1226+
}
1227+
}
1228+
11771229
public onLoginComplete(user: IMParticleUser, _filteredIdentityRequest: unknown): string {
11781230
return this.handleIdentityComplete(user, ROKT_IDENTITY_EVENT_TYPE.LOGIN, 'onLoginComplete');
11791231
}

test/src/tests.spec.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2972,6 +2972,176 @@ describe('Rokt Forwarder', () => {
29722972
});
29732973
});
29742974

2975+
describe('#advertiserIdSync', () => {
2976+
const ADVERTISER_API_KEY = 'advertiser-key-abc123';
2977+
2978+
function makeUser(overrides: any = {}) {
2979+
return {
2980+
getAllUserAttributes: () => ({}),
2981+
getMPID: () => '123',
2982+
getUserIdentities: () => ({ userIdentities: { email: 'test@example.com' } }),
2983+
...overrides,
2984+
};
2985+
}
2986+
2987+
let originalIdentity: any;
2988+
2989+
beforeEach(() => {
2990+
originalIdentity = (window as any).mParticle.Identity;
2991+
});
2992+
2993+
afterEach(() => {
2994+
(window as any).mParticle.Identity = originalIdentity;
2995+
(window as any).mParticle.forwarder.userAttributes = {};
2996+
});
2997+
2998+
it('should call Identity.searchAdvertiser with the configured api key and set userIdentifiedInAdvertiser when 200 returned', async () => {
2999+
let receivedApiKey: any = null;
3000+
let receivedKnownIdentities: any = null;
3001+
(window as any).mParticle.Identity = {
3002+
searchAdvertiser: (apiKey: any, knownIdentities: any, cb: any) => {
3003+
receivedApiKey = apiKey;
3004+
receivedKnownIdentities = knownIdentities;
3005+
cb({ httpCode: 200, body: { mpid: '999' } });
3006+
},
3007+
};
3008+
3009+
await (window as any).mParticle.forwarder.init(
3010+
{ accountId: '123456', advertiserIdSyncApiKey: ADVERTISER_API_KEY },
3011+
reportService.cb,
3012+
true,
3013+
null,
3014+
{},
3015+
);
3016+
3017+
(window as any).mParticle.forwarder.onUserIdentified(makeUser());
3018+
3019+
expect(receivedApiKey).toBe(ADVERTISER_API_KEY);
3020+
expect(receivedKnownIdentities).toEqual({ email: 'test@example.com' });
3021+
expect((window as any).mParticle.forwarder.userAttributes.userIdentifiedInAdvertiser).toBe(true);
3022+
});
3023+
3024+
it('should not set userIdentifiedInAdvertiser when search returns 404', async () => {
3025+
(window as any).mParticle.Identity = {
3026+
searchAdvertiser: (_apiKey: any, _knownIdentities: any, cb: any) => {
3027+
cb({ httpCode: 404 });
3028+
},
3029+
};
3030+
3031+
await (window as any).mParticle.forwarder.init(
3032+
{ accountId: '123456', advertiserIdSyncApiKey: ADVERTISER_API_KEY },
3033+
reportService.cb,
3034+
true,
3035+
null,
3036+
{},
3037+
);
3038+
3039+
(window as any).mParticle.forwarder.onUserIdentified(makeUser());
3040+
3041+
expect((window as any).mParticle.forwarder.userAttributes.userIdentifiedInAdvertiser).toBeUndefined();
3042+
});
3043+
3044+
it('should not call searchAdvertiser when advertiserIdSyncApiKey is missing', async () => {
3045+
let searchCalled = false;
3046+
(window as any).mParticle.Identity = {
3047+
searchAdvertiser: () => {
3048+
searchCalled = true;
3049+
},
3050+
};
3051+
3052+
await (window as any).mParticle.forwarder.init({ accountId: '123456' }, reportService.cb, true, null, {});
3053+
3054+
(window as any).mParticle.forwarder.onUserIdentified(makeUser());
3055+
3056+
expect(searchCalled).toBe(false);
3057+
expect((window as any).mParticle.forwarder.userAttributes.userIdentifiedInAdvertiser).toBeUndefined();
3058+
});
3059+
3060+
it('should not call searchAdvertiser when advertiserIdSyncApiKey is an empty string', async () => {
3061+
let searchCalled = false;
3062+
(window as any).mParticle.Identity = {
3063+
searchAdvertiser: () => {
3064+
searchCalled = true;
3065+
},
3066+
};
3067+
3068+
await (window as any).mParticle.forwarder.init(
3069+
{ accountId: '123456', advertiserIdSyncApiKey: '' },
3070+
reportService.cb,
3071+
true,
3072+
null,
3073+
{},
3074+
);
3075+
3076+
(window as any).mParticle.forwarder.onUserIdentified(makeUser());
3077+
3078+
expect(searchCalled).toBe(false);
3079+
expect((window as any).mParticle.forwarder.userAttributes.userIdentifiedInAdvertiser).toBeUndefined();
3080+
});
3081+
3082+
it('should not call searchAdvertiser when the user has no plain email identity', async () => {
3083+
let searchCalled = false;
3084+
(window as any).mParticle.Identity = {
3085+
searchAdvertiser: () => {
3086+
searchCalled = true;
3087+
},
3088+
};
3089+
3090+
await (window as any).mParticle.forwarder.init(
3091+
{ accountId: '123456', advertiserIdSyncApiKey: ADVERTISER_API_KEY },
3092+
reportService.cb,
3093+
true,
3094+
null,
3095+
{},
3096+
);
3097+
3098+
(window as any).mParticle.forwarder.onUserIdentified(
3099+
makeUser({ getUserIdentities: () => ({ userIdentities: {} }) }),
3100+
);
3101+
3102+
expect(searchCalled).toBe(false);
3103+
expect((window as any).mParticle.forwarder.userAttributes.userIdentifiedInAdvertiser).toBeUndefined();
3104+
});
3105+
3106+
it('should not throw when Identity.searchAdvertiser is unavailable', async () => {
3107+
(window as any).mParticle.Identity = {};
3108+
3109+
await (window as any).mParticle.forwarder.init(
3110+
{ accountId: '123456', advertiserIdSyncApiKey: ADVERTISER_API_KEY },
3111+
reportService.cb,
3112+
true,
3113+
null,
3114+
{},
3115+
);
3116+
3117+
expect(() => {
3118+
(window as any).mParticle.forwarder.onUserIdentified(makeUser());
3119+
}).not.toThrow();
3120+
expect((window as any).mParticle.forwarder.userAttributes.userIdentifiedInAdvertiser).toBeUndefined();
3121+
});
3122+
3123+
it('should swallow errors thrown by searchAdvertiser', async () => {
3124+
(window as any).mParticle.Identity = {
3125+
searchAdvertiser: () => {
3126+
throw new Error('boom');
3127+
},
3128+
};
3129+
3130+
await (window as any).mParticle.forwarder.init(
3131+
{ accountId: '123456', advertiserIdSyncApiKey: ADVERTISER_API_KEY },
3132+
reportService.cb,
3133+
true,
3134+
null,
3135+
{},
3136+
);
3137+
3138+
expect(() => {
3139+
(window as any).mParticle.forwarder.onUserIdentified(makeUser());
3140+
}).not.toThrow();
3141+
expect((window as any).mParticle.forwarder.userAttributes.userIdentifiedInAdvertiser).toBeUndefined();
3142+
});
3143+
});
3144+
29753145
describe('#onLoginComplete', () => {
29763146
it('should update userAttributes from the filtered user', () => {
29773147
(window as any).mParticle.forwarder.onLoginComplete({

0 commit comments

Comments
 (0)