Skip to content

Commit 40e2972

Browse files
authored
fix: Allow all user identity types in Identity.search (#1256)
1 parent 2b249b9 commit 40e2972

6 files changed

Lines changed: 120 additions & 37 deletions

File tree

src/identity-utils.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
import { IStore } from './store';
1717
import type { IMParticleWebSDKInstance } from './mp-instance';
1818
import {
19-
IIdentitySearchKnownIdentities,
2019
IIdentitySearchRequestBody,
2120
IdentitySearchCallback,
2221
sendSearchRequest,
@@ -358,7 +357,7 @@ export const buildIdentitySearchEnvelope = (
358357
export const executeSearchRequest = (
359358
mpInstance: IMParticleWebSDKInstance,
360359
workspaceApiKey: string,
361-
knownIdentities: IIdentitySearchKnownIdentities,
360+
knownIdentities: UserIdentities,
362361
callback: IdentitySearchCallback,
363362
): void => {
364363
const { _Helpers, _Store, Logger, _ErrorReportingDispatcher } = mpInstance;

src/identity.interfaces.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@ import {
1212
mParticleUserCart,
1313
IIdentityResponse,
1414
} from './identity-user-interfaces';
15-
import {
16-
IIdentitySearchKnownIdentities,
17-
IdentitySearchCallback,
18-
} from './identity/search';
15+
import { IdentitySearchCallback } from './identity/search';
1916
const { platform, sdkVendor, sdkVersion, HTTPCodes } = Constants;
2017

2118
export type IdentityPreProcessResult = {
@@ -173,18 +170,40 @@ export interface SDKIdentityApi {
173170
*/
174171
search?(
175172
workspaceApiKey: string,
176-
knownIdentities: IIdentitySearchKnownIdentities,
173+
knownIdentities: UserIdentities,
177174
callback: IdentitySearchCallback
178175
): void;
179176
}
180177

181178
export type {
182-
IIdentitySearchKnownIdentities,
183179
IIdentitySearchResult,
184180
IIdentitySearchResponseBody,
185181
IdentitySearchCallback,
186182
} from './identity/search';
187183

184+
export interface IUserIdentities {
185+
customerid?: string;
186+
email?: string;
187+
other?: string;
188+
other2?: string;
189+
other3?: string;
190+
other4?: string;
191+
other5?: string;
192+
other6?: string;
193+
other7?: string;
194+
other8?: string;
195+
other9?: string;
196+
other10?: string;
197+
mobile_number?: string;
198+
phone_number_2?: string;
199+
phone_number_3?: string;
200+
facebook?: string;
201+
facebookcustomaudienceid?: string;
202+
google?: string;
203+
twitter?: string;
204+
microsoft?: string;
205+
yahoo?: string;
206+
}
188207
export interface IIdentity {
189208
audienceManager: AudienceManager;
190209
idCache: BaseVault<Dictionary<ICachedIdentityCall>>;

src/identity.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ export default function Identity(mpInstance) {
745745
*
746746
* @method search
747747
* @param {String} workspaceApiKey Workspace API key (sent as x-mp-key).
748-
* @param {Object} knownIdentities `{ email: string }`
748+
* @param {Object} knownIdentities A `UserIdentities` map.
749749
* @param {Function} callback Invoked with the `IIdentitySearchResult`.
750750
*/
751751
search: function(workspaceApiKey, knownIdentities, callback) {

src/identity/search.ts

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,11 @@ import {
1212
IErrorReportingService,
1313
WSDKErrorSeverity,
1414
} from '../reporting/types';
15+
import { IdentityApiData, UserIdentities } from '@mparticle/web-sdk';
16+
import Validators from '../validators';
1517

1618
const { HTTPCodes } = Constants;
1719

18-
/**
19-
* Shape of `known_identities` accepted by `search`.
20-
*
21-
* The IDSync `/v1/search` endpoint accepts the same identity keys as
22-
* `/v1/identify`, but for v1 of this client API we only support `email`.
23-
* Additional identity types can be added here in the future without breaking
24-
* existing consumers.
25-
*/
26-
export interface IIdentitySearchKnownIdentities {
27-
email: string;
28-
}
29-
3020
/**
3121
* Body payload returned by the `/v1/search` endpoint, as parsed JSON.
3222
*
@@ -72,7 +62,7 @@ export interface IIdentitySearchRequestBody {
7262
environment: Environment;
7363
request_id: string;
7464
request_timestamp_ms: number;
75-
known_identities: IIdentitySearchKnownIdentities;
65+
known_identities: UserIdentities;
7666
}
7767

7868
interface IIdentitySearchPayload extends IFetchPayload {
@@ -88,17 +78,17 @@ interface IIdentitySearchPayload extends IFetchPayload {
8878
* with the HTTP status and parsed body.
8979
*
9080
* Defensive contract:
91-
* - Missing/invalid `email` -> callback with `{ httpCode: noHttpCoverage }`,
92-
* no network call.
93-
* - Missing `apiKey` -> callback with `{ httpCode: noHttpCoverage }`,
81+
* - No identifier with a non-empty string value -> callback with
82+
* `{ httpCode: noHttpCoverage }`, no network call.
83+
* - Missing `apiKey` -> callback with `{ httpCode: noHttpCoverage }`,
9484
* no network call.
9585
* - Network/JSON-parse errors are caught and surfaced via the callback,
9686
* never thrown. Network errors are also reported through the optional
9787
* `errorReporter` so any registered IErrorReportingService can observe
9888
* them (matches the pattern used by identifyRequest in identityApiClient).
9989
*/
10090
export const sendSearchRequest = async (
101-
knownIdentities: IIdentitySearchKnownIdentities,
91+
knownIdentities: UserIdentities,
10292
apiKey: string,
10393
requestBuilder: () => Omit<IIdentitySearchRequestBody, 'known_identities'>,
10494
searchUrl: string,
@@ -127,11 +117,20 @@ export const sendSearchRequest = async (
127117
}
128118
};
129119

130-
// No valid email -> deliver httpCode: noHttpCoverage so callers waiting on
131-
// the callback (e.g. to clear a loading state) don't hang.
132-
if (!knownIdentities || typeof knownIdentities.email !== 'string' || !knownIdentities.email) {
120+
const cleanedKnownIdentities: UserIdentities = Validators.removeFalsyIdentityValues(
121+
{ userIdentities: knownIdentities ?? {} } as IdentityApiData,
122+
logger,
123+
).userIdentities;
124+
125+
// No usable identifier -> deliver httpCode: noHttpCoverage so callers
126+
// waiting on the callback (e.g. to clear a loading state) don't hang.
127+
if (
128+
!Object.values(cleanedKnownIdentities ?? {}).some(
129+
(v) => typeof v === 'string' && v.length > 0,
130+
)
131+
) {
133132
logger.verbose(
134-
'search called without a valid email; skipping request.',
133+
'Identity search called with empty identifiers; skipping request.',
135134
);
136135
safeInvoke({ httpCode: HTTPCodes.noHttpCoverage });
137136
return;
@@ -153,11 +152,10 @@ export const sendSearchRequest = async (
153152
// rejecting and the caller hanging on a never-fired callback.
154153
try {
155154
const requestEnvelope = requestBuilder();
155+
156156
const requestBody: IIdentitySearchRequestBody = {
157157
...requestEnvelope,
158-
known_identities: {
159-
email: knownIdentities.email,
160-
},
158+
known_identities: { ...cleanedKnownIdentities },
161159
};
162160

163161
const fetchPayload: IIdentitySearchPayload = {

src/public-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ export type {
5656
IAliasCallback,
5757
IAliasResult,
5858
SDKIdentityTypeEnum,
59-
IIdentitySearchKnownIdentities,
6059
IIdentitySearchResult,
6160
IIdentitySearchResponseBody,
6261
IdentitySearchCallback,
62+
IUserIdentities
6363
} from './identity.interfaces';
6464

6565
// eCommerce

test/src/tests-search.ts

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ describe('search', () => {
196196
expect(requestBuilderSpy.called).to.eq(false);
197197
});
198198

199-
it('invokes the callback with noHttpCoverage when knownIdentities.email is missing or invalid (no network)', async () => {
199+
it('invokes the callback with noHttpCoverage when no identifier has a non-empty string value (no network)', async () => {
200200
const callback = sinon.spy();
201201

202202
await sendSearchRequest(
@@ -218,15 +218,15 @@ describe('search', () => {
218218
);
219219

220220
await sendSearchRequest(
221-
({ email: 12345 } as any),
221+
({ email: 12345, other: null } as any),
222222
apiKey,
223223
buildEnvelope,
224224
searchUrl,
225225
callback,
226226
logger,
227227
);
228228

229-
// Missing/invalid email: no network, but callback fires for each
229+
// No usable identifier: no network, but callback fires for each
230230
// call so callers can resolve any pending loading state.
231231
expect(fetchMock.calls(searchUrl).length).to.equal(0);
232232
expect(callback.callCount).to.equal(3);
@@ -236,6 +236,73 @@ describe('search', () => {
236236
}
237237
});
238238

239+
it('forwards non-email identifiers (e.g. hashed email in `other`) to the wire', async () => {
240+
fetchMock.post(searchUrl, {
241+
status: 200,
242+
body: JSON.stringify({ mpid: '42' }),
243+
});
244+
245+
const callback = sinon.spy();
246+
await sendSearchRequest(
247+
{ other: 'sha256:abc123', customerid: 'cust-1' },
248+
apiKey,
249+
buildEnvelope,
250+
searchUrl,
251+
callback,
252+
logger,
253+
);
254+
255+
expect(fetchMock.calls(searchUrl).length).to.equal(1);
256+
const sentBody = JSON.parse(
257+
fetchMock.calls(searchUrl)[0][1].body as string,
258+
);
259+
expect(sentBody.known_identities).to.deep.equal({
260+
other: 'sha256:abc123',
261+
customerid: 'cust-1',
262+
});
263+
expect(callback.calledOnce).to.eq(true);
264+
const result = callback.getCall(0).args[0] as IIdentitySearchResult;
265+
expect(result.httpCode).to.equal(200);
266+
});
267+
268+
it('strips falsy identity values but preserves null on the wire', async () => {
269+
// removeFalsyIdentityValues drops undefined / 0 / false / '' but
270+
// explicitly preserves null (the server accepts it as a "no value"
271+
// sentinel). Anything with at least one non-empty string value
272+
// still triggers the network call.
273+
fetchMock.post(searchUrl, {
274+
status: 200,
275+
body: JSON.stringify({ mpid: '99' }),
276+
});
277+
278+
const callback = sinon.spy();
279+
await sendSearchRequest(
280+
({
281+
email: 'valid@example.com',
282+
customerid: null,
283+
other: '',
284+
} as any),
285+
apiKey,
286+
buildEnvelope,
287+
searchUrl,
288+
callback,
289+
logger,
290+
);
291+
292+
expect(fetchMock.calls(searchUrl).length).to.equal(1);
293+
const sentBody = JSON.parse(
294+
fetchMock.calls(searchUrl)[0][1].body as string,
295+
);
296+
// `other: ''` stripped, `customerid: null` preserved on the wire.
297+
expect(sentBody.known_identities).to.deep.equal({
298+
email: 'valid@example.com',
299+
customerid: null,
300+
});
301+
expect(callback.calledOnce).to.eq(true);
302+
const result = callback.getCall(0).args[0] as IIdentitySearchResult;
303+
expect(result.httpCode).to.equal(200);
304+
});
305+
239306
it('catches network errors and surfaces noHttpCoverage via the callback (not thrown)', async () => {
240307
fetchMock.post(searchUrl, { throws: new Error('network down') });
241308

0 commit comments

Comments
 (0)