@mparticle/web-sdk 2.66.0 → 2.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ import { BaseVault } from './vault';
5
5
  import { Dictionary, Environment, valueof } from './utils';
6
6
  import Constants from './constants';
7
7
  import { IdentityCallback, IUserAttributeChangeEvent, IUserIdentityChangeEvent, IMParticleUser, mParticleUserCart, IIdentityResponse } from './identity-user-interfaces';
8
+ import { IdentitySearchCallback } from './identity/search';
8
9
  declare const platform: "web", sdkVendor: "mparticle", sdkVersion: string, HTTPCodes: {
9
10
  readonly noHttpCoverage: -1;
10
11
  readonly activeIdentityRequest: -2;
@@ -97,6 +98,42 @@ export interface SDKIdentityApi {
97
98
  getUsers?(): IMParticleUser[];
98
99
  aliasUsers?(aliasRequest?: IAliasRequest, callback?: IdentityCallback): void;
99
100
  createAliasRequest?(sourceUser: IMParticleUser, destinationUser: IMParticleUser, scope?: AliasRequestScope): IAliasRequest;
101
+ /**
102
+ * Sends a request to mParticle's IDSync `/v1/search` endpoint to look up
103
+ * a workspace identity without affecting the current user. The callback
104
+ * receives `httpCode` (always) and an optional `body` containing the
105
+ * parsed JSON response. Consumers should gate behaviour on
106
+ * `httpCode === 200`.
107
+ *
108
+ * `workspaceApiKey` is a workspace-specific API key supplied by the
109
+ * caller (from a kit's settings). It is sent as the `x-mp-key` header.
110
+ * The SDK's own workspace token is intentionally not used.
111
+ */
112
+ search?(workspaceApiKey: string, knownIdentities: UserIdentities, callback: IdentitySearchCallback): void;
113
+ }
114
+ export type { IIdentitySearchResult, IIdentitySearchResponseBody, IdentitySearchCallback, } from './identity/search';
115
+ export interface IUserIdentities {
116
+ customerid?: string;
117
+ email?: string;
118
+ other?: string;
119
+ other2?: string;
120
+ other3?: string;
121
+ other4?: string;
122
+ other5?: string;
123
+ other6?: string;
124
+ other7?: string;
125
+ other8?: string;
126
+ other9?: string;
127
+ other10?: string;
128
+ mobile_number?: string;
129
+ phone_number_2?: string;
130
+ phone_number_3?: string;
131
+ facebook?: string;
132
+ facebookcustomaudienceid?: string;
133
+ google?: string;
134
+ twitter?: string;
135
+ microsoft?: string;
136
+ yahoo?: string;
100
137
  }
101
138
  export interface IIdentity {
102
139
  audienceManager: AudienceManager;
@@ -114,4 +151,3 @@ export interface IIdentity {
114
151
  */
115
152
  mParticleUserCart(): mParticleUserCart;
116
153
  }
117
- export {};
@@ -16,7 +16,7 @@ export type { Batch, } from '@mparticle/event-models';
16
16
  export type { EventType, CommerceEventType, IdentityType, ProductActionType, PromotionActionType, MessageType, } from './types';
17
17
  export type { SDKInitConfig, DataPlanConfig, BaseEvent, SDKEventCustomFlags, LogLevelType, MParticleWebSDK, } from './sdkRuntimeModels';
18
18
  export type { IMParticleUser, IdentityCallback, IdentityResult, IdentityResultBody, IdentityModifyResultBody, ISDKUserIdentity, ISDKUserAttributes, } from './identity-user-interfaces';
19
- export type { SDKIdentityApi, IAliasRequest, IAliasCallback, IAliasResult, SDKIdentityTypeEnum, } from './identity.interfaces';
19
+ export type { SDKIdentityApi, IAliasRequest, IAliasCallback, IAliasResult, SDKIdentityTypeEnum, IIdentitySearchResult, IIdentitySearchResponseBody, IdentitySearchCallback, IUserIdentities } from './identity.interfaces';
20
20
  export type { SDKECommerceAPI, SDKCart, } from './ecommerce.interfaces';
21
21
  export type { SDKProduct, SDKPromotion, SDKImpression, SDKProductImpression, } from './sdkRuntimeModels';
22
22
  export type { SDKConsentApi, SDKConsentState, SDKConsentStateData, SDKGDPRConsentState, SDKCCPAConsentState, } from './consent';
@@ -57,6 +57,7 @@ declare namespace mParticle {
57
57
  export { voidFunction as login };
58
58
  export { voidFunction as logout };
59
59
  export { voidFunction as modify };
60
+ export { voidFunction as search };
60
61
  }
61
62
  }
62
63
  declare function voidFunction(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mparticle/web-sdk",
3
- "version": "2.66.0",
3
+ "version": "2.67.0",
4
4
  "description": "mParticle core SDK for web applications",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -0,0 +1,233 @@
1
+ import Constants, { HTTP_OK, HTTP_NOT_FOUND } from '../constants';
2
+ import { SDKLoggerApi } from '../sdkRuntimeModels';
3
+ import { Environment, isFunction } from '../utils';
4
+ import {
5
+ AsyncUploader,
6
+ FetchUploader,
7
+ IFetchPayload,
8
+ XHRUploader,
9
+ } from '../uploaders';
10
+ import {
11
+ ErrorCodes,
12
+ IErrorReportingService,
13
+ WSDKErrorSeverity,
14
+ } from '../reporting/types';
15
+ import { IdentityApiData, UserIdentities } from '@mparticle/web-sdk';
16
+ import Validators from '../validators';
17
+
18
+ const { HTTPCodes } = Constants;
19
+
20
+ /**
21
+ * Body payload returned by the `/v1/search` endpoint, as parsed JSON.
22
+ *
23
+ * The shape mirrors `/v1/identify` responses. All fields are optional because
24
+ * non-200 responses (e.g. 404 NOT_FOUND_ERROR) may include partial or
25
+ * error-shaped bodies, and the consumer should only rely on body fields when
26
+ * `httpCode === 200`.
27
+ */
28
+ export interface IIdentitySearchResponseBody {
29
+ context?: string | null;
30
+ mpid?: string;
31
+ matched_identities?: Record<string, string>;
32
+ is_ephemeral?: boolean;
33
+ is_logged_in?: boolean;
34
+ }
35
+
36
+ /**
37
+ * Result delivered to the consumer's callback. `httpCode` is always present;
38
+ * `body` is present whenever the response had a parseable JSON body.
39
+ *
40
+ * For non-network errors (missing API key, validation failures, JSON parse
41
+ * errors) `httpCode` will be `HTTPCodes.noHttpCoverage` (-1) and `body` will
42
+ * be omitted. The consumer is expected to gate behaviour on
43
+ * `httpCode === 200`.
44
+ */
45
+ export interface IIdentitySearchResult {
46
+ httpCode: number;
47
+ body?: IIdentitySearchResponseBody;
48
+ }
49
+
50
+ export type IdentitySearchCallback = (result: IIdentitySearchResult) => void;
51
+
52
+ /**
53
+ * Body posted to `/v1/search`. Mirrors the `/v1/identify` request envelope so
54
+ * that the IDSync service can correlate requests across endpoints.
55
+ */
56
+ export interface IIdentitySearchRequestBody {
57
+ client_sdk: {
58
+ platform: string;
59
+ sdk_vendor: string;
60
+ sdk_version: string;
61
+ };
62
+ environment: Environment;
63
+ request_id: string;
64
+ request_timestamp_ms: number;
65
+ known_identities: UserIdentities;
66
+ }
67
+
68
+ interface IIdentitySearchPayload extends IFetchPayload {
69
+ headers: {
70
+ Accept: string;
71
+ 'Content-Type': string;
72
+ 'x-mp-key': string;
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Sends a POST to mParticle's IDSync Search endpoint and invokes `callback`
78
+ * with the HTTP status and parsed body.
79
+ *
80
+ * Defensive contract:
81
+ * - No identifier with a non-empty string value -> callback with
82
+ * `{ httpCode: noHttpCoverage }`, no network call.
83
+ * - Missing `apiKey` -> callback with `{ httpCode: noHttpCoverage }`,
84
+ * no network call.
85
+ * - Network/JSON-parse errors are caught and surfaced via the callback,
86
+ * never thrown. Network errors are also reported through the optional
87
+ * `errorReporter` so any registered IErrorReportingService can observe
88
+ * them (matches the pattern used by identifyRequest in identityApiClient).
89
+ */
90
+ export const sendSearchRequest = async (
91
+ knownIdentities: UserIdentities,
92
+ apiKey: string,
93
+ requestBuilder: () => Omit<IIdentitySearchRequestBody, 'known_identities'>,
94
+ searchUrl: string,
95
+ callback: IdentitySearchCallback,
96
+ logger: SDKLoggerApi,
97
+ uploader?: AsyncUploader,
98
+ errorReporter?: IErrorReportingService,
99
+ ): Promise<void> => {
100
+ // Validate the callback up front. If it isn't a function we have nowhere
101
+ // to deliver a result to, so log and bail out without invoking anything.
102
+ if (!isFunction(callback)) {
103
+ logger.error(
104
+ 'search called without a callback function; skipping request.',
105
+ );
106
+ return;
107
+ }
108
+
109
+ const safeInvoke = (result: IIdentitySearchResult): void => {
110
+ try {
111
+ callback(result);
112
+ } catch (e) {
113
+ logger.error(
114
+ 'Error invoking search callback: ' +
115
+ ((e as Error)?.message || String(e)),
116
+ );
117
+ }
118
+ };
119
+
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
+ ) {
132
+ logger.verbose(
133
+ 'Identity search called with empty identifiers; skipping request.',
134
+ );
135
+ safeInvoke({ httpCode: HTTPCodes.noHttpCoverage });
136
+ return;
137
+ }
138
+
139
+ // No API key -> same: deliver noHttpCoverage rather than hanging.
140
+ if (!apiKey) {
141
+ logger.verbose(
142
+ 'search called without a workspace API key; skipping request.',
143
+ );
144
+ safeInvoke({ httpCode: HTTPCodes.noHttpCoverage });
145
+ return;
146
+ }
147
+
148
+ // Wrap request setup AND the network call in the try/catch so any throw
149
+ // — from requestBuilder, JSON.stringify (e.g. circular refs), or
150
+ // uploader construction — flows into the catch below and the consumer's
151
+ // callback fires with noHttpCoverage rather than the async function
152
+ // rejecting and the caller hanging on a never-fired callback.
153
+ try {
154
+ const requestEnvelope = requestBuilder();
155
+
156
+ const requestBody: IIdentitySearchRequestBody = {
157
+ ...requestEnvelope,
158
+ known_identities: { ...cleanedKnownIdentities },
159
+ };
160
+
161
+ const fetchPayload: IIdentitySearchPayload = {
162
+ method: 'post',
163
+ headers: {
164
+ Accept: 'application/json',
165
+ 'Content-Type': 'application/json',
166
+ 'x-mp-key': apiKey,
167
+ },
168
+ body: JSON.stringify(requestBody),
169
+ };
170
+
171
+ const api: AsyncUploader =
172
+ uploader ||
173
+ (window.fetch
174
+ ? new FetchUploader(searchUrl)
175
+ : new XHRUploader(searchUrl));
176
+
177
+ logger.verbose('Sending search request to ' + searchUrl);
178
+ const response: Response = await api.upload(fetchPayload, searchUrl);
179
+
180
+ let body: IIdentitySearchResponseBody | undefined;
181
+
182
+ // FetchUploader returns a real Response with .json(); XHRUploader
183
+ // returns an XHR-shaped object with `responseText`. We tolerate both.
184
+ if (isFunction(response.json)) {
185
+ try {
186
+ body = (await response.json()) as IIdentitySearchResponseBody;
187
+ } catch (e) {
188
+ logger.verbose(
189
+ 'search response had no parseable JSON body.',
190
+ );
191
+ }
192
+ } else {
193
+ const xhrLike = (response as unknown) as XMLHttpRequest;
194
+ if (xhrLike?.responseText) {
195
+ try {
196
+ body = JSON.parse(xhrLike.responseText) as IIdentitySearchResponseBody;
197
+ } catch (e) {
198
+ logger.verbose(
199
+ 'search XHR response was not valid JSON.',
200
+ );
201
+ }
202
+ }
203
+ }
204
+
205
+ if (response.status === HTTP_OK) {
206
+ logger.verbose('search received 200 OK.');
207
+ } else if (response.status === HTTP_NOT_FOUND) {
208
+ // 404 NOT_FOUND_ERROR is an expected steady-state outcome and is
209
+ // intentionally not logged as an error.
210
+ logger.verbose('search received 404 (no match).');
211
+ } else {
212
+ logger.verbose(
213
+ 'search received non-success status ' + response.status,
214
+ );
215
+ }
216
+
217
+ safeInvoke({ httpCode: response.status, body });
218
+ } catch (e) {
219
+ const message = (e as Error)?.message || String(e);
220
+ const reportMessage = 'Error sending search request: ' + message;
221
+ logger.error(reportMessage);
222
+ // Mirror the identity-route pattern in identityApiClient.ts: log to
223
+ // console AND push a structured report through the dispatcher so any
224
+ // registered IErrorReportingService (e.g. the Rokt kit's) can observe
225
+ // the failure.
226
+ errorReporter?.report({
227
+ message: reportMessage,
228
+ code: ErrorCodes.IDENTITY_REQUEST,
229
+ severity: WSDKErrorSeverity.ERROR,
230
+ });
231
+ safeInvoke({ httpCode: HTTPCodes.noHttpCoverage });
232
+ }
233
+ };
@@ -1,5 +1,5 @@
1
1
  import Constants, { ONE_DAY_IN_SECONDS, MILLIS_IN_ONE_SEC } from './constants';
2
- import { Dictionary, parseNumber, isObject, generateHash, isEmpty } from './utils';
2
+ import { Dictionary, Environment, parseNumber, isObject, generateHash, generateUniqueId, isEmpty, isFunction } from './utils';
3
3
  import { BaseVault } from './vault';
4
4
  import Types from './types';
5
5
  import {
@@ -14,8 +14,15 @@ import {
14
14
  IMParticleUser,
15
15
  } from './identity-user-interfaces';
16
16
  import { IStore } from './store';
17
+ import type { IMParticleWebSDKInstance } from './mp-instance';
18
+ import {
19
+ IIdentitySearchRequestBody,
20
+ IdentitySearchCallback,
21
+ sendSearchRequest,
22
+ } from './identity/search';
17
23
 
18
24
  const { Identify, Modify, Login, Logout } = Constants.IdentityMethods;
25
+ const { HTTPCodes, Messages } = Constants;
19
26
  export const CACHE_HEADER = 'x-mp-max-age' as const;
20
27
 
21
28
  export type IdentityCache = BaseVault<Dictionary<ICachedIdentityCall>>;
@@ -321,3 +328,76 @@ export const hasExplicitIdentifier = (store: IStore | undefined | null): boolean
321
328
  }
322
329
  return !!store?.SDKConfig?.deviceId;
323
330
  };
331
+
332
+ /**
333
+ * Builds the /v1/identify-style envelope (client_sdk, environment,
334
+ * request_id, request_timestamp_ms) used to correlate IDSync requests
335
+ * across endpoints. `known_identities` is omitted so the caller can
336
+ * fold in the search-specific identifiers alongside the envelope.
337
+ */
338
+ export const buildIdentitySearchEnvelope = (
339
+ environment: Environment,
340
+ ): Omit<IIdentitySearchRequestBody, 'known_identities'> => ({
341
+ client_sdk: {
342
+ platform: Constants.platform,
343
+ sdk_vendor: Constants.sdkVendor,
344
+ sdk_version: Constants.sdkVersion,
345
+ },
346
+ environment,
347
+ request_id: generateUniqueId(),
348
+ request_timestamp_ms: Date.now(),
349
+ });
350
+
351
+ /**
352
+ * Wires the SDK instance into `sendSearchRequest`: gates on `canLog`,
353
+ * builds the `/v1/search` URL and request envelope, and dispatches.
354
+ * Lives here so the SDK glue (URL building, opt-out gate, dispatcher
355
+ * plumbing) is type-checked instead of being expressed in plain JS.
356
+ */
357
+ export const executeSearchRequest = (
358
+ mpInstance: IMParticleWebSDKInstance,
359
+ workspaceApiKey: string,
360
+ knownIdentities: UserIdentities,
361
+ callback: IdentitySearchCallback,
362
+ ): void => {
363
+ const { _Helpers, _Store, Logger, _ErrorReportingDispatcher } = mpInstance;
364
+ const { identityUrl, isDevelopmentMode } = _Store.SDKConfig;
365
+
366
+ if (!_Helpers.canLog()) {
367
+ Logger.verbose(Messages.InformationMessages.AbandonLogEvent);
368
+ if (isFunction(callback)) {
369
+ try {
370
+ callback({
371
+ httpCode: HTTPCodes.loggingDisabledOrMissingAPIKey,
372
+ });
373
+ } catch (e) {
374
+ Logger.error(
375
+ 'Error invoking search callback: ' +
376
+ ((e as Error)?.message || String(e)),
377
+ );
378
+ }
379
+ }
380
+ return;
381
+ }
382
+
383
+ // The Search endpoint is colocated with /v1/identify under
384
+ // identityUrl, so we reuse the same service URL builder. We do
385
+ // NOT append the apiKey to the URL — auth is done via x-mp-key.
386
+ const serviceUrl: string = _Helpers.createServiceUrl(identityUrl);
387
+ const searchUrl: string = serviceUrl + 'search?cb=1';
388
+
389
+ const environment: Environment = isDevelopmentMode
390
+ ? 'development'
391
+ : 'production';
392
+
393
+ sendSearchRequest(
394
+ knownIdentities,
395
+ workspaceApiKey,
396
+ () => buildIdentitySearchEnvelope(environment),
397
+ searchUrl,
398
+ callback,
399
+ Logger,
400
+ undefined,
401
+ _ErrorReportingDispatcher,
402
+ );
403
+ };
@@ -12,6 +12,7 @@ import {
12
12
  mParticleUserCart,
13
13
  IIdentityResponse,
14
14
  } from './identity-user-interfaces';
15
+ import { IdentitySearchCallback } from './identity/search';
15
16
  const { platform, sdkVendor, sdkVersion, HTTPCodes } = Constants;
16
17
 
17
18
  export type IdentityPreProcessResult = {
@@ -156,8 +157,53 @@ export interface SDKIdentityApi {
156
157
  destinationUser: IMParticleUser,
157
158
  scope?: AliasRequestScope
158
159
  ): IAliasRequest;
160
+ /**
161
+ * Sends a request to mParticle's IDSync `/v1/search` endpoint to look up
162
+ * a workspace identity without affecting the current user. The callback
163
+ * receives `httpCode` (always) and an optional `body` containing the
164
+ * parsed JSON response. Consumers should gate behaviour on
165
+ * `httpCode === 200`.
166
+ *
167
+ * `workspaceApiKey` is a workspace-specific API key supplied by the
168
+ * caller (from a kit's settings). It is sent as the `x-mp-key` header.
169
+ * The SDK's own workspace token is intentionally not used.
170
+ */
171
+ search?(
172
+ workspaceApiKey: string,
173
+ knownIdentities: UserIdentities,
174
+ callback: IdentitySearchCallback
175
+ ): void;
159
176
  }
160
177
 
178
+ export type {
179
+ IIdentitySearchResult,
180
+ IIdentitySearchResponseBody,
181
+ IdentitySearchCallback,
182
+ } from './identity/search';
183
+
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
+ }
161
207
  export interface IIdentity {
162
208
  audienceManager: AudienceManager;
163
209
  idCache: BaseVault<Dictionary<ICachedIdentityCall>>;
package/src/identity.js CHANGED
@@ -3,6 +3,7 @@ import Types, { IdentityType } from './types';
3
3
  import {
4
4
  cacheOrClearIdCache,
5
5
  createKnownIdentities,
6
+ executeSearchRequest,
6
7
  tryCacheIdentity,
7
8
  } from './identity-utils';
8
9
  import AudienceManager from './audienceManager';
@@ -730,6 +731,32 @@ export default function Identity(mpInstance) {
730
731
  }
731
732
  },
732
733
 
734
+ /**
735
+ * Search the IDSync Workspace endpoint for a known identity.
736
+ *
737
+ * POSTs to mParticle's `/v1/search` endpoint and invokes `callback`
738
+ * with `{ httpCode, body? }`.
739
+ *
740
+ * The `workspaceApiKey` is a workspace-specific API key supplied by
741
+ * the caller (passed in from a kit's settings). It is intentionally
742
+ * NOT read from the SDK's own workspace token, so that workspace
743
+ * searches can be authorised independently of the host SDK's
744
+ * workspace.
745
+ *
746
+ * @method search
747
+ * @param {String} workspaceApiKey Workspace API key (sent as x-mp-key).
748
+ * @param {Object} knownIdentities A `UserIdentities` map.
749
+ * @param {Function} callback Invoked with the `IIdentitySearchResult`.
750
+ */
751
+ search: function(workspaceApiKey, knownIdentities, callback) {
752
+ executeSearchRequest(
753
+ mpInstance,
754
+ workspaceApiKey,
755
+ knownIdentities,
756
+ callback
757
+ );
758
+ },
759
+
733
760
  /**
734
761
  Create a default AliasRequest for 2 MParticleUsers. This will construct the request
735
762
  using the sourceUser's firstSeenTime as the startTime, and its lastSeenTime as the endTime.
@@ -394,6 +394,13 @@ function mParticleInstanceManager(this: IMParticleInstanceManager) {
394
394
  modify: function(identityApiData, callback) {
395
395
  self.getInstance().Identity.modify(identityApiData, callback);
396
396
  },
397
+ search: function(workspaceApiKey, knownIdentities, callback) {
398
+ self.getInstance().Identity.search(
399
+ workspaceApiKey,
400
+ knownIdentities,
401
+ callback
402
+ );
403
+ },
397
404
  };
398
405
 
399
406
  this.sessionManager = {
@@ -56,6 +56,10 @@ export type {
56
56
  IAliasCallback,
57
57
  IAliasResult,
58
58
  SDKIdentityTypeEnum,
59
+ IIdentitySearchResult,
60
+ IIdentitySearchResponseBody,
61
+ IdentitySearchCallback,
62
+ IUserIdentities
59
63
  } from './identity.interfaces';
60
64
 
61
65
  // eCommerce
@@ -56,6 +56,7 @@ let mParticle = {
56
56
  login: voidFunction,
57
57
  logout: voidFunction,
58
58
  modify: voidFunction,
59
+ search: voidFunction,
59
60
  },
60
61
  };
61
62