feat: Add Identity.search for IDSync Search#1255
feat: Add Identity.search for IDSync Search#1255rmi22186 wants to merge 23 commits intodevelopmentfrom
Conversation
Adds a new public API mParticle.Identity.searchAdvertiser(apiKey, knownIdentities, callback) that POSTs to mParticle's IDSync /v1/search endpoint so kits (initially the Rokt Web Kit) can look up an advertiser identity (today, only email) without affecting the current user. The advertiser API key is supplied explicitly by the caller rather than read from the SDK's workspace token, so advertiser searches can be authorised independently of the host SDK's workspace. Missing apiKey, missing/invalid email, and a non-function callback all return silently (no network call, no callback) so the path is inert when the consumer hasn't opted in. Note: a separate Fastly CORS configuration is required to permit the x-mp-key header on /v1/search; this SDK code is built assuming that allow-list will land separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The IDSync /v1/search endpoint requires Basic auth (or HMAC); key-only auth via x-mp-key is not provisioned at the Fastly edge. Update the public API to take an explicit `secret` argument and send `Authorization: Basic <base64(apiKey:secret)>` instead of `x-mp-key`. Public signature: Identity.searchAdvertiser(apiKey, secret, knownIdentities, callback) Both apiKey and secret are required; either being missing/empty makes the call silently inert (no network, no callback). For Web workspaces the "secret" ships in the browser bundle and is not actually a secret, but the Basic scheme still requires it as the password component. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ secret)" This reverts commit 5382278.
PR SummaryMedium Risk Overview Implements a typed request/envelope builder and network sender ( Updates preload snippets/stubs and instance-manager proxies to surface Reviewed by Cursor Bugbot for commit e47d52a. Bugbot is set up for automated code reviews on this repo. Configure here. |
…cher Match the pattern in identityApiClient.sendIdentityRequest: on a thrown network error, log to console AND push a structured ISDKError through the dispatcher so any registered listener (e.g. the Rokt Web Kit's ErrorReportingService, registered via _registerErrorReportingService) can observe the failure. Adds an optional `errorReporter?: IErrorReportingService` parameter to sendSearchAdvertiserRequest. The Identity.searchAdvertiser public wrapper passes mpInstance._ErrorReportingDispatcher through. Mirrors identifyRequest's error code and severity: code: ErrorCodes.IDENTITY_REQUEST severity: WSDKErrorSeverity.ERROR Validation paths (missing apiKey, missing/invalid email, non-function callback) remain silently inert — they're consumer-contract failures, not infrastructure errors, and the existing identity routes don't report on those either. Tests: - reports a structured error through errorReporter on network failure - does not throw when errorReporter is omitted on network failure Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two findings from the automated review on PR #1255: 1. Validation paths now invoke the callback with `httpCode: noHttpCoverage` instead of returning silently. The earlier silent-return refactor contradicted the function's documented contract and broke parity with other identity methods. A consumer holding a loading state on the callback would have hung indefinitely on missing email or apiKey. 2. `Identity.searchAdvertiser` now gates on `_Helpers.canLog()` before making the network call, mirroring identify/login/logout/modify. Without this, an opted-out user (`setOptOut(true)`) would still POST their email to /v1/search. On guard fail the callback fires with `httpCode: loggingDisabledOrMissingAPIKey`, matching the existing identity-route precedent. Tests updated to assert callback invocation on validation failures and on the opt-out path (1077 passing on ChromeHeadless, +1 net). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The opt-out short-circuit in `Identity.searchAdvertiser` was using
`mpInstance._Helpers.invokeCallback`, which delivers the standard
Identity callback shape `{ httpCode, body, getUser, getPreviousUser }`.
That contradicts the `ISearchAdvertiserResult` contract
(`{ httpCode, body? }` with `body` typed as a parsed JSON object) and
would cause a consumer that did `if (result.body) result.body.mpid` to
crash on the abandon-log message string.
Invoke the callback directly with `{ httpCode: loggingDisabledOrMissingAPIKey }`
so all skipped-request paths in `searchAdvertiser` deliver a consistent,
typed result. Strengthens the opt-out test to assert `body`, `getUser`
and `getPreviousUser` are absent from the delivered result.
Addresses cursor-bot review on commit bb58f23.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The four locals introduced in `Identity.searchAdvertiser` (`serviceUrl`, `searchUrl`, `environment`, `requestBuilder`) are never reassigned, so declare them with `const`. The surrounding file uses `var` for legacy reasons but new PR code should prefer `const` where possible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cursor-bot review on PR #1255 (low severity) flagged that `requestBuilder()`, `JSON.stringify(requestBody)`, and uploader construction all executed outside the try/catch block. If any of those threw, the async function rejected, but the caller in identity.js discards the returned Promise (no await, no .catch) — turning it into an unhandled rejection and leaving the consumer's callback unfired, which could hang any loading state the consumer was holding open. None of those operations are likely to throw in practice (request shape is plain objects, no circular refs; uploaders only fail in exotic environments), but the documented contract is "callback always fires," so defense in depth is the right call. Move the existing try opener up to wrap request setup as well — the existing catch already calls safeInvoke({ httpCode: HTTPCodes.noHttpCoverage }) and reports through the dispatcher, so the contract holds without any new code. Test: - catches errors thrown during request setup (e.g. requestBuilder) and surfaces noHttpCoverage via the callback — passes a builder that throws and asserts the callback fires with noHttpCoverage, no exception propagates, and no network call happens. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small changes to the IDSync searchAdvertiser API in identity.js
plus a related import cleanup:
1. JSDoc cleanup. Removed two misleading lines:
- "Both 200 (match) and 404 (no match) are expected steady-state
outcomes. Consumers should gate behaviour on httpCode === 200."
The 200/404 enumeration was incomplete (the endpoint can also
return 400/401/429/5xx), and the gate-on-200 advice is the
consumer's call, not a contract this SDK enforces.
- "v1 only supports email in knownIdentities." Misleading because
/v1/search itself accepts the full IdentityTypeDto enum
(customerid, ios_idfa, etc.). The single-email constraint is a
self-imposed SDK type choice in ISearchAdvertiserKnownIdentities,
not a server limitation.
2. Renamed parameter `apiKey` → `advertiserApiKey` to make it clear
the caller passes the advertiser's workspace key — not the SDK's
own _Store.devToken.
3. Appended a static cache-bust query parameter (`search?abc=123`) to
the request URL. This is a workaround for Fastly's OPTIONS preflight
cache poisoning on /v1/search: by giving this SDK's traffic a
distinct URL, we get a separate cache slot at the edge, sidestepping
the stale `Access-Control-Allow-Headers` body that's currently
served for naked /v1/search. The proper fix is the
`Vary: Access-Control-Request-Headers` header on origin (mpserver
PR), at which point this query parameter can be removed.
Also drops the now-unused ISearchAdvertiserResult import from
identity.interfaces.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop `?abc=123` cache-buster from /v1/search URL (no longer needed). - Propagate `apiKey` -> `advertiserApiKey` rename to the public TS surface (`identity.interfaces.ts`) and the instance-manager wrapper so IDE/IntelliSense matches the implementation. - Sync test mock URL with the SDK's actual request URL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Trim JSDoc on `searchAdvertiser` (interface + identity.js) and remove the internal callback-shape note from identity.js. - Drop the Fastly/CORS NOTE from `searchAdvertiser.ts` — unblocked. - Delete the redundant "does not throw when errorReporter is omitted" test; the case is already covered by the prior "catches network errors" test, which calls `sendSearchAdvertiserRequest` without an `errorReporter`. Removing it clears the SonarCloud duplicated-block in this test file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "advertiser" terminology was Rokt-internal context. mParticle customers don't share that vocabulary — the underlying concept is a workspace-scoped IDSync search. Rename the public API and all supporting types/files to use "workspace" so the surface reads naturally for any consumer. Public surface changes (pre-release, no consumers yet): - `mParticle.Identity.searchAdvertiser` -> `mParticle.Identity.searchWorkspace` - `advertiserApiKey` parameter -> `workspaceApiKey` - `ISearchAdvertiser*` types -> `ISearchWorkspace*` - `SearchAdvertiserCallback` -> `SearchWorkspaceCallback` File renames: - `src/searchAdvertiser.ts` -> `src/searchWorkspace.ts` - `test/src/tests-search-advertiser.ts` -> `test/src/tests-search-workspace.ts` Paired with the Rokt kit rename in mparticle-javascript-integration-rokt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Note: Check to see if onIdentifyComplete is ever called when identify fails. |
Aligns the new IDSync search primitive with the existing public Identity API surface (`identify`/`login`/`logout`/`modify`/`aliasUsers` /`search`). The "Workspace" qualifier on the method/types/file leaked the kit-level feature name into the SDK primitive — the SDK doesn't need to be told *what* the caller is searching, only *how*. The caller's workspace API key is still surfaced via the `workspaceApiKey` parameter, which keeps the auth contract explicit without coloring the method name. - `mParticle.Identity.searchWorkspace` -> `mParticle.Identity.search` - `sendSearchWorkspaceRequest` -> `sendSearchRequest` - `ISearchWorkspace*` types -> `ISearch*`; `SearchWorkspaceCallback` -> `SearchCallback` - `src/searchWorkspace.ts` -> `src/search.ts` - `test/src/tests-search-workspace.ts` -> `test/src/tests-search.ts` Paired with the Rokt kit rename in mparticle-javascript-integration-rokt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Identity.search wrapper body lived in identity.js — opt-out gating, URL construction, request envelope building, and dispatch to sendSearchRequest, all in plain JS. Move that glue into a typed `executeSearchRequest(mpInstance, workspaceApiKey, knownIdentities, callback)` helper in identity-utils.ts so the SDK wiring is type-checked against `IMParticleWebSDKInstance`, `ISearchKnownIdentities`, `ISearchRequestBody`, and `SearchCallback` instead of being implicit. Behaviour is unchanged — identity.js's search method now delegates in one line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| ISearchKnownIdentities, | ||
| SearchCallback, |
There was a problem hiding this comment.
Nit:
| ISearchKnownIdentities, | |
| SearchCallback, | |
| IIdentitySearchKnownIdentities, | |
| IdentitySearchCallback, |
| mpInstance.Logger.verbose( | ||
| Messages.InformationMessages.AbandonLogEvent | ||
| ); | ||
| if (mpInstance._Helpers.Validators.isFunction(callback)) { |
There was a problem hiding this comment.
This can be pulled from utils.ts instead of Validators.
| if (mpInstance._Helpers.Validators.isFunction(callback)) { | |
| if (isFunction(callback)) { |
| } catch (e) { | ||
| mpInstance.Logger.error( | ||
| 'Error invoking search callback: ' + | ||
| ((e && e.message) || String(e)) |
There was a problem hiding this comment.
I like the robot's suggestion below.
| sdk_version: Constants.sdkVersion, | ||
| }, | ||
| environment: environment, | ||
| request_id: mpInstance._Helpers.generateUniqueId(), |
There was a problem hiding this comment.
Use utils.ts.
| request_id: mpInstance._Helpers.generateUniqueId(), | |
| request_id: generateUniqueId(), |
| // Build the same envelope that /v1/identify uses (client_sdk, | ||
| // request_id, request_timestamp_ms, environment) so the IDSync | ||
| // service can correlate requests across endpoints. | ||
| const requestBuilder = function() { |
There was a problem hiding this comment.
Nit: can we extract this method outside of the function to make it more readable?
| ); | ||
| const searchUrl = serviceUrl + 'search?cb=1'; | ||
|
|
||
| const environment = mpInstance._Store.SDKConfig.isDevelopmentMode |
There was a problem hiding this comment.
I think you can destructure a lot of these configvalues to make the code more readable.
| @@ -0,0 +1,234 @@ | |||
| import Constants, { HTTP_OK, HTTP_NOT_FOUND } from './constants'; | |||
There was a problem hiding this comment.
I think we should put this in src/identity/search.ts so that we can eventually split out the other identity functions into their own space for cleaner organization.
Address PR review nit (alexs-mparticle on #1255): the `ISearch*` / `SearchCallback` family is too generic for the public types surface — scope them to `Identity` so they read clearly alongside the rest of the IDSync types. - ISearchKnownIdentities -> IIdentitySearchKnownIdentities - ISearchResponseBody -> IIdentitySearchResponseBody - ISearchResult -> IIdentitySearchResult - ISearchRequestBody -> IIdentitySearchRequestBody - ISearchPayload -> IIdentitySearchPayload - SearchCallback -> IdentitySearchCallback Function names (`sendSearchRequest`, `executeSearchRequest`) and the public method (`Identity.search`) are unchanged — they're already contextually scoped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Use `isFunction` from utils.ts instead of `Validators.isFunction` (r3169167597). - Use `generateUniqueId` from utils.ts instead of `mpInstance._Helpers.generateUniqueId()` (r3169189020). - Destructure `_Helpers`/`_Store`/`Logger`/`_ErrorReportingDispatcher` and `identityUrl`/`isDevelopmentMode` from `mpInstance`/`SDKConfig` in `executeSearchRequest` for readability (r3169201394). - Extract the IDSync envelope construction into a standalone `buildIdentitySearchEnvelope(environment)` helper so it can be unit-tested independently of the full request flow. - Add four unit tests for `buildIdentitySearchEnvelope` (SDK identifiers + supplied environment, development branch, fresh request_id per call, no `known_identities` leakage). - Move `src/search.ts` to `src/identity/search.ts` so future identity primitives can be grouped under one namespace (r3169209572); update all import paths and the relative imports inside the moved file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| ): Promise<void> => { | ||
| // Validate the callback up front. If it isn't a function we have nowhere | ||
| // to deliver a result to, so log and bail out without invoking anything. | ||
| if (typeof callback !== 'function') { |
There was a problem hiding this comment.
utils.ts.
| if (typeof callback !== 'function') { | |
| if (!isFunction(callback)) { |
|
|
||
| // FetchUploader returns a real Response with .json(); XHRUploader | ||
| // returns an XHR-shaped object with `responseText`. We tolerate both. | ||
| if (typeof (response as Response).json === 'function') { |
…arch - Use `isFunction` from utils.ts at the two `typeof X === 'function'` call sites in `src/identity/search.ts` (lines 111 and 185), per Alex's nits on the latest review pass. - Add `'search'` to the snippet pre-init `identityMethods` arrays in `snippet.js` and `snippet.rokt.js`, and add `search: voidFunction` to `src/stub/mparticle.stub.js` — without these, `Identity.search` doesn't get a queued stub, so a kit calling `mParticle.Identity.search(...)` before the SDK loads would throw `TypeError` instead of being replayed after init (cursor-bot bug flagged on identity.js:758). Email validation (Alex's nit on line 131) is intentionally not added: the IDSync backend accepts any string and validation belongs at the client site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cursor-bot caught (PR #1255 r3170092708): the destructured local `_ErrorReportingDispatcher` was unused — the call to `sendSearchRequest` still went through `mpInstance._ErrorReportingDispatcher`. Use the local now that it's been destructured, matching the pattern in `identityApiClient.ts` (`_ErrorReportingDispatcher: errorReporter`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new tests exercising the pre-init queueing path and the offline stub for the new `Identity.search` method: - `test/snippet/tests-snippet.js`: extends the "mParticle object should proxy Identity methods" case to call `mParticle.Identity.search(...)` before init and assert the queued entry lands in `config.rq` as `Identity.search` with the workspace key, identities object, and callback function intact. - `test/stub/tests-mParticle-stub.js`: extends the Identity stub case to call `Identity.identify`/`login`/`logout`/`modify`/`search` and verify they don't throw (matching the void-function pattern). Also regenerates `snippet.min.js` and `snippet.rokt.min.js` so the minified bundles include the new `'search'` entry in `identityMethods`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marginal cleanup — `Date.now()` is the idiomatic way to get the current millisecond timestamp; `new Date().getTime()` allocates a Date object just to immediately read its time. Same value, no allocation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
||
| // FetchUploader returns a real Response with .json(); XHRUploader | ||
| // returns an XHR-shaped object with `responseText`. We tolerate both. | ||
| if (isFunction((response as Response).json)) { |
There was a problem hiding this comment.
I think the robot is right. You can probably remove as Response.
Co-authored-by: Alex S <49695018+alexs-mparticle@users.noreply.github.com>
Two follow-ups from Alex's review pass on the latest commits: - Fix the unresolved `Environment` reference in `executeSearchRequest` (suggestion 723e7e8 didn't add the import) and propagate the named type to `buildIdentitySearchEnvelope`'s parameter and to `IIdentitySearchRequestBody.environment`. `Environment` is the same `'development' | 'production'` union as before, just centralized through `Constants.Environment`. - Drop redundant `(response as Response)` assertions in `identity/search.ts` — `response` is already typed `Response` from the awaited `api.upload(...)` return, so the cast is a no-op (Sonar S4325). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e47d52a. Configure here.
| logger.verbose( | ||
| 'search response had no parseable JSON body.', | ||
| ); | ||
| } |
There was a problem hiding this comment.
JSON parse errors reported as success
Medium Severity
sendSearchRequest swallows response.json() failures and still returns the original HTTP status. A malformed or non-JSON 200 response is surfaced as success (httpCode: 200) instead of HTTPCodes.noHttpCoverage, and no structured error is reported. This can make callers treat failed IDSync responses as valid search hits.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit e47d52a. Configure here.





Summary
mParticle.Identity.search(workspaceApiKey, knownIdentities, callback)POSTs to mParticle's IDSync/v1/searchand invokes the callback with{ httpCode, body? }. Sits alongsideidentify/login/logout/modify/aliasUserson the Identity surface. The caller supplies a workspace-scoped API key (sent asx-mp-key), held separately from the host SDK's_Store.devToken, so a single install can search against an arbitrary workspace without being reconfigured.{ httpCode: 200, body }; 404 →{ httpCode: 404 }; missing key or invalid email →noHttpCoverage(no network); SDK opted out →loggingDisabledOrMissingAPIKey; network or JSON-parse failure →noHttpCoverageplus a structuredISDKErrorthrough_ErrorReportingDispatcher, mirroringidentifyRequestinidentityApiClient.ts.mpInstance._Helpers.canLog()before any network call, sosetOptOut(true)short-circuits the request and no email is transmitted — matchingidentify/login/logout/modify/aliasUsers./v1/searchbeing updated to includex-mp-key(curl already round-trips 200/404; the browser preflight is the only remaining blocker).npm run lint+npm run buildclean; Karma on ChromeHeadless 1077 passing covering 200/404, validation, opt-out, and error-reporter paths intests-search.ts. Live browser smoke test still blocked on the Fastly allowlist update.🤖 Generated with Claude Code