Skip to content

Commit b92e611

Browse files
rmi22186claude
andcommitted
refactor: Adopt IUserIdentities for kit user-identity types
`@mparticle/web-sdk` 2.66.0 ships its own public types entry, which TypeScript prefers over `@types/mparticle__web-sdk`, so the previous `UserIdentities` import (only present in DT) no longer resolves. Switch to the SDK's `IUserIdentities` and thread it through `replaceOtherIdentityWithEmailsha256`, `WorkspaceIdSyncSearcher`, and the workspace-search identifier collection. Also collapse `FilteredUser` to a type alias of `IMParticleUser`. The previous interface redeclared `getMPID` and `getUserIdentities`, both of which are already required on the SDK's `User` base — the redeclarations conflicted with the authoritative shape. Adds a regression test covering `hashedEmailUserIdentityType` mapped at a key whose value is null: emailsha256 must not be synthesized and the source key must not appear in the placements payload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f742616 commit b92e611

2 files changed

Lines changed: 265 additions & 46 deletions

File tree

src/Rokt-Kit.ts

Lines changed: 66 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
// ============================================================
1818

1919
import { Batch, KitInterface, IMParticleUser, SDKEvent } from '@mparticle/web-sdk/internal';
20+
import type { IUserIdentities } from '@mparticle/web-sdk';
2021
// BaseEvent not re-exported from @mparticle/web-sdk/internal, so we import directly from @mparticle/event-models.
2122
import { BaseEvent } from '@mparticle/event-models';
2223

@@ -80,12 +81,9 @@ interface RoktGlobal {
8081
setExtensionData(data: Record<string, unknown>): void;
8182
}
8283

83-
// TODO: getMPID and getUserIdentities exist on the User base type but are not re-exported from
84-
// @mparticle/web-sdk/internal, so we redeclare them here until the internal types expose them.
85-
interface FilteredUser extends IMParticleUser {
86-
getMPID(): string;
87-
getUserIdentities?: () => { userIdentities: Record<string, string> };
88-
}
84+
// FilteredUser is the IMParticleUser shape we receive after kit filtering.
85+
// `getMPID` and `getUserIdentities` are inherited from the SDK's `User` base type.
86+
type FilteredUser = IMParticleUser;
8987

9088
// TODO: Replace with `IIdentitySearchResult` from `@mparticle/web-sdk` once
9189
// a version that exports it is published (currently on a feature branch in
@@ -106,7 +104,7 @@ interface WorkspaceIdSyncResult {
106104
// `@mparticle/web-sdk` once published (mirrors `SDKIdentityApi.search`).
107105
type WorkspaceIdSyncSearcher = (
108106
apiKey: string,
109-
knownIdentities: { email: string },
107+
knownIdentities: IUserIdentities,
110108
callback: (result: WorkspaceIdSyncResult) => void,
111109
) => void;
112110

@@ -727,11 +725,14 @@ class RoktKit implements KitInterface {
727725
// can wait for the HTTP response before reading userIdentifiedInWorkspace;
728726
// — otherwise the first placement call ships without the flag.
729727
private _workspaceSearchInFlightPromise: Promise<void> | null = null;
730-
// The email value sent in the most recent successful search
731-
// dispatch. If a subsequent identification arrives with the same email,
732-
// we skip the network call (the flag is still correct from the prior
733-
// search). Cleared on logout so a re-login re-evaluates fresh.
734-
private _workspaceLastSearchedEmail?: string;
728+
// Stable serialization of the identifier set sent in the most recent
729+
// successful search dispatch. If a subsequent identification arrives with
730+
// an identical set, we skip the network call (the flag is still correct
731+
// from the prior search). Keyed over the full IUserIdentities map — not
732+
// just email — so partners passing hashed email through `other`/`other2-10`
733+
// or any other identifier benefit from the same dedupe. Cleared on logout
734+
// so a re-login re-evaluates fresh.
735+
private _workspaceLastSearchedIdentitiesKey?: string;
735736

736737
// ---- Private helpers ----
737738

@@ -836,7 +837,7 @@ class RoktKit implements KitInterface {
836837
return {};
837838
}
838839

839-
const userIdentities = filteredUser.getUserIdentities().userIdentities;
840+
const userIdentities: IUserIdentities = filteredUser.getUserIdentities().userIdentities;
840841

841842
return this.replaceOtherIdentityWithEmailsha256(userIdentities);
842843
}
@@ -851,11 +852,11 @@ class RoktKit implements KitInterface {
851852
return mp().Rokt.getLocalSessionAttributes!();
852853
}
853854

854-
private replaceOtherIdentityWithEmailsha256(userIdentities: Record<string, string>): Record<string, string> {
855+
private replaceOtherIdentityWithEmailsha256(userIdentities: IUserIdentities): Record<string, string> {
855856
const newUserIdentities: Record<string, string> = { ...(userIdentities || {}) };
856857
const key = this._mappedEmailSha256Key;
857-
if (key && userIdentities[key]) {
858-
newUserIdentities[RoktKit.EMAIL_SHA256_KEY] = userIdentities[key];
858+
if (key && userIdentities[key as keyof IUserIdentities]) {
859+
newUserIdentities[RoktKit.EMAIL_SHA256_KEY] = userIdentities[key as keyof IUserIdentities] as string;
859860
}
860861
if (key) {
861862
delete newUserIdentities[key];
@@ -1255,48 +1256,76 @@ class RoktKit implements KitInterface {
12551256
const apiKey = this._workspaceIdSyncApiKey;
12561257
if (!apiKey) {
12571258
this.userIdentifiedInWorkspace = false;
1258-
this._workspaceLastSearchedEmail = undefined;
1259+
this._workspaceLastSearchedIdentitiesKey = undefined;
12591260
return Promise.resolve();
12601261
}
12611262
const search = mp().Identity?.search;
12621263
if (typeof search !== 'function') {
12631264
this.userIdentifiedInWorkspace = false;
1264-
this._workspaceLastSearchedEmail = undefined;
1265+
this._workspaceLastSearchedIdentitiesKey = undefined;
12651266
return Promise.resolve();
12661267
}
1267-
const userIdentities = filteredUser.getUserIdentities ? filteredUser.getUserIdentities().userIdentities : null;
1268-
const email = userIdentities?.email;
1269-
if (!email || !isString(email)) {
1268+
1269+
const userIdentities: IUserIdentities | null = filteredUser.getUserIdentities
1270+
? filteredUser.getUserIdentities().userIdentities
1271+
: null;
1272+
1273+
// Forward every non-empty string identifier the user has — email,
1274+
// customerid, other/other2-10 (commonly used for hashed email),
1275+
// mobile_number, facebook, etc. The host SDK's Identity.search accepts
1276+
// the full IUserIdentities surface and the server validates it.
1277+
const knownIdentities: Record<string, string> = {};
1278+
if (userIdentities) {
1279+
for (const key of Object.keys(userIdentities) as Array<keyof IUserIdentities>) {
1280+
const value = userIdentities[key];
1281+
if (isString(value) && value.length > 0) {
1282+
knownIdentities[key] = value;
1283+
}
1284+
}
1285+
}
1286+
1287+
const identityKeys = Object.keys(knownIdentities);
1288+
if (identityKeys.length === 0) {
12701289
this.userIdentifiedInWorkspace = false;
1271-
this._workspaceLastSearchedEmail = undefined;
1290+
this._workspaceLastSearchedIdentitiesKey = undefined;
12721291
return Promise.resolve();
12731292
}
12741293

1275-
// Same email as the last successful dispatch → skip the network call.
1276-
// The current flag value still reflects the correct match status.
1277-
if (email === this._workspaceLastSearchedEmail) {
1294+
// Stable cache key: sort keys so insertion-order differences don't
1295+
// cause false misses. The values are partner-supplied strings; no
1296+
// hashing needed — equality on this serialization is sufficient.
1297+
const identitiesKey = identityKeys
1298+
.sort()
1299+
.map((k) => `${k}=${knownIdentities[k]}`)
1300+
.join('&');
1301+
1302+
// Same identifier set as the last successful dispatch → skip the
1303+
// network call. The current flag value still reflects the correct
1304+
// match status.
1305+
if (identitiesKey === this._workspaceLastSearchedIdentitiesKey) {
12781306
return Promise.resolve();
12791307
}
12801308

1281-
// New / different email → reset and re-search. Cache the email up front
1282-
// so a second concurrent invocation with the same email also dedupes.
1309+
// New / different identifier set → reset and re-search. Cache the key
1310+
// up front so a second concurrent invocation with the same set also
1311+
// dedupes.
12831312
this.userIdentifiedInWorkspace = false;
1284-
this._workspaceLastSearchedEmail = email;
1313+
this._workspaceLastSearchedIdentitiesKey = identitiesKey;
12851314

12861315
return new Promise<void>((resolve) => {
12871316
try {
1288-
search(apiKey, { email }, (result: WorkspaceIdSyncResult) => {
1317+
search(apiKey, knownIdentities as IUserIdentities, (result: WorkspaceIdSyncResult) => {
12891318
if (result?.httpCode === 200) {
12901319
this.userIdentifiedInWorkspace = true;
12911320
}
12921321
resolve();
12931322
});
12941323
} catch (err) {
12951324
console.error('Rokt Kit: Workspace IDSync search failed', err);
1296-
// Dispatch failed — clear the cache so the same email can retry on
1297-
// the next identification rather than being stuck behind a poisoned
1298-
// entry that short-circuits future searches.
1299-
this._workspaceLastSearchedEmail = undefined;
1325+
// Dispatch failed — clear the cache so the same identifier set
1326+
// can retry on the next identification rather than being stuck
1327+
// behind a poisoned entry that short-circuits future searches.
1328+
this._workspaceLastSearchedIdentitiesKey = undefined;
13001329
resolve();
13011330
}
13021331
});
@@ -1308,12 +1337,12 @@ class RoktKit implements KitInterface {
13081337

13091338
public onLogoutComplete(user: IMParticleUser, _filteredIdentityRequest: unknown): string {
13101339
// Anonymous sessions must not carry the previous user's match forward.
1311-
// Clear the flag explicitly here. Also clear the email cache so a
1312-
// re-login (possibly the same email) dispatches a fresh search rather
1313-
// than reusing a stale answer.
1340+
// Clear the flag explicitly here. Also clear the identities cache so a
1341+
// re-login (possibly with the same identifiers) dispatches a fresh
1342+
// search rather than reusing a stale answer.
13141343
this.userIdentifiedInWorkspace = false;
13151344
this._workspaceSearchInFlightPromise = null;
1316-
this._workspaceLastSearchedEmail = undefined;
1345+
this._workspaceLastSearchedIdentitiesKey = undefined;
13171346
return this.handleIdentityComplete(user, ROKT_IDENTITY_EVENT_TYPE.LOGOUT, 'onLogoutComplete');
13181347
}
13191348

0 commit comments

Comments
 (0)