@mparticle/web-sdk 2.66.0 → 2.68.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.
- package/dist/mparticle.common.js +154 -69
- package/dist/mparticle.esm.js +154 -69
- package/dist/mparticle.js +425 -131
- package/dist/types/src/identity/search.d.ts +64 -0
- package/dist/types/src/identity-utils.d.ts +21 -1
- package/dist/types/src/identity.interfaces.d.ts +37 -1
- package/dist/types/src/public-types.d.ts +1 -1
- package/dist/types/src/sdkRuntimeModels.d.ts +5 -5
- package/dist/types/src/stub/mparticle.stub.d.ts +1 -0
- package/package.json +2 -2
- package/src/identity/search.ts +233 -0
- package/src/identity-utils.ts +127 -5
- package/src/identity.interfaces.ts +46 -0
- package/src/identity.js +39 -1
- package/src/mparticle-instance-manager.ts +7 -0
- package/src/public-types.ts +4 -0
- package/src/sdkRuntimeModels.ts +5 -5
- package/src/stub/mparticle.stub.js +1 -0
- package/src/types.ts +4 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { SDKLoggerApi } from '../sdkRuntimeModels';
|
|
2
|
+
import { Environment } from '../utils';
|
|
3
|
+
import { AsyncUploader } from '../uploaders';
|
|
4
|
+
import { IErrorReportingService } from '../reporting/types';
|
|
5
|
+
import { UserIdentities } from '@mparticle/web-sdk';
|
|
6
|
+
/**
|
|
7
|
+
* Body payload returned by the `/v1/search` endpoint, as parsed JSON.
|
|
8
|
+
*
|
|
9
|
+
* The shape mirrors `/v1/identify` responses. All fields are optional because
|
|
10
|
+
* non-200 responses (e.g. 404 NOT_FOUND_ERROR) may include partial or
|
|
11
|
+
* error-shaped bodies, and the consumer should only rely on body fields when
|
|
12
|
+
* `httpCode === 200`.
|
|
13
|
+
*/
|
|
14
|
+
export interface IIdentitySearchResponseBody {
|
|
15
|
+
context?: string | null;
|
|
16
|
+
mpid?: string;
|
|
17
|
+
matched_identities?: Record<string, string>;
|
|
18
|
+
is_ephemeral?: boolean;
|
|
19
|
+
is_logged_in?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Result delivered to the consumer's callback. `httpCode` is always present;
|
|
23
|
+
* `body` is present whenever the response had a parseable JSON body.
|
|
24
|
+
*
|
|
25
|
+
* For non-network errors (missing API key, validation failures, JSON parse
|
|
26
|
+
* errors) `httpCode` will be `HTTPCodes.noHttpCoverage` (-1) and `body` will
|
|
27
|
+
* be omitted. The consumer is expected to gate behaviour on
|
|
28
|
+
* `httpCode === 200`.
|
|
29
|
+
*/
|
|
30
|
+
export interface IIdentitySearchResult {
|
|
31
|
+
httpCode: number;
|
|
32
|
+
body?: IIdentitySearchResponseBody;
|
|
33
|
+
}
|
|
34
|
+
export type IdentitySearchCallback = (result: IIdentitySearchResult) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Body posted to `/v1/search`. Mirrors the `/v1/identify` request envelope so
|
|
37
|
+
* that the IDSync service can correlate requests across endpoints.
|
|
38
|
+
*/
|
|
39
|
+
export interface IIdentitySearchRequestBody {
|
|
40
|
+
client_sdk: {
|
|
41
|
+
platform: string;
|
|
42
|
+
sdk_vendor: string;
|
|
43
|
+
sdk_version: string;
|
|
44
|
+
};
|
|
45
|
+
environment: Environment;
|
|
46
|
+
request_id: string;
|
|
47
|
+
request_timestamp_ms: number;
|
|
48
|
+
known_identities: UserIdentities;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Sends a POST to mParticle's IDSync Search endpoint and invokes `callback`
|
|
52
|
+
* with the HTTP status and parsed body.
|
|
53
|
+
*
|
|
54
|
+
* Defensive contract:
|
|
55
|
+
* - No identifier with a non-empty string value -> callback with
|
|
56
|
+
* `{ httpCode: noHttpCoverage }`, no network call.
|
|
57
|
+
* - Missing `apiKey` -> callback with `{ httpCode: noHttpCoverage }`,
|
|
58
|
+
* no network call.
|
|
59
|
+
* - Network/JSON-parse errors are caught and surfaced via the callback,
|
|
60
|
+
* never thrown. Network errors are also reported through the optional
|
|
61
|
+
* `errorReporter` so any registered IErrorReportingService can observe
|
|
62
|
+
* them (matches the pattern used by identifyRequest in identityApiClient).
|
|
63
|
+
*/
|
|
64
|
+
export declare const sendSearchRequest: (knownIdentities: UserIdentities, apiKey: string, requestBuilder: () => Omit<IIdentitySearchRequestBody, 'known_identities'>, searchUrl: string, callback: IdentitySearchCallback, logger: SDKLoggerApi, uploader?: AsyncUploader, errorReporter?: IErrorReportingService) => Promise<void>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Dictionary } from './utils';
|
|
1
|
+
import { Dictionary, Environment } from './utils';
|
|
2
2
|
import { BaseVault } from './vault';
|
|
3
3
|
import { IdentityApiData, UserIdentities, IdentityCallback } from '@mparticle/web-sdk';
|
|
4
4
|
import { IdentityAPIMethod } from './identity.interfaces';
|
|
5
5
|
import { IIdentityResponse, IMParticleUser } from './identity-user-interfaces';
|
|
6
6
|
import { IStore } from './store';
|
|
7
|
+
import type { IMParticleWebSDKInstance } from './mp-instance';
|
|
8
|
+
import { IIdentitySearchRequestBody, IdentitySearchCallback } from './identity/search';
|
|
7
9
|
export declare const CACHE_HEADER: "x-mp-max-age";
|
|
8
10
|
export type IdentityCache = BaseVault<Dictionary<ICachedIdentityCall>>;
|
|
9
11
|
export type IParseCachedIdentityResponse = (cachedIdentity: IIdentityResponse, mpid: string, callback: IdentityCallback, identityApiData: IdentityApiData, identityMethod: string, knownIdentities: IKnownIdentities, fromCachedIdentity: boolean) => void;
|
|
@@ -23,6 +25,9 @@ export declare const getCachedIdentity: (method: IdentityAPIMethod, proposedUser
|
|
|
23
25
|
export declare const createKnownIdentities: (identityApiData: IdentityApiData, deviceId: string) => IKnownIdentities;
|
|
24
26
|
export declare const removeExpiredIdentityCacheDates: (idCache: BaseVault<Dictionary<ICachedIdentityCall>>) => void;
|
|
25
27
|
export declare const tryCacheIdentity: (knownIdentities: IKnownIdentities, idCache: IdentityCache, parseIdentityResponse: IParseCachedIdentityResponse, mpid: string, callback: IdentityCallback, identityApiData: IdentityApiData, identityMethod: IdentityAPIMethod) => boolean;
|
|
28
|
+
type Sha256IdentityAlias = 'email_sha256' | 'mobile_sha256';
|
|
29
|
+
type UserIdentitiesWithAliases = UserIdentities & Partial<Record<Sha256IdentityAlias, string | null>>;
|
|
30
|
+
export declare const normalizeUserIdentityKeys: (userIdentities: UserIdentitiesWithAliases) => UserIdentities;
|
|
26
31
|
export declare const hasIdentityRequestChanged: (currentUser: IMParticleUser, newIdentityRequest: IdentityApiData) => boolean;
|
|
27
32
|
/**
|
|
28
33
|
* Checks if deviceId or other user identifiers (like email) were explicitly provided
|
|
@@ -34,3 +39,18 @@ export declare const hasIdentityRequestChanged: (currentUser: IMParticleUser, ne
|
|
|
34
39
|
* @returns true if deviceId or other identifiers were explicitly provided in config, false otherwise
|
|
35
40
|
*/
|
|
36
41
|
export declare const hasExplicitIdentifier: (store: IStore | undefined | null) => boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Builds the /v1/identify-style envelope (client_sdk, environment,
|
|
44
|
+
* request_id, request_timestamp_ms) used to correlate IDSync requests
|
|
45
|
+
* across endpoints. `known_identities` is omitted so the caller can
|
|
46
|
+
* fold in the search-specific identifiers alongside the envelope.
|
|
47
|
+
*/
|
|
48
|
+
export declare const buildIdentitySearchEnvelope: (environment: Environment) => Omit<IIdentitySearchRequestBody, 'known_identities'>;
|
|
49
|
+
/**
|
|
50
|
+
* Wires the SDK instance into `sendSearchRequest`: gates on `canLog`,
|
|
51
|
+
* builds the `/v1/search` URL and request envelope, and dispatches.
|
|
52
|
+
* Lives here so the SDK glue (URL building, opt-out gate, dispatcher
|
|
53
|
+
* plumbing) is type-checked instead of being expressed in plain JS.
|
|
54
|
+
*/
|
|
55
|
+
export declare const executeSearchRequest: (mpInstance: IMParticleWebSDKInstance, workspaceApiKey: string, knownIdentities: UserIdentities, callback: IdentitySearchCallback) => void;
|
|
56
|
+
export {};
|
|
@@ -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';
|
|
@@ -118,9 +118,9 @@ export interface SDKProductAction {
|
|
|
118
118
|
TaxAmount?: number;
|
|
119
119
|
}
|
|
120
120
|
export interface SDKProduct {
|
|
121
|
-
Sku
|
|
122
|
-
Name
|
|
123
|
-
Price
|
|
121
|
+
Sku: string;
|
|
122
|
+
Name: string;
|
|
123
|
+
Price: number;
|
|
124
124
|
Quantity?: number;
|
|
125
125
|
Brand?: string;
|
|
126
126
|
Variant?: string;
|
|
@@ -128,7 +128,7 @@ export interface SDKProduct {
|
|
|
128
128
|
Position?: number;
|
|
129
129
|
CouponCode?: string;
|
|
130
130
|
TotalAmount?: number;
|
|
131
|
-
Attributes?: Record<string, unknown
|
|
131
|
+
Attributes?: Record<string, unknown>;
|
|
132
132
|
}
|
|
133
133
|
export interface MParticleWebSDK {
|
|
134
134
|
addForwarder(forwarder: UnregisteredKit): void;
|
|
@@ -203,7 +203,7 @@ export declare const LogLevelType: {
|
|
|
203
203
|
readonly Warning: "warning";
|
|
204
204
|
readonly Error: "error";
|
|
205
205
|
};
|
|
206
|
-
export interface SDKInitConfig extends Omit<MPConfiguration, 'dataPlan' | 'logLevel'> {
|
|
206
|
+
export interface SDKInitConfig extends Omit<MPConfiguration, 'dataPlan' | 'logLevel' | 'identityCallback'> {
|
|
207
207
|
dataPlan?: DataPlanConfig | KitBlockerDataPlan;
|
|
208
208
|
logLevel?: LogLevelType;
|
|
209
209
|
kitConfigs?: IKitConfigs[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mparticle/web-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.68.0",
|
|
4
4
|
"description": "mParticle core SDK for web applications",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"@types/jest": "^29.5.1",
|
|
119
119
|
"@types/jest-expect-message": "^1.1.0",
|
|
120
120
|
"@types/mocha": "^9.0.0",
|
|
121
|
-
"@types/mparticle__web-sdk": "^2.
|
|
121
|
+
"@types/mparticle__web-sdk": "^2.66.0",
|
|
122
122
|
"@types/node": "^20.1.0",
|
|
123
123
|
"babel-preset-minify": "^0.5.1",
|
|
124
124
|
"browser-sync": "^2.26.3",
|
|
@@ -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
|
+
};
|
package/src/identity-utils.ts
CHANGED
|
@@ -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>>;
|
|
@@ -282,6 +289,48 @@ const getExpireTimestamp = (maxAge: number = ONE_DAY_IN_SECONDS): number =>
|
|
|
282
289
|
const parseIdentityResponse = (responseText: string): IdentityResultBody =>
|
|
283
290
|
responseText ? JSON.parse(responseText) : ({} as IdentityResultBody);
|
|
284
291
|
|
|
292
|
+
type Sha256IdentityAlias = 'email_sha256' | 'mobile_sha256';
|
|
293
|
+
|
|
294
|
+
const SHA256_IDENTITY_ALIASES: Readonly<
|
|
295
|
+
Record<Sha256IdentityAlias, keyof UserIdentities>
|
|
296
|
+
> = {
|
|
297
|
+
email_sha256: 'other',
|
|
298
|
+
mobile_sha256: 'other2',
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
type UserIdentitiesWithAliases = UserIdentities &
|
|
302
|
+
Partial<Record<Sha256IdentityAlias, string | null>>;
|
|
303
|
+
|
|
304
|
+
export const normalizeUserIdentityKeys = (
|
|
305
|
+
userIdentities: UserIdentitiesWithAliases
|
|
306
|
+
): UserIdentities => {
|
|
307
|
+
const normalized: UserIdentitiesWithAliases = { ...userIdentities };
|
|
308
|
+
(Object.keys(SHA256_IDENTITY_ALIASES) as Sha256IdentityAlias[]).forEach(
|
|
309
|
+
(alias) => {
|
|
310
|
+
if (alias in normalized) {
|
|
311
|
+
const value = normalized[alias];
|
|
312
|
+
delete normalized[alias];
|
|
313
|
+
normalized[SHA256_IDENTITY_ALIASES[alias]] = value;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
return normalized;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Compare two identity objects by key and value, independent of
|
|
321
|
+
// object key insertion order. The two sides come from different sources
|
|
322
|
+
// (numeric IdentityType iteration vs. partner-provided / alias-normalized
|
|
323
|
+
// order)
|
|
324
|
+
const identitiesEqual = (
|
|
325
|
+
a?: UserIdentities,
|
|
326
|
+
b?: UserIdentities
|
|
327
|
+
): boolean => {
|
|
328
|
+
const aKeys = Object.keys(a ?? {});
|
|
329
|
+
const bKeys = Object.keys(b ?? {});
|
|
330
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
331
|
+
return aKeys.every((key) => a[key] === b[key]);
|
|
332
|
+
};
|
|
333
|
+
|
|
285
334
|
export const hasIdentityRequestChanged = (
|
|
286
335
|
currentUser: IMParticleUser,
|
|
287
336
|
newIdentityRequest: IdentityApiData
|
|
@@ -293,11 +342,11 @@ export const hasIdentityRequestChanged = (
|
|
|
293
342
|
const currentUserIdentities =
|
|
294
343
|
currentUser.getUserIdentities().userIdentities;
|
|
295
344
|
|
|
296
|
-
const newIdentities =
|
|
297
|
-
|
|
298
|
-
return (
|
|
299
|
-
JSON.stringify(currentUserIdentities) !== JSON.stringify(newIdentities)
|
|
345
|
+
const newIdentities = normalizeUserIdentityKeys(
|
|
346
|
+
newIdentityRequest.userIdentities
|
|
300
347
|
);
|
|
348
|
+
|
|
349
|
+
return !identitiesEqual(currentUserIdentities, newIdentities);
|
|
301
350
|
};
|
|
302
351
|
|
|
303
352
|
/**
|
|
@@ -321,3 +370,76 @@ export const hasExplicitIdentifier = (store: IStore | undefined | null): boolean
|
|
|
321
370
|
}
|
|
322
371
|
return !!store?.SDKConfig?.deviceId;
|
|
323
372
|
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Builds the /v1/identify-style envelope (client_sdk, environment,
|
|
376
|
+
* request_id, request_timestamp_ms) used to correlate IDSync requests
|
|
377
|
+
* across endpoints. `known_identities` is omitted so the caller can
|
|
378
|
+
* fold in the search-specific identifiers alongside the envelope.
|
|
379
|
+
*/
|
|
380
|
+
export const buildIdentitySearchEnvelope = (
|
|
381
|
+
environment: Environment,
|
|
382
|
+
): Omit<IIdentitySearchRequestBody, 'known_identities'> => ({
|
|
383
|
+
client_sdk: {
|
|
384
|
+
platform: Constants.platform,
|
|
385
|
+
sdk_vendor: Constants.sdkVendor,
|
|
386
|
+
sdk_version: Constants.sdkVersion,
|
|
387
|
+
},
|
|
388
|
+
environment,
|
|
389
|
+
request_id: generateUniqueId(),
|
|
390
|
+
request_timestamp_ms: Date.now(),
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Wires the SDK instance into `sendSearchRequest`: gates on `canLog`,
|
|
395
|
+
* builds the `/v1/search` URL and request envelope, and dispatches.
|
|
396
|
+
* Lives here so the SDK glue (URL building, opt-out gate, dispatcher
|
|
397
|
+
* plumbing) is type-checked instead of being expressed in plain JS.
|
|
398
|
+
*/
|
|
399
|
+
export const executeSearchRequest = (
|
|
400
|
+
mpInstance: IMParticleWebSDKInstance,
|
|
401
|
+
workspaceApiKey: string,
|
|
402
|
+
knownIdentities: UserIdentities,
|
|
403
|
+
callback: IdentitySearchCallback,
|
|
404
|
+
): void => {
|
|
405
|
+
const { _Helpers, _Store, Logger, _ErrorReportingDispatcher } = mpInstance;
|
|
406
|
+
const { identityUrl, isDevelopmentMode } = _Store.SDKConfig;
|
|
407
|
+
|
|
408
|
+
if (!_Helpers.canLog()) {
|
|
409
|
+
Logger.verbose(Messages.InformationMessages.AbandonLogEvent);
|
|
410
|
+
if (isFunction(callback)) {
|
|
411
|
+
try {
|
|
412
|
+
callback({
|
|
413
|
+
httpCode: HTTPCodes.loggingDisabledOrMissingAPIKey,
|
|
414
|
+
});
|
|
415
|
+
} catch (e) {
|
|
416
|
+
Logger.error(
|
|
417
|
+
'Error invoking search callback: ' +
|
|
418
|
+
((e as Error)?.message || String(e)),
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// The Search endpoint is colocated with /v1/identify under
|
|
426
|
+
// identityUrl, so we reuse the same service URL builder. We do
|
|
427
|
+
// NOT append the apiKey to the URL — auth is done via x-mp-key.
|
|
428
|
+
const serviceUrl: string = _Helpers.createServiceUrl(identityUrl);
|
|
429
|
+
const searchUrl: string = serviceUrl + 'search?cb=1';
|
|
430
|
+
|
|
431
|
+
const environment: Environment = isDevelopmentMode
|
|
432
|
+
? 'development'
|
|
433
|
+
: 'production';
|
|
434
|
+
|
|
435
|
+
sendSearchRequest(
|
|
436
|
+
knownIdentities,
|
|
437
|
+
workspaceApiKey,
|
|
438
|
+
() => buildIdentitySearchEnvelope(environment),
|
|
439
|
+
searchUrl,
|
|
440
|
+
callback,
|
|
441
|
+
Logger,
|
|
442
|
+
undefined,
|
|
443
|
+
_ErrorReportingDispatcher,
|
|
444
|
+
);
|
|
445
|
+
};
|