Skip to content

Commit

Permalink
feat: Replace XHR in IdentityApiClient with AsyncUploader (#909)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexs-mparticle committed Oct 11, 2024
1 parent 6d042df commit 9526e3a
Show file tree
Hide file tree
Showing 6 changed files with 2,052 additions and 882 deletions.
29 changes: 1 addition & 28 deletions src/identity-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from './identity-user-interfaces';

const { Identify, Modify, Login, Logout } = Constants.IdentityMethods;
const CACHE_HEADER = 'x-mp-max-age' as const;
export const CACHE_HEADER = 'x-mp-max-age' as const;

export type IdentityCache = BaseVault<Dictionary<ICachedIdentityCall>>;

Expand All @@ -40,33 +40,6 @@ export interface ICachedIdentityCall {
expireTimestamp: number;
}

// https://go.mparticle.com/work/SQDSDKS-6568
// Temporary adapter to convert the XMLHttpRequest response to the IIdentityResponse interface
export const xhrIdentityResponseAdapter = (
possiblyXhr: XMLHttpRequest | IIdentityResponse
): IIdentityResponse => {
if (possiblyXhr.hasOwnProperty('expireTimestamp')) {
// If there is an `expireTimestamp`, it is an IIdentityResponse object, so just return it. This indicates it was a previously cached value.
return possiblyXhr as IIdentityResponse;
} else {
// If there is no `expireTimestamp`, then it is an XHR object and needs to be parsed.
return {
status: possiblyXhr.status,

// Sometimes responseText can be an empty string, such as a 404 response
responseText: (possiblyXhr as XMLHttpRequest).responseText
? JSON.parse((possiblyXhr as XMLHttpRequest).responseText)
: {},
cacheMaxAge: parseNumber(
(possiblyXhr as XMLHttpRequest)?.getResponseHeader(
CACHE_HEADER
) || ''
),
expireTimestamp: 0,
};
}
};

export const cacheOrClearIdCache = (
method: string,
knownIdentities: IKnownIdentities,
Expand Down
6 changes: 1 addition & 5 deletions src/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
cacheOrClearIdCache,
createKnownIdentities,
tryCacheIdentity,
xhrIdentityResponseAdapter,
} from './identity-utils';
import AudienceManager from './audienceManager';
const { Messages, HTTPCodes, FeatureFlags, IdentityMethods } = Constants;
Expand Down Expand Up @@ -1514,14 +1513,11 @@ export default function Identity(mpInstance) {

if (identityResponse.status === HTTP_OK) {
if (getFeatureFlag(CacheIdentity)) {
const identityResponseForCache = xhrIdentityResponseAdapter(
identityResponse
);
cacheOrClearIdCache(
method,
knownIdentities,
self.idCache,
identityResponseForCache,
identityResponse,
parsingCachedResponse
);
}
Expand Down
182 changes: 100 additions & 82 deletions src/identityApiClient.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Constants from './constants';
import { xhrIdentityResponseAdapter } from './identity-utils';
import { sendAliasRequest } from './aliasRequestApiClient';
import { FetchUploader, XHRUploader } from './uploaders';
import { CACHE_HEADER } from './identity-utils';
import { parseNumber } from './utils';

var HTTPCodes = Constants.HTTPCodes,
Messages = Constants.Messages;
Expand All @@ -12,7 +14,7 @@ export default function IdentityAPIClient(mpInstance) {
await sendAliasRequest(mpInstance, aliasRequest, callback);
};

this.sendIdentityRequest = function(
this.sendIdentityRequest = async function(
identityApiRequest,
method,
callback,
Expand All @@ -21,93 +23,109 @@ export default function IdentityAPIClient(mpInstance) {
mpid,
knownIdentities
) {
var xhr,
previousMPID,
xhrCallback = function() {
if (xhr.readyState === 4) {
// https://go.mparticle.com/work/SQDSDKS-6368
mpInstance.Logger.verbose(
'Received ' + xhr.statusText + ' from server'
);

// https://go.mparticle.com/work/SQDSDKS-6565
const identityResponse = xhrIdentityResponseAdapter(xhr);
parseIdentityResponse(
identityResponse,
previousMPID,
callback,
originalIdentityApiData,
method,
knownIdentities,
false
);
}
};

mpInstance.Logger.verbose(
Messages.InformationMessages.SendIdentityBegin
);
const { verbose, error } = mpInstance.Logger;
const { invokeCallback } = mpInstance._Helpers;

verbose(Messages.InformationMessages.SendIdentityBegin);
if (!identityApiRequest) {
mpInstance.Logger.error(Messages.ErrorMessages.APIRequestEmpty);
error(Messages.ErrorMessages.APIRequestEmpty);
return;
}
verbose(Messages.InformationMessages.SendIdentityHttp);

mpInstance.Logger.verbose(
Messages.InformationMessages.SendIdentityHttp
);
xhr = mpInstance._Helpers.createXHR(xhrCallback);

if (xhr) {
try {
if (mpInstance._Store.identityCallInFlight) {
mpInstance._Helpers.invokeCallback(
callback,
HTTPCodes.activeIdentityRequest,
'There is currently an Identity request processing. Please wait for this to return before requesting again'
);
} else {
previousMPID = mpid || null;
if (method === Modify) {
xhr.open(
'post',
mpInstance._Helpers.createServiceUrl(
mpInstance._Store.SDKConfig.identityUrl
) +
mpid +
'/' +
method
);
} else {
xhr.open(
'post',
mpInstance._Helpers.createServiceUrl(
mpInstance._Store.SDKConfig.identityUrl
) + method
);
}
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader(
'x-mp-key',
mpInstance._Store.devToken
);
mpInstance._Store.identityCallInFlight = true;
xhr.send(JSON.stringify(identityApiRequest));
}
} catch (e) {
mpInstance._Store.identityCallInFlight = false;
mpInstance._Helpers.invokeCallback(
callback,
HTTPCodes.noHttpCoverage,
e
);
mpInstance.Logger.error(
'Error sending identity request to servers with status code ' +
xhr.status +
' - ' +
e
if (mpInstance._Store.identityCallInFlight) {
invokeCallback(
callback,
HTTPCodes.activeIdentityRequest,
'There is currently an Identity request processing. Please wait for this to return before requesting again'
);
return;
}

const previousMPID = mpid || null;
const uploadUrl = this.getUploadUrl(method, mpid);

const uploader = window.fetch
? new FetchUploader(uploadUrl)
: new XHRUploader(uploadUrl);

const fetchPayload = {
method: 'post',
headers: {
Accept: 'text/plain;charset=UTF-8',
'Content-Type': 'application/json',
'x-mp-key': mpInstance._Store.devToken,
},
body: JSON.stringify(identityApiRequest),
};

try {
mpInstance._Store.identityCallInFlight = true;
const response = await uploader.upload(fetchPayload);

let identityResponse;

if (response.json) {
// https://go.mparticle.com/work/SQDSDKS-6568
// FetchUploader returns the response as a JSON object that we have to await
const responseBody = await response.json();
identityResponse = this.getIdentityResponseFromFetch(
response,
responseBody
);
} else {
identityResponse = this.getIdentityResponseFromXHR(response);
}

verbose(
'Received Identity Response from server: ' +
JSON.stringify(identityResponse.responseText)
);

parseIdentityResponse(
identityResponse,
previousMPID,
callback,
originalIdentityApiData,
method,
knownIdentities,
false
);
} catch (err) {
mpInstance._Store.identityCallInFlight = false;
invokeCallback(callback, HTTPCodes.noHttpCoverage, err);
error('Error sending identity request to servers' + ' - ' + err);
}
};

this.getUploadUrl = (method, mpid) => {
const uploadServiceUrl = mpInstance._Helpers.createServiceUrl(
mpInstance._Store.SDKConfig.identityUrl
);

const uploadUrl =
method === Modify
? uploadServiceUrl + mpid + '/' + method
: uploadServiceUrl + method;

return uploadUrl;
};

this.getIdentityResponseFromFetch = (response, responseBody) => ({
status: response.status,
responseText: responseBody,
cacheMaxAge: parseInt(response.headers.get(CACHE_HEADER)) || 0,
expireTimestamp: 0,
});

this.getIdentityResponseFromXHR = response => ({
status: response.status,
responseText: response.responseText
? JSON.parse(response.responseText)
: {},
cacheMaxAge: parseNumber(
response.getResponseHeader(CACHE_HEADER) || ''
),
expireTimestamp: 0,
});
}
14 changes: 11 additions & 3 deletions src/uploaders.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
type HTTPMethod = 'get' | 'post';

export interface fetchPayload {
method: string;
headers: {
Expand Down Expand Up @@ -34,7 +36,8 @@ export class XHRUploader extends AsyncUploader {
const response: Response = await this.makeRequest(
this.url,
fetchPayload.body,
fetchPayload.method as 'post' | 'get'
fetchPayload.method as HTTPMethod,
fetchPayload.headers
);
return response;
}
Expand All @@ -51,7 +54,8 @@ export class XHRUploader extends AsyncUploader {
private async makeRequest(
url: string,
data: string,
method: 'post' | 'get' = 'post'
method: HTTPMethod = 'post',
headers: Record<string, string> = {}
): Promise<Response> {
const xhr: XMLHttpRequest = new XMLHttpRequest();
return new Promise((resolve, reject) => {
Expand All @@ -69,8 +73,12 @@ export class XHRUploader extends AsyncUploader {
reject((xhr as unknown) as Response);
};


xhr.open(method, url);

Object.entries(headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value);
});

xhr.send(data);
});
}
Expand Down
31 changes: 28 additions & 3 deletions test/src/config/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,12 @@ var pluses = /\+/g,
return null;
}
var batch = JSON.parse(request[1].body);
for (var i = 0; i<batch.events.length; i++) {

if (!batch.events) {
return null;
}

for (var i = 0; i < batch.events.length; i++) {
var foundEventFromBatch = findEventFromBatch(batch, eventName);
if (foundEventFromBatch) {
matchingRequest = request;
Expand Down Expand Up @@ -584,7 +589,26 @@ var pluses = /\+/g,
var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
}
}
},
waitForCondition = function async(
conditionFn,
timeout = 200,
interval = 10
) {
return new Promise((resolve, reject) => {
const startTime = Date.now();

(function poll() {
if (conditionFn()) {
return resolve(undefined);
} else if (Date.now() - startTime > timeout) {
return reject(new Error('Timeout waiting for condition'));
} else {
setTimeout(poll, interval);
}
})();
});
};

var TestsCore = {
getLocalStorageProducts: getLocalStorageProducts,
Expand All @@ -608,7 +632,8 @@ var TestsCore = {
workspaceToken: workspaceToken,
workspaceCookieName: workspaceCookieName,
forwarderDefaultConfiguration: forwarderDefaultConfiguration,
deleteAllCookies: deleteAllCookies
deleteAllCookies: deleteAllCookies,
waitForCondition: waitForCondition,
};

export default TestsCore;
Loading

0 comments on commit 9526e3a

Please sign in to comment.