@mparticle/web-sdk 2.28.2 → 2.29.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 +69 -45
- package/dist/mparticle.esm.js +69 -45
- package/dist/mparticle.js +407 -116
- package/package.json +1 -1
- package/src/aliasRequestApiClient.ts +89 -0
- package/src/constants.ts +8 -1
- package/src/identity.interfaces.ts +10 -1
- package/src/identity.js +2 -3
- package/src/identityApiClient.js +3 -53
- package/src/integrationCapture.ts +153 -0
- package/src/mp-instance.js +7 -1
- package/src/sdkRuntimeModels.ts +12 -1
- package/src/serverModel.ts +14 -1
- package/src/store.ts +2 -0
- package/src/uploaders.ts +10 -5
- package/src/utils.ts +105 -3
package/package.json
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { IAliasRequest, IAliasCallback } from "./identity.interfaces";
|
|
2
|
+
import { MParticleWebSDK } from "./sdkRuntimeModels";
|
|
3
|
+
import Constants from './constants';
|
|
4
|
+
import { FetchUploader, XHRUploader } from './uploaders';
|
|
5
|
+
import { HTTP_ACCEPTED, HTTP_OK } from "./constants";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const { HTTPCodes, Messages } = Constants;
|
|
9
|
+
|
|
10
|
+
interface IAliasResponseBody {
|
|
11
|
+
message?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function sendAliasRequest (mpInstance: MParticleWebSDK, aliasRequest: IAliasRequest, aliasCallback: IAliasCallback) {
|
|
15
|
+
const { verbose, error } = mpInstance.Logger;
|
|
16
|
+
const { invokeAliasCallback } = mpInstance._Helpers;
|
|
17
|
+
const { aliasUrl } = mpInstance._Store.SDKConfig;
|
|
18
|
+
const { devToken: apiKey } = mpInstance._Store;
|
|
19
|
+
|
|
20
|
+
verbose(Messages.InformationMessages.SendAliasHttp);
|
|
21
|
+
|
|
22
|
+
// https://go.mparticle.com/work/SQDSDKS-6750
|
|
23
|
+
const uploadUrl = `https://${aliasUrl}${apiKey}/Alias`;
|
|
24
|
+
const uploader = window.fetch
|
|
25
|
+
? new FetchUploader(uploadUrl)
|
|
26
|
+
: new XHRUploader(uploadUrl);
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
// https://go.mparticle.com/work/SQDSDKS-6568
|
|
30
|
+
const uploadPayload = {
|
|
31
|
+
method: 'post',
|
|
32
|
+
headers: {
|
|
33
|
+
Accept: 'text/plain;charset=UTF-8',
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify(aliasRequest),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const response = await uploader.upload(uploadPayload);
|
|
41
|
+
|
|
42
|
+
let message: string;
|
|
43
|
+
let aliasResponseBody: IAliasResponseBody;
|
|
44
|
+
|
|
45
|
+
// FetchUploader returns the response as a JSON object that we have to await
|
|
46
|
+
if (response.json) {
|
|
47
|
+
// HTTP responses of 202, 200, and 403 do not have a response. response.json will always exist on a fetch, but can only be await-ed when the response is not empty, otherwise it will throw an error.
|
|
48
|
+
try {
|
|
49
|
+
aliasResponseBody = await response.json();
|
|
50
|
+
} catch (e) {
|
|
51
|
+
verbose('The request has no response body');
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
// https://go.mparticle.com/work/SQDSDKS-6568
|
|
55
|
+
// XHRUploader returns the response as a string that we need to parse
|
|
56
|
+
const xhrResponse = response as unknown as XMLHttpRequest;
|
|
57
|
+
// debugger;
|
|
58
|
+
aliasResponseBody = xhrResponse.responseText
|
|
59
|
+
? JSON.parse(xhrResponse.responseText)
|
|
60
|
+
: '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let errorMessage: string;
|
|
64
|
+
|
|
65
|
+
switch (response.status) {
|
|
66
|
+
case HTTP_OK:
|
|
67
|
+
case HTTP_ACCEPTED:
|
|
68
|
+
// https://go.mparticle.com/work/SQDSDKS-6670
|
|
69
|
+
message =
|
|
70
|
+
'Successfully sent forwarding stats to mParticle Servers';
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
// 400 has an error message, but 403 doesn't
|
|
74
|
+
if (aliasResponseBody?.message) {
|
|
75
|
+
errorMessage = aliasResponseBody.message;
|
|
76
|
+
}
|
|
77
|
+
message =
|
|
78
|
+
'Issue with sending Alias Request to mParticle Servers, received HTTP Code of ' +
|
|
79
|
+
response.status;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
verbose(message);
|
|
83
|
+
invokeAliasCallback(aliasCallback, response.status, errorMessage);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
const err = e as Error;
|
|
86
|
+
error('Error sending alias request to mParticle servers. ' + err);
|
|
87
|
+
invokeAliasCallback(aliasCallback, HTTPCodes.noHttpCoverage, (err.message));
|
|
88
|
+
}
|
|
89
|
+
};
|
package/src/constants.ts
CHANGED
|
@@ -174,6 +174,7 @@ const Constants = {
|
|
|
174
174
|
DirectUrlRouting: 'directURLRouting',
|
|
175
175
|
CacheIdentity: 'cacheIdentity',
|
|
176
176
|
AudienceAPI: 'audienceAPI',
|
|
177
|
+
CaptureIntegrationSpecificIds: 'captureIntegrationSpecificIds',
|
|
177
178
|
},
|
|
178
179
|
DefaultInstance: 'default_instance',
|
|
179
180
|
CCPAPurpose: 'data_sale_opt_out',
|
|
@@ -189,4 +190,10 @@ export default Constants;
|
|
|
189
190
|
|
|
190
191
|
// https://go.mparticle.com/work/SQDSDKS-6080
|
|
191
192
|
export const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
|
|
192
|
-
export const MILLIS_IN_ONE_SEC = 1000;
|
|
193
|
+
export const MILLIS_IN_ONE_SEC = 1000;
|
|
194
|
+
|
|
195
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
|
|
196
|
+
export const HTTP_OK = 200 as const;
|
|
197
|
+
export const HTTP_ACCEPTED = 202 as const;
|
|
198
|
+
export const HTTP_BAD_REQUEST = 400 as const;
|
|
199
|
+
export const HTTP_FORBIDDEN = 403 as const;
|
|
@@ -55,7 +55,7 @@ export interface IIdentityAPIRequestData {
|
|
|
55
55
|
context: string | null;
|
|
56
56
|
environment: Environment;
|
|
57
57
|
request_id: string;
|
|
58
|
-
|
|
58
|
+
request_timestamp_unixtime_ms: number;
|
|
59
59
|
previous_mpid: MPID | null;
|
|
60
60
|
known_identities: IKnownIdentities;
|
|
61
61
|
}
|
|
@@ -115,6 +115,15 @@ export interface IAliasRequest {
|
|
|
115
115
|
scope?: string;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
export interface IAliasCallback {
|
|
119
|
+
(result: IAliasResult): void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface IAliasResult {
|
|
123
|
+
httpCode: number;
|
|
124
|
+
message: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
118
127
|
export interface SDKIdentityApi {
|
|
119
128
|
HTTPCodes: typeof HTTPCodes;
|
|
120
129
|
identify?(
|
package/src/identity.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Constants from './constants';
|
|
1
|
+
import Constants, { HTTP_OK } from './constants';
|
|
2
2
|
import Types from './types';
|
|
3
3
|
import {
|
|
4
4
|
cacheOrClearIdCache,
|
|
@@ -13,7 +13,6 @@ const { CacheIdentity } = FeatureFlags;
|
|
|
13
13
|
const { Identify, Modify, Login, Logout } = IdentityMethods;
|
|
14
14
|
import {
|
|
15
15
|
generateDeprecationMessage,
|
|
16
|
-
HTTP_SUCCESS,
|
|
17
16
|
isEmpty,
|
|
18
17
|
isFunction,
|
|
19
18
|
isObject,
|
|
@@ -1513,7 +1512,7 @@ export default function Identity(mpInstance) {
|
|
|
1513
1512
|
);
|
|
1514
1513
|
}
|
|
1515
1514
|
|
|
1516
|
-
if (identityResponse.status ===
|
|
1515
|
+
if (identityResponse.status === HTTP_OK) {
|
|
1517
1516
|
if (getFeatureFlag(CacheIdentity)) {
|
|
1518
1517
|
const identityResponseForCache = xhrIdentityResponseAdapter(
|
|
1519
1518
|
identityResponse
|
package/src/identityApiClient.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Constants from './constants';
|
|
2
2
|
import { xhrIdentityResponseAdapter } from './identity-utils';
|
|
3
|
+
import { sendAliasRequest } from './aliasRequestApiClient';
|
|
3
4
|
|
|
4
5
|
var HTTPCodes = Constants.HTTPCodes,
|
|
5
6
|
Messages = Constants.Messages;
|
|
@@ -7,59 +8,8 @@ var HTTPCodes = Constants.HTTPCodes,
|
|
|
7
8
|
const { Modify } = Constants.IdentityMethods;
|
|
8
9
|
|
|
9
10
|
export default function IdentityAPIClient(mpInstance) {
|
|
10
|
-
this.sendAliasRequest = function(aliasRequest, callback) {
|
|
11
|
-
|
|
12
|
-
xhrCallback = function() {
|
|
13
|
-
if (xhr.readyState === 4) {
|
|
14
|
-
// https://go.mparticle.com/work/SQDSDKS-6368
|
|
15
|
-
mpInstance.Logger.verbose(
|
|
16
|
-
'Received ' + xhr.statusText + ' from server'
|
|
17
|
-
);
|
|
18
|
-
//only parse error messages from failing requests
|
|
19
|
-
if (xhr.status !== 200 && xhr.status !== 202) {
|
|
20
|
-
if (xhr.responseText) {
|
|
21
|
-
var response = JSON.parse(xhr.responseText);
|
|
22
|
-
if (response.hasOwnProperty('message')) {
|
|
23
|
-
var errorMessage = response.message;
|
|
24
|
-
mpInstance._Helpers.invokeAliasCallback(
|
|
25
|
-
callback,
|
|
26
|
-
xhr.status,
|
|
27
|
-
errorMessage
|
|
28
|
-
);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
mpInstance._Helpers.invokeAliasCallback(
|
|
34
|
-
callback,
|
|
35
|
-
xhr.status
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
mpInstance.Logger.verbose(Messages.InformationMessages.SendAliasHttp);
|
|
40
|
-
|
|
41
|
-
xhr = mpInstance._Helpers.createXHR(xhrCallback);
|
|
42
|
-
if (xhr) {
|
|
43
|
-
try {
|
|
44
|
-
xhr.open(
|
|
45
|
-
'post',
|
|
46
|
-
mpInstance._Helpers.createServiceUrl(
|
|
47
|
-
mpInstance._Store.SDKConfig.aliasUrl,
|
|
48
|
-
mpInstance._Store.devToken
|
|
49
|
-
) + '/Alias'
|
|
50
|
-
);
|
|
51
|
-
xhr.send(JSON.stringify(aliasRequest));
|
|
52
|
-
} catch (e) {
|
|
53
|
-
mpInstance._Helpers.invokeAliasCallback(
|
|
54
|
-
callback,
|
|
55
|
-
HTTPCodes.noHttpCoverage,
|
|
56
|
-
e
|
|
57
|
-
);
|
|
58
|
-
mpInstance.Logger.error(
|
|
59
|
-
'Error sending alias request to mParticle servers. ' + e
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
11
|
+
this.sendAliasRequest = async function(aliasRequest, callback) {
|
|
12
|
+
await sendAliasRequest(mpInstance, aliasRequest, callback);
|
|
63
13
|
};
|
|
64
14
|
|
|
65
15
|
this.sendIdentityRequest = function(
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { SDKEventCustomFlags } from './sdkRuntimeModels';
|
|
2
|
+
import {
|
|
3
|
+
Dictionary,
|
|
4
|
+
queryStringParser,
|
|
5
|
+
getCookies,
|
|
6
|
+
getHref,
|
|
7
|
+
isEmpty,
|
|
8
|
+
} from './utils';
|
|
9
|
+
|
|
10
|
+
export interface IntegrationCaptureProcessorFunction {
|
|
11
|
+
(clickId: string, url: string, timestamp?: number): string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Facebook Click ID has specific formatting rules
|
|
15
|
+
// The formatted ClickID value must be of the form version.subdomainIndex.creationTime.<fbclid>, where:
|
|
16
|
+
// - version is always this prefix: fb
|
|
17
|
+
// - subdomainIndex is which domain the cookie is defined on ('com' = 0, 'example.com' = 1, 'www.example.com' = 2)
|
|
18
|
+
// - creationTime is the UNIX time since epoch in milliseconds when the _fbc was stored. If you don't save the _fbc cookie, use the timestamp when you first observed or received this fbclid value
|
|
19
|
+
// - <fbclid> is the value for the fbclid query parameter in the page URL.
|
|
20
|
+
// https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc
|
|
21
|
+
export const facebookClickIdProcessor: IntegrationCaptureProcessorFunction = (
|
|
22
|
+
clickId: string,
|
|
23
|
+
url: string,
|
|
24
|
+
timestamp?: number,
|
|
25
|
+
): string => {
|
|
26
|
+
if (!clickId || !url) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const urlSegments = url?.split('//')
|
|
31
|
+
if (!urlSegments) {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const urlParts = urlSegments[1].split('/');
|
|
36
|
+
const domainParts = urlParts[0].split('.');
|
|
37
|
+
let subdomainIndex: number = 1;
|
|
38
|
+
|
|
39
|
+
// The rules for subdomainIndex are for parsing the domain portion
|
|
40
|
+
// of the URL for cookies, but in this case we are parsing the URL
|
|
41
|
+
// itself, so we can ignore the use of 0 for 'com'
|
|
42
|
+
if (domainParts.length >= 3) {
|
|
43
|
+
subdomainIndex = 2;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// If timestamp is not provided, use the current time
|
|
47
|
+
const _timestamp = timestamp || Date.now();
|
|
48
|
+
|
|
49
|
+
return `fb.${subdomainIndex}.${_timestamp}.${clickId}`;
|
|
50
|
+
};
|
|
51
|
+
interface IntegrationIdMapping {
|
|
52
|
+
[key: string]: {
|
|
53
|
+
mappedKey: string;
|
|
54
|
+
processor?: IntegrationCaptureProcessorFunction;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const integrationMapping: IntegrationIdMapping = {
|
|
59
|
+
fbclid: {
|
|
60
|
+
mappedKey: 'Facebook.ClickId',
|
|
61
|
+
processor: facebookClickIdProcessor,
|
|
62
|
+
},
|
|
63
|
+
_fbp: {
|
|
64
|
+
mappedKey: 'Facebook.BrowserId',
|
|
65
|
+
},
|
|
66
|
+
_fbc: {
|
|
67
|
+
mappedKey: 'Facebook.ClickId',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default class IntegrationCapture {
|
|
72
|
+
public clickIds: Dictionary<string>;
|
|
73
|
+
public readonly initialTimestamp: number;
|
|
74
|
+
|
|
75
|
+
constructor() {
|
|
76
|
+
this.initialTimestamp = Date.now();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Captures Integration Ids from cookies and query params and stores them in clickIds object
|
|
81
|
+
*/
|
|
82
|
+
public capture(): void {
|
|
83
|
+
const queryParams = this.captureQueryParams() || {};
|
|
84
|
+
const cookies = this.captureCookies() || {};
|
|
85
|
+
|
|
86
|
+
// Exclude _fbc if fbclid is present
|
|
87
|
+
// https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc#retrieve-from-fbclid-url-query-parameter
|
|
88
|
+
if (queryParams['fbclid'] && cookies['_fbc']) {
|
|
89
|
+
delete cookies['_fbc'];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.clickIds = { ...this.clickIds, ...queryParams, ...cookies };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Captures cookies based on the integration ID mapping.
|
|
97
|
+
*/
|
|
98
|
+
public captureCookies(): Dictionary<string> {
|
|
99
|
+
const cookies = getCookies(Object.keys(integrationMapping));
|
|
100
|
+
return this.applyProcessors(cookies);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Captures query parameters based on the integration ID mapping.
|
|
105
|
+
*/
|
|
106
|
+
public captureQueryParams(): Dictionary<string> {
|
|
107
|
+
const queryParams = this.getQueryParams();
|
|
108
|
+
return this.applyProcessors(queryParams, getHref(), this.initialTimestamp);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Gets the query parameters based on the integration ID mapping.
|
|
113
|
+
* @returns {Dictionary<string>} The query parameters.
|
|
114
|
+
*/
|
|
115
|
+
public getQueryParams(): Dictionary<string> {
|
|
116
|
+
return queryStringParser(getHref(), Object.keys(integrationMapping));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Converts captured click IDs to custom flags for SDK events.
|
|
121
|
+
* @returns {SDKEventCustomFlags} The custom flags.
|
|
122
|
+
*/
|
|
123
|
+
public getClickIdsAsCustomFlags(): SDKEventCustomFlags {
|
|
124
|
+
const customFlags: SDKEventCustomFlags = {};
|
|
125
|
+
|
|
126
|
+
if (!this.clickIds) {
|
|
127
|
+
return customFlags;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const [key, value] of Object.entries(this.clickIds)) {
|
|
131
|
+
const mappedKey = integrationMapping[key]?.mappedKey;
|
|
132
|
+
if (!isEmpty(mappedKey)) {
|
|
133
|
+
customFlags[mappedKey] = value;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return customFlags;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private applyProcessors(clickIds: Dictionary<string>, url?: string, timestamp?: number): Dictionary<string> {
|
|
140
|
+
const processedClickIds: Dictionary<string> = {};
|
|
141
|
+
|
|
142
|
+
for (const [key, value] of Object.entries(clickIds)) {
|
|
143
|
+
const processor = integrationMapping[key]?.processor;
|
|
144
|
+
if (processor) {
|
|
145
|
+
processedClickIds[key] = processor(value, url, timestamp);
|
|
146
|
+
} else {
|
|
147
|
+
processedClickIds[key] = value;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return processedClickIds;
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/mp-instance.js
CHANGED
|
@@ -39,9 +39,10 @@ import IdentityAPIClient from './identityApiClient';
|
|
|
39
39
|
import { isEmpty, isFunction } from './utils';
|
|
40
40
|
import { LocalStorageVault } from './vault';
|
|
41
41
|
import { removeExpiredIdentityCacheDates } from './identity-utils';
|
|
42
|
+
import IntegrationCapture from './integrationCapture';
|
|
42
43
|
|
|
43
44
|
const { Messages, HTTPCodes, FeatureFlags } = Constants;
|
|
44
|
-
const { ReportBatching } = FeatureFlags;
|
|
45
|
+
const { ReportBatching, CaptureIntegrationSpecificIds } = FeatureFlags;
|
|
45
46
|
const { StartingInitialization } = Messages.InformationMessages;
|
|
46
47
|
|
|
47
48
|
/**
|
|
@@ -77,6 +78,7 @@ export default function mParticleInstance(instanceName) {
|
|
|
77
78
|
integrationDelays: {},
|
|
78
79
|
forwarderConstructors: [],
|
|
79
80
|
};
|
|
81
|
+
this._IntegrationCapture = new IntegrationCapture();
|
|
80
82
|
|
|
81
83
|
// required for forwarders once they reference the mparticle instance
|
|
82
84
|
this.IdentityType = Types.IdentityType;
|
|
@@ -1336,6 +1338,10 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
|
|
|
1336
1338
|
mpInstance._ForwardingStatsUploader.startForwardingStatsTimer();
|
|
1337
1339
|
}
|
|
1338
1340
|
|
|
1341
|
+
if (mpInstance._Helpers.getFeatureFlag(CaptureIntegrationSpecificIds)) {
|
|
1342
|
+
mpInstance._IntegrationCapture.capture();
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1339
1345
|
mpInstance._Forwarders.processForwarders(
|
|
1340
1346
|
config,
|
|
1341
1347
|
mpInstance._APIClient.prepareForwardingStats
|
package/src/sdkRuntimeModels.ts
CHANGED
|
@@ -16,7 +16,11 @@ import { IPersistence } from './persistence.interfaces';
|
|
|
16
16
|
import { IMPSideloadedKit } from './sideloadedKit';
|
|
17
17
|
import { ISessionManager } from './sessionManager';
|
|
18
18
|
import { Kit, MPForwarder } from './forwarders.interfaces';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
IIdentity,
|
|
21
|
+
SDKIdentityApi,
|
|
22
|
+
IAliasCallback,
|
|
23
|
+
} from './identity.interfaces';
|
|
20
24
|
import {
|
|
21
25
|
ISDKUserAttributeChangeData,
|
|
22
26
|
ISDKUserIdentityChanges,
|
|
@@ -26,6 +30,7 @@ import {
|
|
|
26
30
|
ISDKUserAttributes,
|
|
27
31
|
} from './identity-user-interfaces';
|
|
28
32
|
import { IIdentityType } from './types.interfaces';
|
|
33
|
+
import IntegrationCapture from './integrationCapture';
|
|
29
34
|
|
|
30
35
|
// TODO: Resolve this with version in @mparticle/web-sdk
|
|
31
36
|
export type SDKEventCustomFlags = Dictionary<any>;
|
|
@@ -146,6 +151,7 @@ interface IEvents {
|
|
|
146
151
|
|
|
147
152
|
export interface MParticleWebSDK {
|
|
148
153
|
addForwarder(mockForwarder: MPForwarder): void;
|
|
154
|
+
_IntegrationCapture: IntegrationCapture;
|
|
149
155
|
IdentityType: IIdentityType;
|
|
150
156
|
_Identity: IIdentity;
|
|
151
157
|
Identity: SDKIdentityApi;
|
|
@@ -265,6 +271,11 @@ export interface SDKHelpersApi {
|
|
|
265
271
|
generateHash?(value: string): string;
|
|
266
272
|
// https://go.mparticle.com/work/SQDSDKS-6317
|
|
267
273
|
getFeatureFlag?(feature: string): boolean | string; // TODO: Feature Constants should be converted to enum
|
|
274
|
+
invokeAliasCallback(
|
|
275
|
+
aliasCallback: IAliasCallback,
|
|
276
|
+
number: number,
|
|
277
|
+
errorMessage: string
|
|
278
|
+
): void;
|
|
268
279
|
isDelayedByIntegration?(
|
|
269
280
|
delayedIntegrations: Dictionary<boolean>,
|
|
270
281
|
timeoutStart: number,
|
package/src/serverModel.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
BaseEvent,
|
|
9
9
|
MParticleWebSDK,
|
|
10
10
|
SDKEvent,
|
|
11
|
+
SDKEventCustomFlags,
|
|
11
12
|
SDKGeoLocation,
|
|
12
13
|
SDKProduct,
|
|
13
14
|
} from './sdkRuntimeModels';
|
|
@@ -317,6 +318,18 @@ export default function ServerModel(
|
|
|
317
318
|
event.messageType === Types.MessageType.OptOut ||
|
|
318
319
|
mpInstance._Store.webviewBridgeEnabled
|
|
319
320
|
) {
|
|
321
|
+
let customFlags: SDKEventCustomFlags = {...event.customFlags};
|
|
322
|
+
|
|
323
|
+
// https://go.mparticle.com/work/SQDSDKS-5053
|
|
324
|
+
if (mpInstance._Helpers.getFeatureFlag && mpInstance._Helpers.getFeatureFlag(Constants.FeatureFlags.CaptureIntegrationSpecificIds)) {
|
|
325
|
+
|
|
326
|
+
// Attempt to recapture click IDs in case a third party integration
|
|
327
|
+
// has added or updated new click IDs since the last event was sent.
|
|
328
|
+
mpInstance._IntegrationCapture.capture();
|
|
329
|
+
const transformedClickIDs = mpInstance._IntegrationCapture.getClickIdsAsCustomFlags();
|
|
330
|
+
customFlags = {...transformedClickIDs, ...customFlags};
|
|
331
|
+
}
|
|
332
|
+
|
|
320
333
|
if (event.hasOwnProperty('toEventAPIObject')) {
|
|
321
334
|
eventObject = event.toEventAPIObject();
|
|
322
335
|
} else {
|
|
@@ -336,7 +349,7 @@ export default function ServerModel(
|
|
|
336
349
|
event.sourceMessageId ||
|
|
337
350
|
mpInstance._Helpers.generateUniqueId(),
|
|
338
351
|
EventDataType: event.messageType,
|
|
339
|
-
CustomFlags:
|
|
352
|
+
CustomFlags: customFlags,
|
|
340
353
|
UserAttributeChanges: event.userAttributeChanges,
|
|
341
354
|
UserIdentityChanges: event.userIdentityChanges,
|
|
342
355
|
};
|
package/src/store.ts
CHANGED
|
@@ -710,6 +710,7 @@ export function processFlags(config: SDKInitConfig): IFeatureFlags {
|
|
|
710
710
|
DirectUrlRouting,
|
|
711
711
|
CacheIdentity,
|
|
712
712
|
AudienceAPI,
|
|
713
|
+
CaptureIntegrationSpecificIds,
|
|
713
714
|
} = Constants.FeatureFlags;
|
|
714
715
|
|
|
715
716
|
if (!config.flags) {
|
|
@@ -727,6 +728,7 @@ export function processFlags(config: SDKInitConfig): IFeatureFlags {
|
|
|
727
728
|
flags[DirectUrlRouting] = config.flags[DirectUrlRouting] === 'True';
|
|
728
729
|
flags[CacheIdentity] = config.flags[CacheIdentity] === 'True';
|
|
729
730
|
flags[AudienceAPI] = config.flags[AudienceAPI] === 'True';
|
|
731
|
+
flags[CaptureIntegrationSpecificIds] = config.flags[CaptureIntegrationSpecificIds] === 'True';
|
|
730
732
|
|
|
731
733
|
return flags;
|
|
732
734
|
}
|
package/src/uploaders.ts
CHANGED
|
@@ -47,6 +47,7 @@ export class XHRUploader extends AsyncUploader {
|
|
|
47
47
|
// 3 LOADING Downloading; responseText holds partial data.
|
|
48
48
|
// 4 DONE The operation is complete.
|
|
49
49
|
|
|
50
|
+
// https://go.mparticle.com/work/SQDSDKS-6736
|
|
50
51
|
private async makeRequest(
|
|
51
52
|
url: string,
|
|
52
53
|
data: string,
|
|
@@ -58,13 +59,17 @@ export class XHRUploader extends AsyncUploader {
|
|
|
58
59
|
if (xhr.readyState !== 4) return;
|
|
59
60
|
|
|
60
61
|
// Process the response
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
reject(xhr);
|
|
65
|
-
}
|
|
62
|
+
// We resolve all xhr responses whose ready state is 4 regardless of HTTP codes that may be errors (400+)
|
|
63
|
+
// because these are valid HTTP responses.
|
|
64
|
+
resolve((xhr as unknown) as Response);
|
|
66
65
|
};
|
|
67
66
|
|
|
67
|
+
// Reject a promise only when there is an xhr error
|
|
68
|
+
xhr.onerror = () => {
|
|
69
|
+
reject((xhr as unknown) as Response);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
|
|
68
73
|
xhr.open(method, url);
|
|
69
74
|
xhr.send(data);
|
|
70
75
|
});
|
package/src/utils.ts
CHANGED
|
@@ -9,8 +9,6 @@ export type Dictionary<V = any> = Record<string, V>;
|
|
|
9
9
|
|
|
10
10
|
export type Environment = 'development' | 'production';
|
|
11
11
|
|
|
12
|
-
export const HTTP_SUCCESS = 200 as const;
|
|
13
|
-
|
|
14
12
|
const createCookieString = (value: string): string =>
|
|
15
13
|
replaceCommasWithPipes(replaceQuotesWithApostrophes(value));
|
|
16
14
|
|
|
@@ -224,7 +222,7 @@ const isDataPlanSlug = (str: string): boolean => str === toDataPlanSlug(str);
|
|
|
224
222
|
const isStringOrNumber = (value: any): boolean =>
|
|
225
223
|
isString(value) || isNumber(value);
|
|
226
224
|
|
|
227
|
-
const isEmpty = (value: Dictionary<any> | null | undefined): boolean =>
|
|
225
|
+
const isEmpty = (value: Dictionary<any> | string | null | undefined): boolean =>
|
|
228
226
|
value == null || !(Object.keys(value) || value).length;
|
|
229
227
|
|
|
230
228
|
const mergeObjects = <T extends object>(...objects: T[]): T => {
|
|
@@ -234,6 +232,107 @@ const mergeObjects = <T extends object>(...objects: T[]): T => {
|
|
|
234
232
|
const moveElementToEnd = <T>(array: T[], index: number): T[] =>
|
|
235
233
|
array.slice(0, index).concat(array.slice(index + 1), array[index]);
|
|
236
234
|
|
|
235
|
+
const queryStringParser = (
|
|
236
|
+
url: string,
|
|
237
|
+
keys: string[] = []
|
|
238
|
+
): Dictionary<string> => {
|
|
239
|
+
let urlParams: URLSearchParams | URLSearchParamsFallback;
|
|
240
|
+
let results: Dictionary<string> = {};
|
|
241
|
+
|
|
242
|
+
if (!url) return results;
|
|
243
|
+
|
|
244
|
+
if (typeof URL !== 'undefined' && typeof URLSearchParams !== 'undefined') {
|
|
245
|
+
const urlObject = new URL(url);
|
|
246
|
+
urlParams = new URLSearchParams(urlObject.search);
|
|
247
|
+
} else {
|
|
248
|
+
urlParams = queryStringParserFallback(url);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (isEmpty(keys)) {
|
|
252
|
+
urlParams.forEach((value, key) => {
|
|
253
|
+
results[key] = value;
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
keys.forEach(key => {
|
|
257
|
+
const value = urlParams.get(key);
|
|
258
|
+
if (value) {
|
|
259
|
+
results[key] = value;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return results;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
interface URLSearchParamsFallback {
|
|
268
|
+
get: (key: string) => string | null;
|
|
269
|
+
forEach: (callback: (value: string, key: string) => void) => void;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const queryStringParserFallback = (url: string): URLSearchParamsFallback => {
|
|
273
|
+
let params: Dictionary<string> = {};
|
|
274
|
+
const queryString = url.split('?')[1] || '';
|
|
275
|
+
const pairs = queryString.split('&');
|
|
276
|
+
|
|
277
|
+
pairs.forEach(pair => {
|
|
278
|
+
var [key, value] = pair.split('=');
|
|
279
|
+
if (key && value) {
|
|
280
|
+
params[key] = decodeURIComponent(value || '');
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
get: function(key: string) {
|
|
286
|
+
return params[key];
|
|
287
|
+
},
|
|
288
|
+
forEach: function(callback: (value: string, key: string) => void) {
|
|
289
|
+
for (var key in params) {
|
|
290
|
+
if (params.hasOwnProperty(key)) {
|
|
291
|
+
callback(params[key], key);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Get cookies as a dictionary
|
|
299
|
+
const getCookies = (keys?: string[]): Dictionary<string> => {
|
|
300
|
+
// Helper function to parse cookies from document.cookie
|
|
301
|
+
const parseCookies = (): string[] => {
|
|
302
|
+
if (typeof window === 'undefined') {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
return window.document.cookie.split(';').map(cookie => cookie.trim());
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Helper function to filter cookies by keys
|
|
309
|
+
const filterCookies = (
|
|
310
|
+
cookies: string[],
|
|
311
|
+
keys?: string[]
|
|
312
|
+
): Dictionary<string> => {
|
|
313
|
+
const results: Dictionary<string> = {};
|
|
314
|
+
for (const cookie of cookies) {
|
|
315
|
+
const [key, value] = cookie.split('=');
|
|
316
|
+
if (!keys || keys.includes(key)) {
|
|
317
|
+
results[key] = value;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return results;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Parse cookies from document.cookie
|
|
324
|
+
const parsedCookies: string[] = parseCookies();
|
|
325
|
+
|
|
326
|
+
// Filter cookies by keys if provided
|
|
327
|
+
return filterCookies(parsedCookies, keys);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const getHref = (): string => {
|
|
331
|
+
return typeof window !== 'undefined' && window.location
|
|
332
|
+
? window.location.href
|
|
333
|
+
: '';
|
|
334
|
+
};
|
|
335
|
+
|
|
237
336
|
export {
|
|
238
337
|
createCookieString,
|
|
239
338
|
revertCookieString,
|
|
@@ -264,4 +363,7 @@ export {
|
|
|
264
363
|
isValidCustomFlagProperty,
|
|
265
364
|
mergeObjects,
|
|
266
365
|
moveElementToEnd,
|
|
366
|
+
queryStringParser,
|
|
367
|
+
getCookies,
|
|
368
|
+
getHref,
|
|
267
369
|
};
|