Skip to content

Commit

Permalink
refactor: Use fetch instead of XHR for Alias Requests (#915)
Browse files Browse the repository at this point in the history
  • Loading branch information
rmi22186 authored Sep 16, 2024
1 parent 1d6cbdd commit ce70200
Show file tree
Hide file tree
Showing 13 changed files with 874 additions and 114 deletions.
89 changes: 89 additions & 0 deletions src/aliasRequestApiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { IAliasRequest, IAliasCallback } from "./identity.interfaces";
import { MParticleWebSDK } from "./sdkRuntimeModels";
import Constants from './constants';
import { FetchUploader, XHRUploader } from './uploaders';
import { HTTP_ACCEPTED, HTTP_OK } from "./constants";


const { HTTPCodes, Messages } = Constants;

interface IAliasResponseBody {
message?: string
}

export async function sendAliasRequest (mpInstance: MParticleWebSDK, aliasRequest: IAliasRequest, aliasCallback: IAliasCallback) {
const { verbose, error } = mpInstance.Logger;
const { invokeAliasCallback } = mpInstance._Helpers;
const { aliasUrl } = mpInstance._Store.SDKConfig;
const { devToken: apiKey } = mpInstance._Store;

verbose(Messages.InformationMessages.SendAliasHttp);

// https://go.mparticle.com/work/SQDSDKS-6750
const uploadUrl = `https://${aliasUrl}${apiKey}/Alias`;
const uploader = window.fetch
? new FetchUploader(uploadUrl)
: new XHRUploader(uploadUrl);


// https://go.mparticle.com/work/SQDSDKS-6568
const uploadPayload = {
method: 'post',
headers: {
Accept: 'text/plain;charset=UTF-8',
'Content-Type': 'application/json',
},
body: JSON.stringify(aliasRequest),
};

try {
const response = await uploader.upload(uploadPayload);

let message: string;
let aliasResponseBody: IAliasResponseBody;

// FetchUploader returns the response as a JSON object that we have to await
if (response.json) {
// 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.
try {
aliasResponseBody = await response.json();
} catch (e) {
verbose('The request has no response body');
}
} else {
// https://go.mparticle.com/work/SQDSDKS-6568
// XHRUploader returns the response as a string that we need to parse
const xhrResponse = response as unknown as XMLHttpRequest;
// debugger;
aliasResponseBody = xhrResponse.responseText
? JSON.parse(xhrResponse.responseText)
: '';
}

let errorMessage: string;

switch (response.status) {
case HTTP_OK:
case HTTP_ACCEPTED:
// https://go.mparticle.com/work/SQDSDKS-6670
message =
'Successfully sent forwarding stats to mParticle Servers';
break;
default:
// 400 has an error message, but 403 doesn't
if (aliasResponseBody?.message) {
errorMessage = aliasResponseBody.message;
}
message =
'Issue with sending Alias Request to mParticle Servers, received HTTP Code of ' +
response.status;
}

verbose(message);
invokeAliasCallback(aliasCallback, response.status, errorMessage);
} catch (e) {
const err = e as Error;
error('Error sending alias request to mParticle servers. ' + err);
invokeAliasCallback(aliasCallback, HTTPCodes.noHttpCoverage, (err.message));
}
};
8 changes: 7 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,10 @@ export default Constants;

// https://go.mparticle.com/work/SQDSDKS-6080
export const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
export const MILLIS_IN_ONE_SEC = 1000;
export const MILLIS_IN_ONE_SEC = 1000;

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
export const HTTP_OK = 200 as const;
export const HTTP_ACCEPTED = 202 as const;
export const HTTP_BAD_REQUEST = 400 as const;
export const HTTP_FORBIDDEN = 403 as const;
9 changes: 9 additions & 0 deletions src/identity.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ export interface IAliasRequest {
scope?: string;
}

export interface IAliasCallback {
(result: IAliasResult): void;
}

export interface IAliasResult {
httpCode: number;
message: string;
}

export interface SDKIdentityApi {
HTTPCodes: typeof HTTPCodes;
identify?(
Expand Down
5 changes: 2 additions & 3 deletions src/identity.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Constants from './constants';
import Constants, { HTTP_OK } from './constants';
import Types from './types';
import {
cacheOrClearIdCache,
Expand All @@ -13,7 +13,6 @@ const { CacheIdentity } = FeatureFlags;
const { Identify, Modify, Login, Logout } = IdentityMethods;
import {
generateDeprecationMessage,
HTTP_SUCCESS,
isEmpty,
isFunction,
isObject,
Expand Down Expand Up @@ -1513,7 +1512,7 @@ export default function Identity(mpInstance) {
);
}

if (identityResponse.status === HTTP_SUCCESS) {
if (identityResponse.status === HTTP_OK) {
if (getFeatureFlag(CacheIdentity)) {
const identityResponseForCache = xhrIdentityResponseAdapter(
identityResponse
Expand Down
56 changes: 3 additions & 53 deletions src/identityApiClient.js
Original file line number Diff line number Diff line change
@@ -1,65 +1,15 @@
import Constants from './constants';
import { xhrIdentityResponseAdapter } from './identity-utils';
import { sendAliasRequest } from './aliasRequestApiClient';

var HTTPCodes = Constants.HTTPCodes,
Messages = Constants.Messages;

const { Modify } = Constants.IdentityMethods;

export default function IdentityAPIClient(mpInstance) {
this.sendAliasRequest = function(aliasRequest, callback) {
var xhr,
xhrCallback = function() {
if (xhr.readyState === 4) {
// https://go.mparticle.com/work/SQDSDKS-6368
mpInstance.Logger.verbose(
'Received ' + xhr.statusText + ' from server'
);
//only parse error messages from failing requests
if (xhr.status !== 200 && xhr.status !== 202) {
if (xhr.responseText) {
var response = JSON.parse(xhr.responseText);
if (response.hasOwnProperty('message')) {
var errorMessage = response.message;
mpInstance._Helpers.invokeAliasCallback(
callback,
xhr.status,
errorMessage
);
return;
}
}
}
mpInstance._Helpers.invokeAliasCallback(
callback,
xhr.status
);
}
};
mpInstance.Logger.verbose(Messages.InformationMessages.SendAliasHttp);

xhr = mpInstance._Helpers.createXHR(xhrCallback);
if (xhr) {
try {
xhr.open(
'post',
mpInstance._Helpers.createServiceUrl(
mpInstance._Store.SDKConfig.aliasUrl,
mpInstance._Store.devToken
) + '/Alias'
);
xhr.send(JSON.stringify(aliasRequest));
} catch (e) {
mpInstance._Helpers.invokeAliasCallback(
callback,
HTTPCodes.noHttpCoverage,
e
);
mpInstance.Logger.error(
'Error sending alias request to mParticle servers. ' + e
);
}
}
this.sendAliasRequest = async function(aliasRequest, callback) {
await sendAliasRequest(mpInstance, aliasRequest, callback);
};

this.sendIdentityRequest = function(
Expand Down
11 changes: 10 additions & 1 deletion src/sdkRuntimeModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import { IPersistence } from './persistence.interfaces';
import { IMPSideloadedKit } from './sideloadedKit';
import { ISessionManager } from './sessionManager';
import { Kit, MPForwarder } from './forwarders.interfaces';
import { IIdentity, SDKIdentityApi } from './identity.interfaces';
import {
IIdentity,
SDKIdentityApi,
IAliasCallback,
} from './identity.interfaces';
import {
ISDKUserAttributeChangeData,
ISDKUserIdentityChanges,
Expand Down Expand Up @@ -265,6 +269,11 @@ export interface SDKHelpersApi {
generateHash?(value: string): string;
// https://go.mparticle.com/work/SQDSDKS-6317
getFeatureFlag?(feature: string): boolean | string; // TODO: Feature Constants should be converted to enum
invokeAliasCallback(
aliasCallback: IAliasCallback,
number: number,
errorMessage: string
): void;
isDelayedByIntegration?(
delayedIntegrations: Dictionary<boolean>,
timeoutStart: number,
Expand Down
1 change: 1 addition & 0 deletions src/uploaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class XHRUploader extends AsyncUploader {
// 3 LOADING Downloading; responseText holds partial data.
// 4 DONE The operation is complete.

// https://go.mparticle.com/work/SQDSDKS-6736
private async makeRequest(
url: string,
data: string,
Expand Down
2 changes: 0 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ export type Dictionary<V = any> = Record<string, V>;

export type Environment = 'development' | 'production';

export const HTTP_SUCCESS = 200 as const;

const createCookieString = (value: string): string =>
replaceCommasWithPipes(replaceQuotesWithApostrophes(value));

Expand Down
4 changes: 3 additions & 1 deletion test/src/_test.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ import './tests-config-api-client';
import './tests-identity-utils';
import './tests-audience-manager';
import './tests-feature-flags';
import './tests-user'
import './tests-user';
import './tests-legacy-alias-requests';
import './tests-aliasRequestApiClient';
123 changes: 123 additions & 0 deletions test/src/tests-aliasRequestApiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import sinon from 'sinon';
import fetchMock from 'fetch-mock/esm/client';
import { urls, apiKey, MPConfig, testMPID } from './config/constants';
import { MParticleWebSDK } from '../../src/sdkRuntimeModels';
import { expect } from 'chai';
import { sendAliasRequest } from '../../src/aliasRequestApiClient';
import { IAliasCallback, IAliasRequest } from '../../src/identity.interfaces';
import { HTTP_ACCEPTED, HTTP_BAD_REQUEST, HTTP_FORBIDDEN, HTTP_OK } from '../../src/constants';

declare global {
interface Window {
mParticle: MParticleWebSDK;
fetchMock: any;
}
}

let mockServer;
const mParticle = window.mParticle;

declare global {
interface Window {
mParticle: MParticleWebSDK;
fetchMock: any;
}
}

const aliasUrl = 'https://jssdks.mparticle.com/v1/identity/test_key/Alias';

describe('Alias Request Api Client', function() {
beforeEach(function() {
fetchMock.post(urls.events, 200);
mockServer = sinon.createFakeServer();
mockServer.respondImmediately = true;

mockServer.respondWith(urls.identify, [
200,
{},
JSON.stringify({ mpid: testMPID, is_logged_in: false }),
]);
mParticle.init(apiKey, window.mParticle.config);
});

afterEach(function() {
mockServer.restore();
fetchMock.restore();
mParticle._resetForTests(MPConfig);
});

it('should have just an httpCode on the result passed to the callback on a 200', async () => {
const mpInstance: MParticleWebSDK = mParticle.getInstance();
const aliasRequest: IAliasRequest = {
destinationMpid: '123',
sourceMpid: '456',
startTime: 10001230123,
endTime: 10001231123
};

const aliasCallback = sinon.spy()
fetchMock.post(aliasUrl, HTTP_OK);

await sendAliasRequest(mpInstance, aliasRequest, aliasCallback);
expect(aliasCallback.calledOnce).to.eq(true);
const callbackArgs = aliasCallback.getCall(0).args
expect(callbackArgs[0]).to.deep.equal({httpCode: HTTP_OK});
});

it('should have just an httpCode on the result passed to the callback on a 202', async () => {
const mpInstance: MParticleWebSDK = mParticle.getInstance();
const aliasRequest: IAliasRequest = {
destinationMpid: '123',
sourceMpid: '456',
startTime: 10001230123,
endTime: 10001231123
};

const aliasCallback = sinon.spy()
fetchMock.post(aliasUrl, HTTP_ACCEPTED);

await sendAliasRequest(mpInstance, aliasRequest, aliasCallback);
expect(aliasCallback.calledOnce).to.eq(true);
const callbackArgs = aliasCallback.getCall(0).args
expect(callbackArgs[0]).to.deep.equal({httpCode: HTTP_ACCEPTED});
});

it('should have just an httpCode on the result passed to the callback on a 400', async () => {
const mpInstance: MParticleWebSDK = mParticle.getInstance();
const aliasRequest: IAliasRequest = {
destinationMpid: '123',
sourceMpid: '456',
startTime: 10001230123,
endTime: 10001231123
};

const aliasCallback = sinon.spy()
fetchMock.post(aliasUrl, HTTP_BAD_REQUEST);

await sendAliasRequest(mpInstance, aliasRequest, aliasCallback);
expect(aliasCallback.calledOnce).to.eq(true);
const callbackArgs = aliasCallback.getCall(0).args
expect(callbackArgs[0]).to.deep.equal({httpCode: HTTP_BAD_REQUEST});
});

it('should have an httpCode and an error message passed to the callback on a 403', async () => {
const mpInstance: MParticleWebSDK = mParticle.getInstance();
const aliasRequest: IAliasRequest = {
destinationMpid: '123',
sourceMpid: '456',
startTime: 10001230123,
endTime: 10001231123
};

const aliasCallback = sinon.spy()
fetchMock.post(aliasUrl, {
status: HTTP_FORBIDDEN,
body: JSON.stringify({message: 'error'}),
});

await sendAliasRequest(mpInstance, aliasRequest, aliasCallback);
expect(aliasCallback.calledOnce).to.eq(true);
const callbackArgs = aliasCallback.getCall(0).args
expect(callbackArgs[0]).to.deep.equal({httpCode: HTTP_FORBIDDEN, message: 'error'});
});
});
Loading

0 comments on commit ce70200

Please sign in to comment.