import Constants, { HTTP_ACCEPTED, HTTP_BAD_REQUEST, HTTP_OK, HTTP_SERVER_ERROR } from './constants'; import { AsyncUploader, FetchUploader, XHRUploader, IFetchPayload, } from './uploaders'; import { CACHE_HEADER, IKnownIdentities } from './identity-utils'; import { obfuscateData, parseNumber, valueof, getErrorMessage } from './utils'; import { IAliasCallback, IAliasNetworkRequest, IdentityAPIMethod, IIdentity, IIdentityAPIRequestData, IIdentityAPIModifyRequestData, } from './identity.interfaces'; import { IdentityApiData, MPID, } from '@mparticle/web-sdk'; import { IdentityCallback, IdentityResultBody, IIdentityResponse, } from './identity-user-interfaces'; import { IMParticleWebSDKInstance } from './mp-instance'; import { ErrorCodes, WSDKErrorSeverity } from './reporting/types'; const { HTTPCodes, Messages, IdentityMethods } = Constants; const { Modify } = IdentityMethods; export interface IIdentityApiClient { sendAliasRequest: ( aliasRequest: IAliasNetworkRequest, aliasCallback: IAliasCallback ) => Promise; sendIdentityRequest: ( identityApiRequest: IIdentityAPIRequestData | IIdentityAPIModifyRequestData, method: IdentityAPIMethod, callback: IdentityCallback, originalIdentityApiData: IdentityApiData, parseIdentityResponse: IIdentity['parseIdentityResponse'], mpid: MPID, knownIdentities?: IKnownIdentities ) => Promise; getUploadUrl: (method: IdentityAPIMethod, mpid: MPID) => string; getIdentityResponseFromFetch: ( response: Response, responseBody: IdentityResultBody ) => IIdentityResponse; getIdentityResponseFromXHR: (response: XMLHttpRequest) => IIdentityResponse; } // A successfull Alias request will return a 202 with no body export interface IAliasResponseBody {} interface IdentityApiRequestPayload extends IFetchPayload { headers: { Accept: string; 'Content-Type': string; 'x-mp-key': string; }; } type HTTP_STATUS_CODES = typeof HTTP_OK | typeof HTTP_ACCEPTED; interface IdentityApiError { code: string; message: string; } interface IdentityApiErrorResponse { Errors: IdentityApiError[], ErrorCode: string, StatusCode: valueof; RequestId: string; } // All Identity Api Responses have the same structure, except for Alias interface IAliasErrorResponse extends IdentityApiError {} export default function IdentityAPIClient( this: IIdentityApiClient, mpInstance: IMParticleWebSDKInstance ) { this.sendAliasRequest = async function( aliasRequest: IAliasNetworkRequest, aliasCallback: IAliasCallback ) { const { Logger } = mpInstance; const { invokeAliasCallback } = mpInstance._Helpers; const { aliasUrl } = mpInstance._Store.SDKConfig; const { devToken: apiKey } = mpInstance._Store; Logger.verbose(Messages.InformationMessages.SendAliasHttp); // https://go.mparticle.com/work/SQDSDKS-6750 const uploadUrl = `https://${aliasUrl}${apiKey}/Alias`; const uploader: AsyncUploader = window.fetch ? new FetchUploader(uploadUrl) : new XHRUploader(uploadUrl); // https://go.mparticle.com/work/SQDSDKS-6568 const uploadPayload: IFetchPayload = { method: 'post', headers: { Accept: 'text/plain;charset=UTF-8', 'Content-Type': 'application/json', }, body: JSON.stringify(aliasRequest), }; try { const response: Response = await uploader.upload(uploadPayload); let aliasResponseBody: IAliasResponseBody; let message: string; let errorMessage: string; switch (response.status) { // A successfull Alias request will return without a body case HTTP_ACCEPTED: case HTTP_OK: // https://go.mparticle.com/work/SQDSDKS-6670 message = 'Received Alias Response from server: ' + JSON.stringify(response.status); break; // Our Alias Request API will 400 if there is an issue with the request body (ie timestamps are too far // in the past or MPIDs don't exist). // A 400 will return an error in the response body and will go through the happy path to report the error case HTTP_BAD_REQUEST: // 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. if (response.json) { try { aliasResponseBody = await response.json(); } catch (e) { Logger.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; aliasResponseBody = xhrResponse.responseText ? JSON.parse(xhrResponse.responseText) : ''; } const errorResponse: IAliasErrorResponse = aliasResponseBody as unknown as IAliasErrorResponse; if (errorResponse?.message) { errorMessage = errorResponse.message; } message = 'Issue with sending Alias Request to mParticle Servers, received HTTP Code of ' + response.status; if (errorResponse?.code) { message += ' - ' + errorResponse.code; } break; // Any unhandled errors, such as 500 or 429, will be caught here as well default: { throw new Error('Received HTTP Code of ' + response.status); } } Logger.verbose(message); invokeAliasCallback(aliasCallback, response.status, errorMessage); } catch (e) { const errorMessage = getErrorMessage(e); Logger.error('Error sending alias request to mParticle servers. ' + errorMessage); invokeAliasCallback( aliasCallback, HTTPCodes.noHttpCoverage, errorMessage, ); } }; this.sendIdentityRequest = async function( identityApiRequest: IIdentityAPIRequestData | IIdentityAPIModifyRequestData, method: IdentityAPIMethod, callback: IdentityCallback, originalIdentityApiData: IdentityApiData, parseIdentityResponse: IIdentity['parseIdentityResponse'], mpid: MPID, knownIdentities?: IKnownIdentities ) { if (mpInstance._RoktManager?.isInitialized) { mpInstance._Store.identifyRequestCount = (mpInstance._Store.identifyRequestCount || 0) + 1; const requestCount = mpInstance._Store.identifyRequestCount; mpInstance.captureTiming(`${requestCount}-identityRequestStart`); } const { invokeCallback } = mpInstance._Helpers; const { Logger, _ErrorReportingDispatcher: errorReporter } = mpInstance; Logger.verbose(Messages.InformationMessages.SendIdentityBegin); if (!identityApiRequest) { Logger.error(Messages.ErrorMessages.APIRequestEmpty); return; } Logger.verbose(Messages.InformationMessages.SendIdentityHttp); 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: AsyncUploader = window.fetch ? new FetchUploader(uploadUrl) : new XHRUploader(uploadUrl); // https://go.mparticle.com/work/SQDSDKS-6568 const fetchPayload: IdentityApiRequestPayload = { method: 'post', headers: { Accept: 'text/plain;charset=UTF-8', 'Content-Type': 'application/json', 'x-mp-key': mpInstance._Store.devToken, }, body: JSON.stringify(identityApiRequest), }; mpInstance._Store.identityCallInFlight = true; try { const response: Response = await uploader.upload(fetchPayload); let identityResponse: IIdentityResponse; let message: string; switch (response.status) { case HTTP_ACCEPTED: case HTTP_OK: // Our Identity API will return a 400 error if there is an issue with the requeest body // such as if the body is empty or one of the attributes is missing or malformed // A 400 will return an error in the response body and will go through the happy path to report the error case HTTP_BAD_REQUEST: // FetchUploader returns the response as a JSON object that we have to await 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: IdentityResultBody = await response.json(); identityResponse = this.getIdentityResponseFromFetch( response, responseBody ); } else { identityResponse = this.getIdentityResponseFromXHR( (response as unknown) as XMLHttpRequest ); } if (identityResponse.status === HTTP_BAD_REQUEST) { const errorResponse: IdentityApiErrorResponse = identityResponse.responseText as unknown as IdentityApiErrorResponse; message = 'Issue with sending Identity Request to mParticle Servers, received HTTP Code of ' + identityResponse.status; if (errorResponse?.Errors) { const errorMessage = errorResponse.Errors.map((error) => error.message).join(', '); message += ' - ' + errorMessage; } } else if (Logger.isVerbose()) { const responseText = identityResponse.responseText; const { isDevelopmentMode } = mpInstance._Store.SDKConfig; let responseToLog = responseText; if (!isDevelopmentMode && responseText?.matched_identities) { responseToLog = { ...responseText, matched_identities: obfuscateData(responseText.matched_identities) }; } message = 'Received Identity Response from server: ' + JSON.stringify(responseToLog); } break; // Our Identity API will return: // 401 if the `x-mp-key` is incorrect or missing // 403 if the there is a permission or account issue related to the `x-mp-key` // 401 and 403 have no response bodies and should be rejected outright // 5xx server error throws error but RoktManager continues default: { if (response.status >= HTTP_SERVER_ERROR) { throw new Error('Received HTTP Code of ' + response.status); } mpInstance._Store.identityCallInFlight = false; mpInstance._Store.identityCallFailed = false; const errorMessage = 'Received HTTP Code of ' + response.status; Logger.error('Error sending identity request to servers - ' + errorMessage); invokeCallback(callback, HTTPCodes.noHttpCoverage, errorMessage); return; } } mpInstance._Store.identityCallInFlight = false; mpInstance._Store.identityCallFailed = false; if (message) { Logger.verbose(message); } if (mpInstance._RoktManager?.isInitialized) { const requestCount = mpInstance._Store.identifyRequestCount; mpInstance.captureTiming(`${requestCount}-identityRequestEnd`); } parseIdentityResponse( identityResponse, previousMPID, callback, originalIdentityApiData, method, knownIdentities, false ); } catch (err) { mpInstance._Store.identityCallInFlight = false; mpInstance._Store.identityCallFailed = true; if (mpInstance._RoktManager?.isInitialized) { const requestCount = mpInstance._Store.identifyRequestCount; mpInstance.captureTiming(`${requestCount}-identityRequestEnd`); } const errorMessage = getErrorMessage(err); const msg = 'Error sending identity request to servers' + ' - ' + errorMessage; Logger.error(msg); errorReporter?.report({ message: msg, code: ErrorCodes.IDENTITY_REQUEST, severity: WSDKErrorSeverity.ERROR, }); mpInstance.processQueueOnIdentityFailure?.(); invokeCallback(callback, HTTPCodes.noHttpCoverage, errorMessage); } }; this.getUploadUrl = (method: IdentityAPIMethod, mpid: MPID) => { const uploadServiceUrl: string = mpInstance._Helpers.createServiceUrl( mpInstance._Store.SDKConfig.identityUrl ); const uploadUrl: string = method === Modify ? uploadServiceUrl + mpid + '/' + method : uploadServiceUrl + method; return uploadUrl; }; this.getIdentityResponseFromFetch = ( response: Response, responseBody: IdentityResultBody ): IIdentityResponse => ({ status: response.status, responseText: responseBody, cacheMaxAge: parseInt(response.headers.get(CACHE_HEADER)) || 0, expireTimestamp: 0, }); this.getIdentityResponseFromXHR = ( response: XMLHttpRequest ): IIdentityResponse => ({ status: response.status, responseText: response.responseText ? JSON.parse(response.responseText) : {}, cacheMaxAge: parseNumber( response.getResponseHeader(CACHE_HEADER) || '' ), expireTimestamp: 0, }); }