Skip to content

Commit

Permalink
feat: Refactor usage of XHR for Config API (#898)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexs-mparticle authored Aug 26, 2024
1 parent f746c6a commit 0b6fae6
Show file tree
Hide file tree
Showing 15 changed files with 804 additions and 539 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"before": true,
"beforeEach": true,
"afterEach": true,
"after": true
"after": true,
"Promise": true
},
"extends": ["plugin:prettier/recommended", "eslint:recommended"],
"plugins": ["prettier"],
Expand Down
157 changes: 85 additions & 72 deletions src/configAPIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import {
SDKInitConfig,
} from './sdkRuntimeModels';
import { Dictionary } from './utils';

export type SDKCompleteInitCallback = (
apiKey: string,
config: SDKInitConfig,
mpInstance: MParticleWebSDK
) => void;
import {
AsyncUploader,
fetchPayload,
FetchUploader,
XHRUploader,
} from './uploaders';

export interface IKitConfigs extends IKitFilterSettings {
name: string;
Expand Down Expand Up @@ -47,13 +47,13 @@ export interface IKitFilterSettings {
export interface IFilteringEventAttributeValue {
eventAttributeName: string;
eventAttributeValue: string;
includeOnMatch: boolean
includeOnMatch: boolean;
}

export interface IFilteringUserAttributeValue {
userAttributeName: string;
userAttributeValue: string;
includeOnMatch: boolean
includeOnMatch: boolean;
}
export interface IFilteringConsentRuleValues {
includeOnMatch: boolean;
Expand All @@ -62,10 +62,9 @@ export interface IFilteringConsentRuleValues {

export interface IConsentRuleValue {
consentPurpose: string;
hasConsented: boolean
hasConsented: boolean;
}


export interface IPixelConfig {
name: string;
moduleId: number;
Expand All @@ -91,78 +90,92 @@ export interface IConfigResponse {
}

export interface IConfigAPIClient {
getSDKConfiguration: (
apiKey: string,
config: SDKInitConfig,
completeSDKInitialization: SDKCompleteInitCallback,
mpInstance: MParticleWebSDK
) => void;
apiKey: string;
config: SDKInitConfig;
mpInstance: MParticleWebSDK;
getSDKConfiguration: () => Promise<IConfigResponse>;
}

export default function ConfigAPIClient(this: IConfigAPIClient) {
this.getSDKConfiguration = (
apiKey,
config,
completeSDKInitialization,
mpInstance
): void => {
let url: string;
const buildUrl = (
configUrl: string,
apiKey: string,
dataPlanConfig?: DataPlanConfig | null,
isDevelopmentMode?: boolean | null
): string => {
const url = configUrl + apiKey + '/config';
const env = isDevelopmentMode ? '1' : '0';
const queryParams = [`env=${env}`];

const { planId, planVersion } = dataPlanConfig || {};

if (planId) {
queryParams.push(`plan_id=${planId}`);
}

if (planVersion) {
queryParams.push(`plan_version=${planVersion}`);
}

return `${url}?${queryParams.join('&')}`;
};

export default function ConfigAPIClient(
this: IConfigAPIClient,
apiKey: string,
config: SDKInitConfig,
mpInstance: MParticleWebSDK
): void {
const baseUrl = 'https://' + mpInstance._Store.SDKConfig.configUrl;
const { isDevelopmentMode } = config;
const dataPlan = config.dataPlan as DataPlanConfig;
const uploadUrl = buildUrl(baseUrl, apiKey, dataPlan, isDevelopmentMode);
const uploader: AsyncUploader = window.fetch
? new FetchUploader(uploadUrl)
: new XHRUploader(uploadUrl);

this.getSDKConfiguration = async (): Promise<IConfigResponse> => {
let configResponse: IConfigResponse;
const fetchPayload: fetchPayload = {
method: 'get',
headers: {
Accept: 'text/plain;charset=UTF-8',
'Content-Type': 'text/plain;charset=UTF-8',
},
body: null,
};

try {
const xhrCallback = function() {
if (xhr.readyState === 4) {
// when a 200 returns, merge current config with what comes back from config, prioritizing user inputted config
if (xhr.status === 200) {
config = mpInstance._Helpers.extend(
{},
config,
JSON.parse(xhr.responseText)
);
completeSDKInitialization(apiKey, config, mpInstance);
mpInstance.Logger.verbose(
'Successfully received configuration from server'
);
} else {
// if for some reason a 200 doesn't return, then we initialize with the just the passed through config
completeSDKInitialization(apiKey, config, mpInstance);
mpInstance.Logger.verbose(
'Issue with receiving configuration from server, received HTTP Code of ' +
xhr.status
);
}
}
};

const xhr = mpInstance._Helpers.createXHR(xhrCallback);
url =
'https://' +
mpInstance._Store.SDKConfig.configUrl +
apiKey +
'/config?env=';
if (config.isDevelopmentMode) {
url = url + '1';
} else {
url = url + '0';
}
const response = await uploader.upload(fetchPayload);
if (response.status === 200) {
mpInstance?.Logger?.verbose(
'Successfully received configuration from server'
);

const dataPlan = config.dataPlan as DataPlanConfig;
if (dataPlan) {
if (dataPlan.planId) {
url = url + '&plan_id=' + dataPlan.planId || '';
}
if (dataPlan.planVersion) {
url = url + '&plan_version=' + dataPlan.planVersion || '';
// https://go.mparticle.com/work/SQDSDKS-6568
// FetchUploader returns the response as a JSON object that we have to await
if (response.json) {
configResponse = await response.json();
return configResponse;
} 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;
configResponse = JSON.parse(xhrResponse.responseText);
return configResponse;
}
}

if (xhr) {
xhr.open('get', url);
xhr.send(null);
}
mpInstance?.Logger?.verbose(
'Issue with receiving configuration from server, received HTTP Code of ' +
response.statusText
);
} catch (e) {
completeSDKInitialization(apiKey, config, mpInstance);
mpInstance.Logger.error(
mpInstance?.Logger?.error(
'Error getting forwarder configuration from mParticle servers.'
);
}

// Returns the original config object if we cannot retrieve the remote config
return config as IConfigResponse;
};
}
15 changes: 8 additions & 7 deletions src/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -1665,7 +1665,8 @@ export default function Identity(mpInstance) {

mpInstance.Logger.verbose('Successfully parsed Identity Response');

mpInstance._APIClient.processQueuedEvents();
// https://go.mparticle.com/work/SQDSDKS-6654
mpInstance._APIClient?.processQueuedEvents();
} catch (e) {
if (callback) {
mpInstance._Helpers.invokeCallback(
Expand Down Expand Up @@ -1716,7 +1717,7 @@ export default function Identity(mpInstance) {
isNewUserIdentityType,
currentUserInMemory
);
mpInstance._APIClient.sendEventToServer(
mpInstance._APIClient?.sendEventToServer(
userIdentityChangeEvent
);
}
Expand Down Expand Up @@ -1770,7 +1771,7 @@ export default function Identity(mpInstance) {
user
);
if (userAttributeChangeEvent) {
mpInstance._APIClient.sendEventToServer(userAttributeChangeEvent);
mpInstance._APIClient?.sendEventToServer(userAttributeChangeEvent);
}
};

Expand Down Expand Up @@ -1807,7 +1808,7 @@ export default function Identity(mpInstance) {

this.reinitForwardersOnUserChange = function(prevUser, newUser) {
if (hasMPIDAndUserLoginChanged(prevUser, newUser)) {
mpInstance._Forwarders.initForwarders(
mpInstance._Forwarders?.initForwarders(
newUser.getUserIdentities().userIdentities,
mpInstance._APIClient.prepareForwardingStats
);
Expand All @@ -1816,11 +1817,11 @@ export default function Identity(mpInstance) {

this.setForwarderCallbacks = function(user, method) {
// https://go.mparticle.com/work/SQDSDKS-6036
mpInstance._Forwarders.setForwarderUserIdentities(
mpInstance._Forwarders?.setForwarderUserIdentities(
user.getUserIdentities().userIdentities
);
mpInstance._Forwarders.setForwarderOnIdentityComplete(user, method);
mpInstance._Forwarders.setForwarderOnUserIdentified(user);
mpInstance._Forwarders?.setForwarderOnIdentityComplete(user, method);
mpInstance._Forwarders?.setForwarderOnUserIdentified(user);
};
}

Expand Down
13 changes: 11 additions & 2 deletions src/mp-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,21 @@ export default function mParticleInstance(instanceName) {
!config.hasOwnProperty('requestConfig') ||
config.requestConfig
) {
new ConfigAPIClient().getSDKConfiguration(
const configApiClient = new ConfigAPIClient(
apiKey,
config,
completeSDKInitialization,
this
);

configApiClient.getSDKConfiguration().then(result => {
const mergedConfig = this._Helpers.extend(
{},
config,
result
);

completeSDKInitialization(apiKey, mergedConfig, this);
});
} else {
completeSDKInitialization(apiKey, config, this);
}
Expand Down
2 changes: 1 addition & 1 deletion src/sdkRuntimeModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export interface SDKHelpersApi {
canLog?(): boolean;
createMainStorageName?(workspaceToken: string): string;
createProductStorageName?(workspaceToken: string): string;
createServiceUrl(url: string, devToken?: string): void;
createServiceUrl(url: string, devToken?: string): string;
createXHR?(cb: () => void): XMLHttpRequest;
extend?(...args: any[]);
parseNumber?(value: string | number): number;
Expand Down
21 changes: 17 additions & 4 deletions src/uploaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,39 @@ export class XHRUploader extends AsyncUploader {
public async upload(fetchPayload: fetchPayload): Promise<Response> {
const response: Response = await this.makeRequest(
this.url,
fetchPayload.body
fetchPayload.body,
fetchPayload.method as 'post' | 'get'
);
return response;
}

private async makeRequest(url: string, data: string): Promise<Response> {
// XHR Ready States
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState
// 0 UNSENT open() has not been called yet.
// 1 OPENED send() has been called.
// 2 HEADERS_RECEIVED send() has been called, and headers and status are available.
// 3 LOADING Downloading; responseText holds partial data.
// 4 DONE The operation is complete.

private async makeRequest(
url: string,
data: string,
method: 'post' | 'get' = 'post'
): Promise<Response> {
const xhr: XMLHttpRequest = new XMLHttpRequest();
return new Promise((resolve, reject) => {
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) return;

// Process the response
if (xhr.status >= 200 && xhr.status < 300) {
resolve((xhr as unknown) as Response);
resolve(xhr as unknown as Response);
} else {
reject(xhr);
}
};

xhr.open('post', url);
xhr.open(method, url);
xhr.send(data);
});
}
Expand Down
8 changes: 8 additions & 0 deletions test/src/config/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,14 @@ var pluses = /\+/g,
findRequest = function(requests, eventName) {
let matchingRequest;
requests.forEach(function(request) {
// Initial implementation of this function was to find the
// first request that contained a batch that matched the event name
// which would have been a post request. However, this was not
// we are now using 'get' requests for config api requests
// and will likey use them for other requests in the future
if (request[1].method.toLowerCase() === 'get') {
return null;
}
var batch = JSON.parse(request[1].body);
for (var i = 0; i<batch.events.length; i++) {
var foundEventFromBatch = findEventFromBatch(batch, eventName);
Expand Down
Loading

0 comments on commit 0b6fae6

Please sign in to comment.