diff --git a/src/aliasRequestApiClient.ts b/src/aliasRequestApiClient.ts deleted file mode 100644 index 011ac5422..000000000 --- a/src/aliasRequestApiClient.ts +++ /dev/null @@ -1,90 +0,0 @@ -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"; -import { IIdentityApiClientSendAliasRequest } from "./identityApiClient.interfaces"; - - -const { HTTPCodes, Messages } = Constants; - -interface IAliasResponseBody { - message?: string -} - -export const sendAliasRequest: IIdentityApiClientSendAliasRequest = async function (mpInstance: MParticleWebSDK, aliasRequest: IAliasRequest, aliasCallback: IAliasCallback): Promise { - 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; - - 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)); - } - }; \ No newline at end of file diff --git a/src/apiClient.ts b/src/apiClient.ts index 3a6695ad2..39ee67500 100644 --- a/src/apiClient.ts +++ b/src/apiClient.ts @@ -135,7 +135,10 @@ export default function APIClient( this.queueEventForBatchUpload(event); } - if (event.EventName !== Types.MessageType.AppStateTransition) { + // https://go.mparticle.com/work/SQDSDKS-6935 + // While Event Name is 'usually' a string, there are some cases where it is a number + // in that it could be a type of MessageType Enum + if (event.EventName as unknown as number !== Types.MessageType.AppStateTransition) { if (kitBlocker && kitBlocker.kitBlockingEnabled) { event = kitBlocker.createBlockedEvent(event); } diff --git a/src/audienceManager.ts b/src/audienceManager.ts index aac4b95f2..4771bde49 100644 --- a/src/audienceManager.ts +++ b/src/audienceManager.ts @@ -3,7 +3,7 @@ import { FetchUploader, XHRUploader, AsyncUploader, - fetchPayload + IFetchPayload } from './uploaders'; import Audience from './audience'; @@ -38,7 +38,7 @@ export default class AudienceManager { public async sendGetUserAudienceRequest(mpid: string, callback: (userAudiences: IAudienceMemberships) => void) { this.logger.verbose('Fetching user audiences from server'); - const fetchPayload: fetchPayload = { + const fetchPayload: IFetchPayload = { method: 'GET', headers: { Accept: '*/*', diff --git a/src/batchUploader.ts b/src/batchUploader.ts index ea4b66064..c001b67ce 100644 --- a/src/batchUploader.ts +++ b/src/batchUploader.ts @@ -2,14 +2,14 @@ import { Batch } from '@mparticle/event-models'; import Constants from './constants'; import { SDKEvent, MParticleWebSDK, SDKLoggerApi } from './sdkRuntimeModels'; import { convertEvents } from './sdkToEventsApiConverter'; -import Types from './types'; +import { MessageType } from './types'; import { getRampNumber, isEmpty } from './utils'; import { SessionStorageVault, LocalStorageVault } from './vault'; import { AsyncUploader, FetchUploader, XHRUploader, - fetchPayload, + IFetchPayload, } from './uploaders'; import { IMParticleUser } from './identity-user-interfaces'; @@ -167,29 +167,35 @@ export class BatchUploader { * @param event event that should be queued */ public queueEvent(event: SDKEvent): void { - if (!isEmpty(event)) { - this.eventsQueuedForProcessing.push(event); - if (this.offlineStorageEnabled && this.eventVault) { - this.eventVault.store(this.eventsQueuedForProcessing); - } - this.mpInstance.Logger.verbose( - `Queuing event: ${JSON.stringify(event)}` - ); - this.mpInstance.Logger.verbose( - `Queued event count: ${this.eventsQueuedForProcessing.length}` - ); + if (isEmpty(event)) { + return; + } - // TODO: Remove this check once the v2 code path is removed - // https://go.mparticle.com/work/SQDSDKS-3720 - if ( - !this.batchingEnabled || - Types.TriggerUploadType[event.EventDataType] - ) { - this.prepareAndUpload(false, false); - } + const { verbose } = this.mpInstance.Logger; + + this.eventsQueuedForProcessing.push(event); + if (this.offlineStorageEnabled && this.eventVault) { + this.eventVault.store(this.eventsQueuedForProcessing); + } + + verbose(`Queuing event: ${JSON.stringify(event)}`); + verbose(`Queued event count: ${this.eventsQueuedForProcessing.length}`); + + if (this.shouldTriggerImmediateUpload(event.EventDataType)) { + this.prepareAndUpload(false, false); } } + // https://go.mparticle.com/work/SQDSDKS-3720 + private shouldTriggerImmediateUpload (eventDataType: number): boolean { + const priorityEvents = [ + MessageType.Commerce, + MessageType.UserIdentityChange, + ] as const; + + return !this.batchingEnabled || priorityEvents.includes(eventDataType as typeof priorityEvents[number]); + }; + /** * This implements crucial logic to: * - bucket pending events by MPID, and then by Session, and upload individual batches for each bucket. @@ -366,7 +372,7 @@ export class BatchUploader { logger.verbose(`Batch count: ${uploads.length}`); for (let i = 0; i < uploads.length; i++) { - const fetchPayload: fetchPayload = { + const fetchPayload: IFetchPayload = { method: 'POST', headers: { Accept: BatchUploader.CONTENT_TYPE, diff --git a/src/configAPIClient.ts b/src/configAPIClient.ts index b0705a67d..99a007986 100644 --- a/src/configAPIClient.ts +++ b/src/configAPIClient.ts @@ -9,7 +9,7 @@ import { import { Dictionary } from './utils'; import { AsyncUploader, - fetchPayload, + IFetchPayload, FetchUploader, XHRUploader, } from './uploaders'; @@ -125,7 +125,7 @@ export default function ConfigAPIClient( this.getSDKConfiguration = async (): Promise => { let configResponse: IConfigResponse; - const fetchPayload: fetchPayload = { + const fetchPayload: IFetchPayload = { method: 'get', headers: { Accept: 'text/plain;charset=UTF-8', diff --git a/src/constants.ts b/src/constants.ts index ffa6a9dd1..96390f5ed 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -184,6 +184,11 @@ const Constants = { Login: 'login', Identify: 'identify', }, + + Environment: { + Development: 'development', + Production: 'production', + }, } as const; export default Constants; @@ -196,4 +201,7 @@ export const MILLIS_IN_ONE_SEC = 1000; 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_UNAUTHORIZED = 401 as const; export const HTTP_FORBIDDEN = 403 as const; +export const HTTP_NOT_FOUND = 404 as const; +export const HTTP_SERVER_ERROR = 500 as const; diff --git a/src/cookieSyncManager.ts b/src/cookieSyncManager.ts index d0b9c7cc8..c4d3528fc 100644 --- a/src/cookieSyncManager.ts +++ b/src/cookieSyncManager.ts @@ -1,12 +1,14 @@ import { Dictionary, isEmpty, - createCookieSyncUrl, + createInitialCookieSyncUrl, + isFunction, } from './utils'; import Constants from './constants'; import { MParticleWebSDK } from './sdkRuntimeModels'; import { MPID } from '@mparticle/web-sdk'; import { IConsentRules } from './consent'; +import { IPersistenceMinified } from './persistence.interfaces'; const { Messages } = Constants; const { InformationMessages } = Messages; @@ -15,6 +17,19 @@ export const DAYS_IN_MILLISECONDS = 1000 * 60 * 60 * 24; export type CookieSyncDates = Dictionary; +declare global { + interface Window { + __tcfapi: ( + functionName: string, + version: number, + TCFcallback: ( + inAppTCData: TCData, + success: boolean + ) => void + ) => void; + } +} + export interface IPixelConfiguration { name?: string; moduleId: number; @@ -38,12 +53,13 @@ export interface ICookieSyncManager { mpid: MPID, cookieSyncDates: CookieSyncDates, ) => void; - combineUrlWithRedirect: ( - mpid: MPID, - pixelUrl: string, - redirectUrl: string - ) => string; -} + processPixelConfig: ( + pixelSettings: IPixelConfiguration, + persistence: IPersistenceMinified, + mpid: string, + mpidIsNotInCookies: boolean, + ) => void +}; export default function CookieSyncManager( this: ICookieSyncManager, @@ -68,59 +84,87 @@ export default function CookieSyncManager( return; } - pixelConfigurations.forEach((pixelSettings: IPixelConfiguration) => { - // set requiresConsent to false to start each additional pixel configuration - // set to true only if filteringConsenRuleValues.values.length exists - let requiresConsent = false; - // Filtering rules as defined in UI - const { - filteringConsentRuleValues, - pixelUrl, - redirectUrl, - moduleId, - // Tells you how often we should do a cookie sync (in days) - frequencyCap, - } = pixelSettings; - const { values } = filteringConsentRuleValues || {}; - - if (isEmpty(pixelUrl)) { - return; - } + pixelConfigurations.forEach((pixelSetting: IPixelConfiguration) => + self.processPixelConfig( + pixelSetting, + persistence, + mpid, + mpidIsNotInCookies, + ) + ); + }; - if (!isEmpty(values)) { - requiresConsent = true; - } + this.processPixelConfig = async ( + pixelSettings: IPixelConfiguration, + persistence: IPersistenceMinified, + mpid: string, + mpidIsNotInCookies: boolean, + ): Promise => { + // set requiresConsent to false to start each additional pixel configuration + // set to true only if filteringConsenRuleValues.values.length exists + let requiresConsent = false; + // Filtering rules as defined in UI + const { + filteringConsentRuleValues, + pixelUrl, + redirectUrl, + moduleId, + // Tells you how often we should do a cookie sync (in days) + frequencyCap, + } = pixelSettings; + const { values } = filteringConsentRuleValues || {}; + + if (isEmpty(pixelUrl)) { + return; + } - // If MPID is new to cookies, we should not try to perform the cookie sync - // because a cookie sync can only occur once a user either consents or doesn't. - // we should not check if it's enabled if the user has a blank consent - if (requiresConsent && mpidIsNotInCookies) { - return; - } + if (!isEmpty(values)) { + requiresConsent = true; + } + + // If MPID is new to cookies, we should not try to perform the cookie sync + // because a cookie sync can only occur once a user either consents or doesn't. + // we should not check if it's enabled if the user has a blank consent + if (requiresConsent && mpidIsNotInCookies) { + return; + } - const { isEnabledForUserConsent } = mpInstance._Consent; + const { isEnabledForUserConsent } = mpInstance._Consent; - if (!isEnabledForUserConsent(filteringConsentRuleValues, mpInstance.Identity.getCurrentUser())) { - return; - } + if (!isEnabledForUserConsent(filteringConsentRuleValues, mpInstance.Identity.getCurrentUser())) { + return; + } - const cookieSyncDates: CookieSyncDates = persistence[mpid]?.csd ?? {}; - const lastSyncDateForModule: number = cookieSyncDates[moduleId] || null; + const cookieSyncDates: CookieSyncDates = persistence[mpid]?.csd ?? {}; + const lastSyncDateForModule: number = cookieSyncDates[moduleId] || null; - if (!isLastSyncDateExpired(frequencyCap, lastSyncDateForModule)) { - return; - } + if (!isLastSyncDateExpired(frequencyCap, lastSyncDateForModule)) { + return; + } - // Url for cookie sync pixel - const fullUrl = createCookieSyncUrl(mpid, pixelUrl, redirectUrl) + // Url for cookie sync pixel + const initialCookieSyncUrl = createInitialCookieSyncUrl(mpid, pixelUrl, redirectUrl); + const moduleIdString = moduleId.toString(); + + // The IAB Europe Transparency & Consent Framework requires adding certain query parameters to + // a cookie sync url when using GDPR + // fullUrl will be the initialCookieSyncUrl if an error is thrown, or there if TcfApi is not available. + // Otherwise, it will include the GdprConsent parameters that TCF requires + let fullUrl: string; + try { + fullUrl = isTcfApiAvailable() ? await appendGdprConsentUrl(initialCookieSyncUrl) : initialCookieSyncUrl; + } catch (error) { + fullUrl = initialCookieSyncUrl; + const errorMessage = (error as Error).message || error.toString(); + mpInstance.Logger.error(errorMessage); + } - self.performCookieSync( - fullUrl, - moduleId.toString(), - mpid, - cookieSyncDates - ); - }); + self.performCookieSync( + fullUrl, + moduleIdString, + mpid, + cookieSyncDates + ); }; // Private @@ -159,4 +203,38 @@ export const isLastSyncDateExpired = ( new Date().getTime() > new Date(lastSyncDate).getTime() + frequencyCap * DAYS_IN_MILLISECONDS ); -}; \ No newline at end of file +}; + +export const isTcfApiAvailable = (): boolean => isFunction(window.__tcfapi); + +// This is just a partial definition of TCData for the purposes of our implementation. The full schema can be found here: +// https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#tcdata +type TCData = { + gdprApplies?: boolean; + tcString?: string; +}; + +export async function appendGdprConsentUrl(url: string): Promise { + return new Promise((resolve, reject) => { + try { + const tcfAPICallBack = (inAppTCData: TCData, success: boolean) => { + if (success && inAppTCData) { + // gdprApplies is a boolean, but the query parameter is 1 for true, or 0 for false + const gdprApplies: number = inAppTCData.gdprApplies ? 1 : 0; + const tcString = inAppTCData.tcString; + resolve(`${url}&gdpr=${gdprApplies}&gdpr_consent=${tcString}`); + } else { + resolve(url); // No GDPR data; fallback to original URL + } + } + + window.__tcfapi( + 'getInAppTCData', // function name that requests the TCData + 2, // version of TCF (2.2 as of 1/22/2025) + tcfAPICallBack + ); + } catch (error) { + reject(error); + } + }); +} \ No newline at end of file diff --git a/src/ecommerce.interfaces.ts b/src/ecommerce.interfaces.ts new file mode 100644 index 000000000..053f848c0 --- /dev/null +++ b/src/ecommerce.interfaces.ts @@ -0,0 +1,201 @@ +import { + ProductAction, + Product, + Promotion, + CommerceEvent, +} from '@mparticle/event-models'; +import { + SDKEventAttrs, + SDKEventOptions, + TransactionAttributes, +} from '@mparticle/web-sdk'; +import { valueof } from './utils'; +import { + ProductActionType, + PromotionActionType, + CommerceEventType, + EventType, +} from './types'; +import { + SDKEvent, + SDKEventCustomFlags, + SDKImpression, + SDKProduct, + SDKProductImpression, + SDKPromotion, +} from './sdkRuntimeModels'; + +interface IECommerceShared { + createProduct( + name: string, + sku: string | number, + price: string | number, + quantity?: string | number, + variant?: string, + category?: string, + brand?: string, + position?: number, + couponCode?: string, + attributes?: SDKEventAttrs + ): SDKProduct | null; + createImpression(name: string, product: Product): SDKImpression | null; + createPromotion( + id: string | number, + creative?: string, + name?: string, + position?: number + ): SDKPromotion | null; + createTransactionAttributes( + id: string | number, + affiliation?: string, + couponCode?: string, + revenue?: string | number, + shipping?: string | number, + tax?: number + ): TransactionAttributes | null; + expandCommerceEvent(event: CommerceEvent): SDKEvent[] | null; +} + +export interface SDKCart { + add(product: SDKProduct | SDKProduct[], logEvent?: boolean): void; + remove(product: SDKProduct | SDKProduct[], logEvent?: boolean): void; + clear(): void; +} + +// Used for the public `eCommerce` namespace +export interface SDKECommerceAPI extends IECommerceShared { + logCheckout( + step: number, + option?: string, + attrs?: SDKEventAttrs, + customFlags?: SDKEventCustomFlags + ): void; + logImpression( + impression: SDKProductImpression, + attrs?: SDKEventAttrs, + customFlags?: SDKEventCustomFlags, + eventOptions?: SDKEventOptions + ): void; + logProductAction( + productActionType: valueof, + product: SDKProduct | SDKProduct[], + attrs?: SDKEventAttrs, + customFlags?: SDKEventCustomFlags, + transactionAttributes?: TransactionAttributes, + eventOptions?: SDKEventOptions + ): void; + logPromotion( + type: valueof, + promotion: SDKPromotion, + attrs?: SDKEventAttrs, + customFlags?: SDKEventCustomFlags, + eventOptions?: SDKEventOptions + ): void; + setCurrencyCode(code: string): void; + + /* + * @deprecated + */ + Cart: SDKCart; + + /* + * @deprecated + */ + logPurchase( + transactionAttributes: TransactionAttributes, + product: SDKProduct | SDKProduct[], + clearCart?: boolean, + attrs?: SDKEventAttrs, + customFlags?: SDKEventCustomFlags + ): void; + + /* + * @deprecated + */ + logRefund( + transactionAttributes: TransactionAttributes, + product: SDKProduct | SDKProduct[], + clearCart?: boolean, + attrs?: SDKEventAttrs, + customFlags?: SDKEventCustomFlags + ): void; +} + +interface ExtractedActionAttributes { + Affiliation?: string; + 'Coupon Code'?: string; + 'Total Amount'?: number; + 'Shipping Amount'?: number; + 'Tax Amount'?: number; + 'Checkout Option'?: string; + 'Checkout Step'?: number; + 'Transaction ID'?: string; +} +interface ExtractedProductAttributes { + 'Coupon Code'?: string; + Brand?: string; + Category?: string; + Name?: string; + Id?: string; + 'Item Price'?: number; + Quantity?: number; + Position?: number; + Variant?: string; + 'Total Product Amount': number; +} +interface ExtractedPromotionAttributes { + Id?: string; + Creative?: string; + Name?: string; + Position?: number; +} +interface ExtractedTransactionId { + 'Transaction ID'?: string; +} + +// Used for the private `_Ecommerce` namespace +export interface IECommerce extends IECommerceShared { + buildProductList(event: SDKEvent, product: Product | Product[]): Product[]; + convertProductActionToEventType( + productActionType: valueof + ): // https://go.mparticle.com/work/SQDSDKS-4801 + typeof CommerceEventType | typeof EventType | null; + convertPromotionActionToEventType( + promotionActionType: valueof + ): typeof CommerceEventType | null; + convertTransactionAttributesToProductAction( + transactionAttributes: TransactionAttributes, + productAction: ProductAction + ): void; + createCommerceEventObject( + customFlags: SDKEventCustomFlags, + options?: SDKEventOptions + ): SDKEvent | null; + expandProductAction(commerceEvent: CommerceEvent): SDKEvent[]; + expandProductImpression(commerceEvent: CommerceEvent): SDKEvent[]; + expandPromotionAction(commerceEvent: CommerceEvent): SDKEvent[]; + extractActionAttributes( + attributes: ExtractedActionAttributes, + productAction: ProductAction + ): void; + extractProductAttributes( + attributes: ExtractedProductAttributes, + product: Product + ): void; + extractPromotionAttributes( + attributes: ExtractedPromotionAttributes, + promotion: Promotion + ): void; + extractTransactionId( + attributes: ExtractedTransactionId, + productAction: ProductAction + ): void; + generateExpandedEcommerceName(eventName: string, plusOne: boolean): string; + getProductActionEventName( + productActionType: valueof + ): string; + getPromotionActionEventName( + promotionActionType: valueof + ): string; + sanitizeAmount(amount: string | number, category: string): number; +} diff --git a/src/ecommerce.js b/src/ecommerce.js index 2b6b92088..c492e102e 100644 --- a/src/ecommerce.js +++ b/src/ecommerce.js @@ -5,6 +5,8 @@ var Messages = Constants.Messages; export default function Ecommerce(mpInstance) { var self = this; + + // https://go.mparticle.com/work/SQDSDKS-4801 this.convertTransactionAttributesToProductAction = function( transactionAttributes, productAction @@ -103,8 +105,11 @@ export default function Ecommerce(mpInstance) { return Types.CommerceEventType.ProductRemoveFromCart; case Types.ProductActionType.RemoveFromWishlist: return Types.CommerceEventType.ProductRemoveFromWishlist; + + // https://go.mparticle.com/work/SQDSDKS-4801 case Types.ProductActionType.Unknown: return Types.EventType.Unknown; + case Types.ProductActionType.ViewDetail: return Types.CommerceEventType.ProductViewDetail; default: @@ -139,6 +144,7 @@ export default function Ecommerce(mpInstance) { ); }; + // https://go.mparticle.com/work/SQDSDKS-4801 this.extractProductAttributes = function(attributes, product) { if (product.CouponCode) { attributes['Coupon Code'] = product.CouponCode; @@ -170,12 +176,14 @@ export default function Ecommerce(mpInstance) { attributes['Total Product Amount'] = product.TotalAmount || 0; }; + // https://go.mparticle.com/work/SQDSDKS-4801 this.extractTransactionId = function(attributes, productAction) { if (productAction.TransactionId) { attributes['Transaction Id'] = productAction.TransactionId; } }; + // https://go.mparticle.com/work/SQDSDKS-4801 this.extractActionAttributes = function(attributes, productAction) { self.extractTransactionId(attributes, productAction); @@ -208,6 +216,7 @@ export default function Ecommerce(mpInstance) { } }; + // https://go.mparticle.com/work/SQDSDKS-4801 this.extractPromotionAttributes = function(attributes, promotion) { if (promotion.Id) { attributes['Id'] = promotion.Id; diff --git a/src/events.interfaces.ts b/src/events.interfaces.ts new file mode 100644 index 000000000..82290777a --- /dev/null +++ b/src/events.interfaces.ts @@ -0,0 +1,83 @@ +import { + Callback, + SDKEventAttrs, + SDKEventOptions, + TransactionAttributes, +} from '@mparticle/web-sdk'; +import { + BaseEvent, + SDKEvent, + SDKEventCustomFlags, + SDKProduct, + SDKProductImpression, + SDKPromotion, +} from './sdkRuntimeModels'; +import { valueof } from './utils'; +import { EventType, ProductActionType, PromotionActionType } from './types'; + +// Supports wrapping event handlers functions that will ideally return a specific type +type EventHandlerFunction = (element: HTMLLinkElement | HTMLFormElement) => T; + +export interface IEvents { + addEventHandler( + domEvent: string, + selector: string | Node, + eventName: EventHandlerFunction | string, + data: EventHandlerFunction | SDKEventAttrs, + eventType: valueof + ): void; + logAST(): void; + logCheckoutEvent( + step: number, + + // User options specified during the checkout process + // e.g., FedEx, DHL, UPS for delivery options; + // Visa, MasterCard, AmEx for payment options. + option?: string, + + attrs?: SDKEventAttrs, + customFlags?: SDKEventCustomFlags + ): void; + logCommerceEvent( + commerceEvent: SDKEvent, + attrs?: SDKEventAttrs, + options?: SDKEventOptions + ): void; + logEvent(event: BaseEvent, eventOptions?: SDKEventOptions): void; + logImpressionEvent( + impression: SDKProductImpression, + attrs?: SDKEventAttrs, + customFlags?: SDKEventCustomFlags, + eventOptions?: SDKEventOptions + ); + logOptOut(): void; + logProductActionEvent( + productActionType: valueof, + product: SDKProduct, + attrs?: SDKEventAttrs, + customFlags?: SDKEventCustomFlags, + transactionAttributes?: TransactionAttributes, + eventOptions?: SDKEventOptions + ): void; + logPromotionEvent( + promotionType: valueof, + promotion: SDKPromotion, + attrs?: SDKEventAttrs, + customFlags?: SDKEventCustomFlags, + eventOptions?: SDKEventOptions + ): void; + logPurchaseEvent( + transactionAttributes: TransactionAttributes, + product: SDKProduct, + attrs?: SDKEventAttrs, + customFlags?: SDKEventCustomFlags + ): void; + logRefundEvent( + transactionAttributes: TransactionAttributes, + product: SDKProduct, + attrs?: SDKEventAttrs, + customFlags?: SDKEventCustomFlags + ): void; + startTracking(callback: Callback): void; + stopTracking(): void; +} diff --git a/src/events.js b/src/events.js index 4dfde7f93..73969b5df 100644 --- a/src/events.js +++ b/src/events.js @@ -438,10 +438,13 @@ export default function Events(mpInstance) { element = elements[i]; if (element.addEventListener) { + // Modern browsers element.addEventListener(domEvent, handler, false); } else if (element.attachEvent) { + // IE < 9 element.attachEvent('on' + domEvent, handler); } else { + // All other browsers element['on' + domEvent] = handler; } } diff --git a/src/forwarders.interfaces.ts b/src/forwarders.interfaces.ts index a38777c3a..db5016bbb 100644 --- a/src/forwarders.interfaces.ts +++ b/src/forwarders.interfaces.ts @@ -63,9 +63,6 @@ export interface ConfiguredKit setOptOut(isOptingOut: boolean): string | KitMappedMethodFailure; removeUserAttribute(key: string): string; setUserAttribute(key: string, value: string): string; - - // TODO: Convert type to enum during Identity migration - // https://go.mparticle.com/work/SQDSDKS-5218 setUserIdentity(id: UserIdentityId, type: UserIdentityType): void; // TODO: https://go.mparticle.com/work/SQDSDKS-5156 diff --git a/src/identity-user-interfaces.ts b/src/identity-user-interfaces.ts index 497e732a4..6e78c5f86 100644 --- a/src/identity-user-interfaces.ts +++ b/src/identity-user-interfaces.ts @@ -1,21 +1,21 @@ import { AllUserAttributes, MPID, Product, User } from '@mparticle/web-sdk'; import { SDKIdentityTypeEnum } from './identity.interfaces'; -import { MessageType } from './types.interfaces'; -import { BaseEvent } from './sdkRuntimeModels'; +import { MessageType } from './types'; +import { BaseEvent, SDKProduct } from './sdkRuntimeModels'; import Constants from './constants'; const { HTTPCodes } = Constants; // Cart is Deprecated and private to mParticle user in @mparticle/web-sdk // but we need to expose it here for type safety in some of our tests -interface Cart { +interface ICart { /** * @deprecated Cart persistence in mParticle has been deprecated. Please use mParticle.eCommerce.logProductAction(mParticle.ProductActionType.AddToCart, [products]) */ - add: (product: Product, logEventBoolean?: boolean) => void; + add: (product: SDKProduct, logEventBoolean?: boolean) => void; /** * @deprecated Cart persistence in mParticle has been deprecated. Please use mParticle.eCommerce.logProductAction(mParticle.ProductActionType.RemoveFromCart, [products]) */ - remove: (product: Product, logEventBoolean?: boolean) => void; + remove: (product: SDKProduct, logEventBoolean?: boolean) => void; /** * @deprecated Cart persistence in mParticle has been deprecated. */ @@ -23,7 +23,7 @@ interface Cart { /** * @deprecated Cart Products have been deprecated */ - getCartProducts: () => Product[]; + getCartProducts: () => SDKProduct[]; } // https://go.mparticle.com/work/SQDSDKS-5033 @@ -36,7 +36,7 @@ export interface IMParticleUser extends User { /* * @deprecated */ - getCart(): Cart; + getCart(): ICart; } export interface ISDKUserIdentity { @@ -58,7 +58,7 @@ export interface ISDKUserIdentityChangeData { } export interface IUserIdentityChangeEvent extends BaseEvent { - messageType: MessageType.UserIdentityChange; + messageType: typeof MessageType.UserIdentityChange; userIdentityChanges: ISDKUserIdentityChanges; } @@ -75,7 +75,7 @@ export interface ISDKUserAttributeChangeData { } export interface IUserAttributeChangeEvent extends BaseEvent { - messageType: MessageType.UserAttributeChange; + messageType: typeof MessageType.UserAttributeChange; userAttributeChanges: ISDKUserAttributeChangeData; } diff --git a/src/identity.js b/src/identity.js index 5f4ccf15d..686e574db 100644 --- a/src/identity.js +++ b/src/identity.js @@ -1,5 +1,5 @@ import Constants, { HTTP_OK } from './constants'; -import Types from './types'; +import Types, { IdentityType } from './types'; import { cacheOrClearIdCache, createKnownIdentities, @@ -17,7 +17,6 @@ import { isObject, } from './utils'; import { hasMPIDAndUserLoginChanged, hasMPIDChanged } from './user-utils'; -import { getNewIdentitiesByName } from './type-utils'; import { processReadyQueue } from './pre-init-utils'; export default function Identity(mpInstance) { @@ -1619,7 +1618,7 @@ export default function Identity(mpInstance) { self.setForwarderCallbacks(newUser, method); } - const newIdentitiesByName = getNewIdentitiesByName( + const newIdentitiesByName = IdentityType.getNewIdentitiesByName( newIdentitiesByType ); diff --git a/src/identityApiClient.interfaces.ts b/src/identityApiClient.interfaces.ts deleted file mode 100644 index bbea65855..000000000 --- a/src/identityApiClient.interfaces.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { IdentityApiData, MPID, UserIdentities } from '@mparticle/web-sdk'; -import { - IdentityCallback, - IIdentityResponse, -} from './identity-user-interfaces'; -import { - IAliasRequest, - IAliasCallback, - IIdentityRequest, - IdentityAPIMethod, - IIdentity, -} from './identity.interfaces'; -import { MParticleWebSDK } from './sdkRuntimeModels'; - -export interface IIdentityApiClient { - sendAliasRequest: ( - aliasRequest: IAliasRequest, - aliasCallback: IAliasCallback - ) => Promise; - sendIdentityRequest: ( - identityApiRequest: IIdentityRequest, - method: IdentityAPIMethod, - callback: IdentityCallback, - originalIdentityApiData: IdentityApiData, - parseIdentityResponse: IIdentity['parseIdentityResponse'], - mpid: MPID, - knownIdentities: UserIdentities - ) => Promise; - getUploadUrl: (method: IdentityAPIMethod, mpid: MPID) => string; - getIdentityResponseFromFetch: ( - response: Response, - responseBody: string - ) => IIdentityResponse; - getIdentityResponseFromXHR: (response: Response) => IIdentityResponse; -} - -// https://go.mparticle.com/work/SQDSDKS-6568 -// https://go.mparticle.com/work/SQDSDKS-6679 -// Combine with `sendIdentityRequest` above once module is fully migrated -export type IIdentityApiClientSendAliasRequest = ( - mpInstance: MParticleWebSDK, - aliasRequest: IAliasRequest, - aliasCallback: IAliasCallback -) => Promise; diff --git a/src/identityApiClient.js b/src/identityApiClient.js deleted file mode 100644 index a169050eb..000000000 --- a/src/identityApiClient.js +++ /dev/null @@ -1,131 +0,0 @@ -import Constants from './constants'; -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; - -const { Modify } = Constants.IdentityMethods; - -export default function IdentityAPIClient(mpInstance) { - this.sendAliasRequest = async function(aliasRequest, callback) { - await sendAliasRequest(mpInstance, aliasRequest, callback); - }; - - this.sendIdentityRequest = async function( - identityApiRequest, - method, - callback, - originalIdentityApiData, - parseIdentityResponse, - mpid, - knownIdentities - ) { - const { verbose, error } = mpInstance.Logger; - const { invokeCallback } = mpInstance._Helpers; - - verbose(Messages.InformationMessages.SendIdentityBegin); - if (!identityApiRequest) { - error(Messages.ErrorMessages.APIRequestEmpty); - return; - } - 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 = 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, - }); -} diff --git a/src/identityApiClient.ts b/src/identityApiClient.ts new file mode 100644 index 000000000..00ec9d07e --- /dev/null +++ b/src/identityApiClient.ts @@ -0,0 +1,351 @@ +import Constants, { HTTP_ACCEPTED, HTTP_BAD_REQUEST, HTTP_OK } from './constants'; +import { + AsyncUploader, + FetchUploader, + XHRUploader, + IFetchPayload, +} from './uploaders'; +import { CACHE_HEADER } from './identity-utils'; +import { parseNumber, valueof } from './utils'; +import { + IAliasCallback, + IAliasRequest, + IdentityAPIMethod, + IIdentity, + IIdentityAPIRequestData, +} from './identity.interfaces'; +import { + IdentityApiData, + MPID, + UserIdentities, +} from '@mparticle/web-sdk'; +import { + IdentityCallback, + IdentityResultBody, + IIdentityResponse, +} from './identity-user-interfaces'; +import { MParticleWebSDK } from './sdkRuntimeModels'; + +const { HTTPCodes, Messages, IdentityMethods } = Constants; + +const { Modify } = IdentityMethods; + +export interface IIdentityApiClient { + sendAliasRequest: ( + aliasRequest: IAliasRequest, + aliasCallback: IAliasCallback + ) => Promise; + sendIdentityRequest: ( + identityApiRequest: IIdentityAPIRequestData, + method: IdentityAPIMethod, + callback: IdentityCallback, + originalIdentityApiData: IdentityApiData, + parseIdentityResponse: IIdentity['parseIdentityResponse'], + mpid: MPID, + knownIdentities: UserIdentities + ) => 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: MParticleWebSDK +) { + this.sendAliasRequest = async function( + 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: 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) { + 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); + } + + } + + verbose(message); + invokeAliasCallback(aliasCallback, response.status, errorMessage); + } catch (e) { + const errorMessage = (e as Error).message || e.toString(); + error('Error sending alias request to mParticle servers. ' + errorMessage); + invokeAliasCallback( + aliasCallback, + HTTPCodes.noHttpCoverage, + errorMessage, + ); + } + }; + + this.sendIdentityRequest = async function( + identityApiRequest: IIdentityAPIRequestData, + method: IdentityAPIMethod, + callback: IdentityCallback, + originalIdentityApiData: IdentityApiData, + parseIdentityResponse: IIdentity['parseIdentityResponse'], + mpid: MPID, + knownIdentities: UserIdentities + ) { + const { verbose, error } = mpInstance.Logger; + const { invokeCallback } = mpInstance._Helpers; + + verbose(Messages.InformationMessages.SendIdentityBegin); + if (!identityApiRequest) { + error(Messages.ErrorMessages.APIRequestEmpty); + return; + } + 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 { + message = 'Received Identity Response from server: '; + message += JSON.stringify(identityResponse.responseText); + } + + 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 + default: { + throw new Error('Received HTTP Code of ' + response.status); + } + } + + mpInstance._Store.identityCallInFlight = false; + + verbose(message); + parseIdentityResponse( + identityResponse, + previousMPID, + callback, + originalIdentityApiData, + method, + knownIdentities, + false + ); + } catch (err) { + mpInstance._Store.identityCallInFlight = false; + + const errorMessage = (err as Error).message || err.toString(); + + error('Error sending identity request to servers' + ' - ' + errorMessage); + 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, + }); +} diff --git a/src/kitFilterHelper.ts b/src/kitFilterHelper.ts index 76dfbd1c1..0bbb44ba6 100644 --- a/src/kitFilterHelper.ts +++ b/src/kitFilterHelper.ts @@ -1,18 +1,17 @@ -import { generateHash } from "./utils"; +import { generateHash, valueof } from "./utils"; // TODO: https://mparticle-eng.atlassian.net/browse/SQDSDKS-5381 -import { EventTypeEnum, IdentityType } from "./types.interfaces"; -import Constants from './constants'; +import { EventType, IdentityType } from "./types"; export default class KitFilterHelper { - static hashEventType(eventType: EventTypeEnum): number { + static hashEventType(eventType: valueof): number { return generateHash(eventType as unknown as string); }; - static hashEventName(eventName: string, eventType: EventTypeEnum): number { + static hashEventName(eventName: string, eventType: valueof): number { return generateHash(eventType + eventName); }; - static hashEventAttributeKey(eventType: EventTypeEnum, eventName: string, customAttributeName: string): number { + static hashEventAttributeKey(eventType: valueof, eventName: string, customAttributeName: string): number { return generateHash(eventType + eventName + customAttributeName); } @@ -22,7 +21,7 @@ export default class KitFilterHelper { // User Identities are not actually hashed, this method is named this way to // be consistent with the filter class. UserIdentityType is also a number - static hashUserIdentity(userIdentity: IdentityType): IdentityType { + static hashUserIdentity(userIdentity: typeof IdentityType): typeof IdentityType { return userIdentity; } diff --git a/src/mp-instance.js b/src/mp-instance.js index b18158339..56ef1adcf 100644 --- a/src/mp-instance.js +++ b/src/mp-instance.js @@ -212,8 +212,8 @@ export default function mParticleInstance(instanceName) { */ this.getEnvironment = function() { return self._Store.SDKConfig.isDevelopmentMode - ? Types.Environment.Development - : Types.Environment.Production; + ? Constants.Environment.Development + : Constants.Environment.Production; }; /** * Returns the mParticle SDK version number @@ -824,7 +824,7 @@ export default function mParticleInstance(instanceName) { * @for mParticle.eCommerce * @method logCheckout * @param {Number} step checkout step number - * @param {String} option + * @param {String} checkout option string * @param {Object} attrs * @param {Object} [customFlags] Custom flags for the event * @deprecated diff --git a/src/sdkRuntimeModels.ts b/src/sdkRuntimeModels.ts index 2fd867122..092d82a2d 100644 --- a/src/sdkRuntimeModels.ts +++ b/src/sdkRuntimeModels.ts @@ -3,12 +3,13 @@ import { DataPlanVersion } from '@mparticle/data-planning-models'; import { MPConfiguration, MPID, - Callback, IdentityApiData, + SDKEventOptions, + SDKEventAttrs, } from '@mparticle/web-sdk'; import { IStore } from './store'; import Validators from './validators'; -import { Dictionary } from './utils'; +import { Dictionary, valueof } from './utils'; import { IServerModel } from './serverModel'; import { IKitConfigs } from './configAPIClient'; import { SDKConsentApi, SDKConsentState } from './consent'; @@ -29,10 +30,17 @@ import { IdentityCallback, ISDKUserAttributes, } from './identity-user-interfaces'; -import { IIdentityType } from './types.interfaces'; +import { + CommerceEventType, + EventType, + IdentityType, + PromotionActionType, +} from './types'; import IntegrationCapture from './integrationCapture'; import { INativeSdkHelpers } from './nativeSdkHelpers.interfaces'; import { ICookieSyncManager, IPixelConfiguration } from './cookieSyncManager'; +import { IEvents } from './events.interfaces'; +import { IECommerce, SDKECommerceAPI } from './ecommerce.interfaces'; // TODO: Resolve this with version in @mparticle/web-sdk export type SDKEventCustomFlags = Dictionary; @@ -100,6 +108,11 @@ export interface SDKPromotion { Position?: string; } +export interface SDKImpression { + Name: string; + Product: SDKProduct; +} + export interface SDKProductImpression { ProductImpressionList?: string; ProductList?: SDKProduct[]; @@ -143,18 +156,18 @@ export interface SDKProduct { Position?: number; CouponCode?: string; TotalAmount?: number; - Attributes?: { [key: string]: string }; -} -// Temporary Interfaces for Events Module -interface IEvents { - logEvent?(event: BaseEvent): void; + // https://go.mparticle.com/work/SQDSDKS-4801 + Attributes?: Record | null; } export interface MParticleWebSDK { addForwarder(mockForwarder: MPForwarder): void; _IntegrationCapture: IntegrationCapture; - IdentityType: IIdentityType; + IdentityType: valueof; + CommerceEventType: valueof; + EventType: valueof; + PromotionType: valueof; _Identity: IIdentity; Identity: SDKIdentityApi; Logger: SDKLoggerApi; @@ -165,6 +178,7 @@ export interface MParticleWebSDK { _Forwarders: any; _Helpers: SDKHelpersApi; _Events: IEvents; + _Ecommerce: IECommerce; config: SDKInitConfig; _ServerModel: IServerModel; _SessionManager: ISessionManager; @@ -189,7 +203,7 @@ export interface MParticleWebSDK { getDeviceId(): string; setDeviceId(deviceId: string): void; setSessionAttribute(key: string, value: string): void; - getInstance(): MParticleWebSDK; // TODO: Create a new type for MParticleWebSDKInstance + getInstance(instanceName?: string): MParticleWebSDK; // https://go.mparticle.com/work/SQDSDKS-4804 ServerModel(); upload(); setLogLevel(logLevel: LogLevelType): void; @@ -197,16 +211,18 @@ export interface MParticleWebSDK { startNewSession(): void; logEvent( eventName: string, - eventType?: number, - attrs?: { [key: string]: string }, - customFlags?: SDKEventCustomFlags + eventType?: valueof, + attrs?: SDKEventAttrs, + customFlags?: SDKEventCustomFlags, + eventOptions?: SDKEventOptions ): void; - logBaseEvent(event: any): void; - eCommerce: any; + logBaseEvent(event: BaseEvent, eventOptions?: SDKEventOptions): void; + eCommerce: SDKECommerceAPI; logLevel: string; ProductActionType: SDKProductActionType; generateHash(value: string): string; isIOS?: boolean; + sessionManager: Pick; // https://go.mparticle.com/work/SQDSDKS-6949 } // Used in cases where server requires booleans as strings @@ -285,10 +301,11 @@ export interface SDKHelpersApi { timeoutStart: number, now: number ): boolean; + isEventType?(type: valueof): boolean; isObject?(item: any); invokeCallback?( - callback: Callback, - code: string, + callback: IdentityCallback, + code: number, body: string, mParticleUser?: IMParticleUser, previousMpid?: MPID diff --git a/src/sdkToEventsApiConverter.ts b/src/sdkToEventsApiConverter.ts index f4349bdbc..ee95ba38f 100644 --- a/src/sdkToEventsApiConverter.ts +++ b/src/sdkToEventsApiConverter.ts @@ -373,7 +373,7 @@ export function convertProducts( price: sdkProduct.Price, quantity: sdkProduct.Quantity, coupon_code: sdkProduct.CouponCode, - custom_attributes: sdkProduct.Attributes, + custom_attributes: sdkProduct.Attributes as Record, }; products.push(product); } diff --git a/src/serverModel.ts b/src/serverModel.ts index 8136cf4fc..99e9dfc91 100644 --- a/src/serverModel.ts +++ b/src/serverModel.ts @@ -110,7 +110,7 @@ export interface IProductV2DTO { ps: number; cc: string | number; tpa: number; - attrs: Record | null; + attrs: Record | undefined; } export interface IPromotionV2DTO { diff --git a/src/sideloadedKit.ts b/src/sideloadedKit.ts index 3923b8867..5bc6d1e63 100644 --- a/src/sideloadedKit.ts +++ b/src/sideloadedKit.ts @@ -6,22 +6,23 @@ import { IKitFilterSettings, } from './configAPIClient'; import { UnregisteredKit } from './forwarders.interfaces'; -import { EventTypeEnum, IdentityType } from './types.interfaces'; +import { EventType, IdentityType } from './types'; +import { valueof } from './utils'; export interface IMPSideloadedKit { kitInstance: UnregisteredKit; filterDictionary: IKitFilterSettings; - addEventTypeFilter(eventType: EventTypeEnum): void; - addEventNameFilter(eventType: EventTypeEnum, eventName: string): void; + addEventTypeFilter(eventType: valueof): void; + addEventNameFilter(eventType: valueof, eventName: string): void; addEventAttributeFilter( - eventType: EventTypeEnum, + eventType: valueof, eventName: string, customAttributeKey: string ): void; addScreenNameFilter(screenName: string): void; addScreenAttributeFilter(screenName: string, screenAttribute: string): void; - addUserIdentityFilter(userIdentity: IdentityType): void; + addUserIdentityFilter(userIdentity: typeof IdentityType): void; addUserAttributeFilter(userAttributeKey: string): void; } @@ -57,13 +58,13 @@ export default class MPSideloadedKit implements IMPSideloadedKit{ this.kitInstance = unregisteredKitInstance; } - public addEventTypeFilter(eventType: EventTypeEnum): void { + public addEventTypeFilter(eventType: valueof): void { const hashedEventType = KitFilterHelper.hashEventType(eventType); this.filterDictionary.eventTypeFilters.push(hashedEventType); } public addEventNameFilter( - eventType: EventTypeEnum, + eventType: valueof, eventName: string ): void { const hashedEventName = KitFilterHelper.hashEventName( @@ -74,7 +75,7 @@ export default class MPSideloadedKit implements IMPSideloadedKit{ } public addEventAttributeFilter( - eventType: EventTypeEnum, + eventType: valueof, eventName: string, customAttributeKey: string ): void { @@ -89,7 +90,7 @@ export default class MPSideloadedKit implements IMPSideloadedKit{ public addScreenNameFilter(screenName: string): void { const hashedScreenName = KitFilterHelper.hashEventName( screenName, - EventTypeEnum.Unknown + EventType.Unknown, ); this.filterDictionary.screenNameFilters.push(hashedScreenName); } @@ -99,7 +100,7 @@ export default class MPSideloadedKit implements IMPSideloadedKit{ screenAttribute: string ): void { const hashedScreenAttribute = KitFilterHelper.hashEventAttributeKey( - EventTypeEnum.Unknown, + EventType.Unknown, screenName, screenAttribute ); @@ -108,7 +109,7 @@ export default class MPSideloadedKit implements IMPSideloadedKit{ ); } - public addUserIdentityFilter(userIdentity: IdentityType): void { + public addUserIdentityFilter(userIdentity: typeof IdentityType): void { const hashedIdentityType = KitFilterHelper.hashUserIdentity( userIdentity ); diff --git a/src/type-utils.ts b/src/type-utils.ts deleted file mode 100644 index 27d19499f..000000000 --- a/src/type-utils.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { IdentityType } from './types.interfaces'; -import { parseNumber } from './utils'; - -export interface IIdentitiesByType { - [key: number]: string; -} - -export function getNewIdentitiesByName(newIdentitiesByType: IIdentitiesByType) { - const newIdentitiesByName = {}; - - for (var key in newIdentitiesByType) { - const identityNameKey = getIdentityName(parseNumber(key)); - newIdentitiesByName[identityNameKey] = newIdentitiesByType[key]; - } - - return newIdentitiesByName; -} - -export function getIdentityName(identityType: IdentityType): string | null { - switch (identityType) { - case IdentityType.Other: - return 'other'; - case IdentityType.CustomerId: - return 'customerid'; - case IdentityType.Facebook: - return 'facebook'; - case IdentityType.Twitter: - return 'twitter'; - case IdentityType.Google: - return 'google'; - case IdentityType.Microsoft: - return 'microsoft'; - case IdentityType.Yahoo: - return 'yahoo'; - case IdentityType.Email: - return 'email'; - case IdentityType.FacebookCustomAudienceId: - return 'facebookcustomaudienceid'; - case IdentityType.Other2: - return 'other2'; - case IdentityType.Other3: - return 'other3'; - case IdentityType.Other4: - return 'other4'; - case IdentityType.Other5: - return 'other5'; - case IdentityType.Other6: - return 'other6'; - case IdentityType.Other7: - return 'other7'; - case IdentityType.Other8: - return 'other8'; - case IdentityType.Other9: - return 'other9'; - case IdentityType.Other10: - return 'other10'; - case IdentityType.MobileNumber: - return 'mobile_number'; - case IdentityType.PhoneNumber2: - return 'phone_number_2'; - case IdentityType.PhoneNumber3: - return 'phone_number_3'; - default: - return null; - } -} diff --git a/src/types.interfaces.ts b/src/types.interfaces.ts deleted file mode 100644 index 96ce14c52..000000000 --- a/src/types.interfaces.ts +++ /dev/null @@ -1,56 +0,0 @@ -export enum EventTypeEnum { - Unknown, - Navigation, - Location, - Search, - Transaction, - UserContent, - UserPreference, - Social, - Other, - Media, -} - -// TODO: https://mparticle-eng.atlassian.net/browse/SQDSDKS-5403 -export enum MessageType { - SessionStart = 1, - SessionEnd = 2, - PageView = 3, - PageEvent = 4, - CrashReport = 5, - OptOut = 6, - AppStateTransition = 10, - Profile = 14, - Commerce = 16, - UserAttributeChange = 17, - UserIdentityChange = 18, - Media = 20, -}; - -export enum IdentityType { - Other = 0, - CustomerId = 1, - Facebook = 2, - Twitter = 3, - Google = 4, - Microsoft = 5, - Yahoo = 6, - Email = 7, - FacebookCustomAudienceId = 9, - Other2 = 10, - Other3 = 11, - Other4 = 12, - Other5 = 13, - Other6 = 14, - Other7 = 15, - Other8 = 16, - Other9 = 17, - Other10 = 18, - MobileNumber = 19, - PhoneNumber2 = 20, - PhoneNumber3 = 21, -} - -export interface IIdentityType { - getIdentityType(identityType: string): IdentityType | null; -} diff --git a/src/types.js b/src/types.js deleted file mode 100644 index fcabf877f..000000000 --- a/src/types.js +++ /dev/null @@ -1,338 +0,0 @@ -import { getIdentityName } from './type-utils'; - -var MessageType = { - SessionStart: 1, - SessionEnd: 2, - PageView: 3, - PageEvent: 4, - CrashReport: 5, - OptOut: 6, - AppStateTransition: 10, - Profile: 14, - Commerce: 16, - Media: 20, - UserAttributeChange: 17, - UserIdentityChange: 18, -}; - -// Dictionary that contains MessageTypes that will -// trigger an immediate upload. -var TriggerUploadType = { - [MessageType.Commerce]: 1, - [MessageType.UserIdentityChange]: 1, -}; - -var EventType = { - Unknown: 0, - Navigation: 1, - Location: 2, - Search: 3, - Transaction: 4, - UserContent: 5, - UserPreference: 6, - Social: 7, - Other: 8, - Media: 9, - getName: function(id) { - switch (id) { - case EventType.Unknown: - return 'Unknown'; - case EventType.Navigation: - return 'Navigation'; - case EventType.Location: - return 'Location'; - case EventType.Search: - return 'Search'; - case EventType.Transaction: - return 'Transaction'; - case EventType.UserContent: - return 'User Content'; - case EventType.UserPreference: - return 'User Preference'; - case EventType.Social: - return 'Social'; - case CommerceEventType.ProductAddToCart: - return 'Product Added to Cart'; - case CommerceEventType.ProductAddToWishlist: - return 'Product Added to Wishlist'; - case CommerceEventType.ProductCheckout: - return 'Product Checkout'; - case CommerceEventType.ProductCheckoutOption: - return 'Product Checkout Options'; - case CommerceEventType.ProductClick: - return 'Product Click'; - case CommerceEventType.ProductImpression: - return 'Product Impression'; - case CommerceEventType.ProductPurchase: - return 'Product Purchased'; - case CommerceEventType.ProductRefund: - return 'Product Refunded'; - case CommerceEventType.ProductRemoveFromCart: - return 'Product Removed From Cart'; - case CommerceEventType.ProductRemoveFromWishlist: - return 'Product Removed from Wishlist'; - case CommerceEventType.ProductViewDetail: - return 'Product View Details'; - case CommerceEventType.PromotionClick: - return 'Promotion Click'; - case CommerceEventType.PromotionView: - return 'Promotion View'; - default: - return 'Other'; - } - }, -}; - -// Continuation of enum above, but in seperate object since we don't expose these to end user -var CommerceEventType = { - ProductAddToCart: 10, - ProductRemoveFromCart: 11, - ProductCheckout: 12, - ProductCheckoutOption: 13, - ProductClick: 14, - ProductViewDetail: 15, - ProductPurchase: 16, - ProductRefund: 17, - PromotionView: 18, - PromotionClick: 19, - ProductAddToWishlist: 20, - ProductRemoveFromWishlist: 21, - ProductImpression: 22, -}; - -var IdentityType = { - Other: 0, - CustomerId: 1, - Facebook: 2, - Twitter: 3, - Google: 4, - Microsoft: 5, - Yahoo: 6, - Email: 7, - FacebookCustomAudienceId: 9, - Other2: 10, - Other3: 11, - Other4: 12, - Other5: 13, - Other6: 14, - Other7: 15, - Other8: 16, - Other9: 17, - Other10: 18, - MobileNumber: 19, - PhoneNumber2: 20, - PhoneNumber3: 21, -}; - -IdentityType.isValid = function(identityType) { - if (typeof identityType === 'number') { - for (var prop in IdentityType) { - if (IdentityType.hasOwnProperty(prop)) { - if (IdentityType[prop] === identityType) { - return true; - } - } - } - } - - return false; -}; - -IdentityType.getName = function(identityType) { - switch (identityType) { - case window.mParticle.IdentityType.CustomerId: - return 'Customer ID'; - case window.mParticle.IdentityType.Facebook: - return 'Facebook ID'; - case window.mParticle.IdentityType.Twitter: - return 'Twitter ID'; - case window.mParticle.IdentityType.Google: - return 'Google ID'; - case window.mParticle.IdentityType.Microsoft: - return 'Microsoft ID'; - case window.mParticle.IdentityType.Yahoo: - return 'Yahoo ID'; - case window.mParticle.IdentityType.Email: - return 'Email'; - case window.mParticle.IdentityType.FacebookCustomAudienceId: - return 'Facebook App User ID'; - default: - return 'Other ID'; - } -}; - -IdentityType.getIdentityType = function(identityName) { - switch (identityName) { - case 'other': - return IdentityType.Other; - case 'customerid': - return IdentityType.CustomerId; - case 'facebook': - return IdentityType.Facebook; - case 'twitter': - return IdentityType.Twitter; - case 'google': - return IdentityType.Google; - case 'microsoft': - return IdentityType.Microsoft; - case 'yahoo': - return IdentityType.Yahoo; - case 'email': - return IdentityType.Email; - case 'facebookcustomaudienceid': - return IdentityType.FacebookCustomAudienceId; - case 'other2': - return IdentityType.Other2; - case 'other3': - return IdentityType.Other3; - case 'other4': - return IdentityType.Other4; - case 'other5': - return IdentityType.Other5; - case 'other6': - return IdentityType.Other6; - case 'other7': - return IdentityType.Other7; - case 'other8': - return IdentityType.Other8; - case 'other9': - return IdentityType.Other9; - case 'other10': - return IdentityType.Other10; - case 'mobile_number': - return IdentityType.MobileNumber; - case 'phone_number_2': - return IdentityType.PhoneNumber2; - case 'phone_number_3': - return IdentityType.PhoneNumber3; - default: - return false; - } -}; - -IdentityType.getIdentityName = function(identityType) { - return getIdentityName(identityType); -}; - -var ProductActionType = { - Unknown: 0, - AddToCart: 1, - RemoveFromCart: 2, - Checkout: 3, - CheckoutOption: 4, - Click: 5, - ViewDetail: 6, - Purchase: 7, - Refund: 8, - AddToWishlist: 9, - RemoveFromWishlist: 10, -}; - -ProductActionType.getName = function(id) { - switch (id) { - case ProductActionType.AddToCart: - return 'Add to Cart'; - case ProductActionType.RemoveFromCart: - return 'Remove from Cart'; - case ProductActionType.Checkout: - return 'Checkout'; - case ProductActionType.CheckoutOption: - return 'Checkout Option'; - case ProductActionType.Click: - return 'Click'; - case ProductActionType.ViewDetail: - return 'View Detail'; - case ProductActionType.Purchase: - return 'Purchase'; - case ProductActionType.Refund: - return 'Refund'; - case ProductActionType.AddToWishlist: - return 'Add to Wishlist'; - case ProductActionType.RemoveFromWishlist: - return 'Remove from Wishlist'; - default: - return 'Unknown'; - } -}; - -// these are the action names used by server and mobile SDKs when expanding a CommerceEvent -ProductActionType.getExpansionName = function(id) { - switch (id) { - case ProductActionType.AddToCart: - return 'add_to_cart'; - case ProductActionType.RemoveFromCart: - return 'remove_from_cart'; - case ProductActionType.Checkout: - return 'checkout'; - case ProductActionType.CheckoutOption: - return 'checkout_option'; - case ProductActionType.Click: - return 'click'; - case ProductActionType.ViewDetail: - return 'view_detail'; - case ProductActionType.Purchase: - return 'purchase'; - case ProductActionType.Refund: - return 'refund'; - case ProductActionType.AddToWishlist: - return 'add_to_wishlist'; - case ProductActionType.RemoveFromWishlist: - return 'remove_from_wishlist'; - default: - return 'unknown'; - } -}; - -var PromotionActionType = { - Unknown: 0, - PromotionView: 1, - PromotionClick: 2, -}; - -PromotionActionType.getName = function(id) { - switch (id) { - case PromotionActionType.PromotionView: - return 'view'; - case PromotionActionType.PromotionClick: - return 'click'; - default: - return 'unknown'; - } -}; - -// these are the names that the server and mobile SDKs use while expanding CommerceEvent -PromotionActionType.getExpansionName = function(id) { - switch (id) { - case PromotionActionType.PromotionView: - return 'view'; - case PromotionActionType.PromotionClick: - return 'click'; - default: - return 'unknown'; - } -}; - -var ProfileMessageType = { - Logout: 3, -}; -var ApplicationTransitionType = { - AppInit: 1, -}; - -const Environment = { - Production: 'production', - Development: 'development', -}; - -export default { - MessageType: MessageType, - EventType: EventType, - CommerceEventType: CommerceEventType, - IdentityType: IdentityType, - ProfileMessageType: ProfileMessageType, - ApplicationTransitionType: ApplicationTransitionType, - ProductActionType: ProductActionType, - PromotionActionType: PromotionActionType, - TriggerUploadType: TriggerUploadType, - Environment, -}; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 000000000..3ac8a5ad9 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,404 @@ +import Constants from './constants'; +import { isNumber, parseNumber, valueof } from './utils'; + +interface IdentitiesByType { + [key: number]: string; +} + +export const MessageType = { + SessionStart: 1 as const, + SessionEnd: 2 as const, + PageView: 3 as const, + PageEvent: 4 as const, + CrashReport: 5 as const, + OptOut: 6 as const, + AppStateTransition: 10 as const, + Profile: 14 as const, + Commerce: 16 as const, + Media: 20 as const, + UserAttributeChange: 17 as const, + UserIdentityChange: 18 as const, +}; + +export const EventType = { + Unknown: 0 as const, + Navigation: 1 as const, + Location: 2 as const, + Search: 3 as const, + Transaction: 4 as const, + UserContent: 5 as const, + UserPreference: 6 as const, + Social: 7 as const, + Other: 8 as const, + Media: 9 as const, + + getName(id: number): string { + switch (id) { + case EventType.Unknown: + return 'Unknown'; + case EventType.Navigation: + return 'Navigation'; + case EventType.Location: + return 'Location'; + case EventType.Search: + return 'Search'; + case EventType.Transaction: + return 'Transaction'; + case EventType.UserContent: + return 'User Content'; + case EventType.UserPreference: + return 'User Preference'; + case EventType.Social: + return 'Social'; + case CommerceEventType.ProductAddToCart: + return 'Product Added to Cart'; + case CommerceEventType.ProductAddToWishlist: + return 'Product Added to Wishlist'; + case CommerceEventType.ProductCheckout: + return 'Product Checkout'; + case CommerceEventType.ProductCheckoutOption: + return 'Product Checkout Options'; + case CommerceEventType.ProductClick: + return 'Product Click'; + case CommerceEventType.ProductImpression: + return 'Product Impression'; + case CommerceEventType.ProductPurchase: + return 'Product Purchased'; + case CommerceEventType.ProductRefund: + return 'Product Refunded'; + case CommerceEventType.ProductRemoveFromCart: + return 'Product Removed From Cart'; + case CommerceEventType.ProductRemoveFromWishlist: + return 'Product Removed from Wishlist'; + case CommerceEventType.ProductViewDetail: + return 'Product View Details'; + case CommerceEventType.PromotionClick: + return 'Promotion Click'; + case CommerceEventType.PromotionView: + return 'Promotion View'; + default: + return 'Other'; + } + }, +}; + +// Continuation of EventType enum above, but in seperate object since we don't expose these to end user +export const CommerceEventType = { + ProductAddToCart: 10 as const, + ProductRemoveFromCart: 11 as const, + ProductCheckout: 12 as const, + ProductCheckoutOption: 13 as const, + ProductClick: 14 as const, + ProductViewDetail: 15 as const, + ProductPurchase: 16 as const, + ProductRefund: 17 as const, + PromotionView: 18 as const, + PromotionClick: 19 as const, + ProductAddToWishlist: 20 as const, + ProductRemoveFromWishlist: 21 as const, + ProductImpression: 22 as const, +}; + +export const IdentityType = { + Other: 0 as const, + CustomerId: 1 as const, + Facebook: 2 as const, + Twitter: 3 as const, + Google: 4 as const, + Microsoft: 5 as const, + Yahoo: 6 as const, + Email: 7 as const, + FacebookCustomAudienceId: 9 as const, + Other2: 10 as const, + Other3: 11 as const, + Other4: 12 as const, + Other5: 13 as const, + Other6: 14 as const, + Other7: 15 as const, + Other8: 16 as const, + Other9: 17 as const, + Other10: 18 as const, + MobileNumber: 19 as const, + PhoneNumber2: 20 as const, + PhoneNumber3: 21 as const, + + isValid(identityType: number): boolean { + if (typeof identityType === 'number') { + for (const prop in IdentityType) { + if (IdentityType.hasOwnProperty(prop)) { + if (IdentityType[prop] === identityType) { + return true; + } + } + } + } + + return false; + }, + + getName: (identityType: number): string => { + switch (identityType) { + case window.mParticle.IdentityType.CustomerId: + return 'Customer ID'; + case window.mParticle.IdentityType.Facebook: + return 'Facebook ID'; + case window.mParticle.IdentityType.Twitter: + return 'Twitter ID'; + case window.mParticle.IdentityType.Google: + return 'Google ID'; + case window.mParticle.IdentityType.Microsoft: + return 'Microsoft ID'; + case window.mParticle.IdentityType.Yahoo: + return 'Yahoo ID'; + case window.mParticle.IdentityType.Email: + return 'Email'; + case window.mParticle.IdentityType.FacebookCustomAudienceId: + return 'Facebook App User ID'; + default: + return 'Other ID'; + } + }, + + getIdentityType: ( + identityName: string + ): valueof | boolean => { + switch (identityName) { + case 'other': + return IdentityType.Other; + case 'customerid': + return IdentityType.CustomerId; + case 'facebook': + return IdentityType.Facebook; + case 'twitter': + return IdentityType.Twitter; + case 'google': + return IdentityType.Google; + case 'microsoft': + return IdentityType.Microsoft; + case 'yahoo': + return IdentityType.Yahoo; + case 'email': + return IdentityType.Email; + case 'facebookcustomaudienceid': + return IdentityType.FacebookCustomAudienceId; + case 'other2': + return IdentityType.Other2; + case 'other3': + return IdentityType.Other3; + case 'other4': + return IdentityType.Other4; + case 'other5': + return IdentityType.Other5; + case 'other6': + return IdentityType.Other6; + case 'other7': + return IdentityType.Other7; + case 'other8': + return IdentityType.Other8; + case 'other9': + return IdentityType.Other9; + case 'other10': + return IdentityType.Other10; + case 'mobile_number': + return IdentityType.MobileNumber; + case 'phone_number_2': + return IdentityType.PhoneNumber2; + case 'phone_number_3': + return IdentityType.PhoneNumber3; + default: + return false; + } + }, + + getIdentityName: (identityType: number): string | null => { + switch (identityType) { + case IdentityType.Other: + return 'other'; + case IdentityType.CustomerId: + return 'customerid'; + case IdentityType.Facebook: + return 'facebook'; + case IdentityType.Twitter: + return 'twitter'; + case IdentityType.Google: + return 'google'; + case IdentityType.Microsoft: + return 'microsoft'; + case IdentityType.Yahoo: + return 'yahoo'; + case IdentityType.Email: + return 'email'; + case IdentityType.FacebookCustomAudienceId: + return 'facebookcustomaudienceid'; + case IdentityType.Other2: + return 'other2'; + case IdentityType.Other3: + return 'other3'; + case IdentityType.Other4: + return 'other4'; + case IdentityType.Other5: + return 'other5'; + case IdentityType.Other6: + return 'other6'; + case IdentityType.Other7: + return 'other7'; + case IdentityType.Other8: + return 'other8'; + case IdentityType.Other9: + return 'other9'; + case IdentityType.Other10: + return 'other10'; + case IdentityType.MobileNumber: + return 'mobile_number'; + case IdentityType.PhoneNumber2: + return 'phone_number_2'; + case IdentityType.PhoneNumber3: + return 'phone_number_3'; + default: + return null; + } + }, + + // Strips out functions from Identity Types for easier lookups + getValuesAsStrings: (): string[] => + Object.values(IdentityType) + .map(value => (isNumber(value) ? value.toString() : undefined)) + .filter(value => value !== undefined) as string[], + + getNewIdentitiesByName: ( + newIdentitiesByType: IdentitiesByType + ): IdentitiesByType => { + const newIdentitiesByName: IdentitiesByType = {}; + const identityTypeValuesAsStrings: string[] = IdentityType.getValuesAsStrings(); + + for (const key in newIdentitiesByType) { + // IdentityTypes are stored as numbers but are passed in as strings + if (identityTypeValuesAsStrings.includes(key)) { + const identityNameKey = IdentityType.getIdentityName( + parseNumber(key) + ); + newIdentitiesByName[identityNameKey] = newIdentitiesByType[key]; + } + } + + return newIdentitiesByName; + }, +}; + +export const ProductActionType = { + Unknown: 0 as const, + AddToCart: 1 as const, + RemoveFromCart: 2 as const, + Checkout: 3 as const, + CheckoutOption: 4 as const, + Click: 5 as const, + ViewDetail: 6 as const, + Purchase: 7 as const, + Refund: 8 as const, + AddToWishlist: 9 as const, + RemoveFromWishlist: 10 as const, + + getName: (id: number): string => { + switch (id) { + case ProductActionType.AddToCart: + return 'Add to Cart'; + case ProductActionType.RemoveFromCart: + return 'Remove from Cart'; + case ProductActionType.Checkout: + return 'Checkout'; + case ProductActionType.CheckoutOption: + return 'Checkout Option'; + case ProductActionType.Click: + return 'Click'; + case ProductActionType.ViewDetail: + return 'View Detail'; + case ProductActionType.Purchase: + return 'Purchase'; + case ProductActionType.Refund: + return 'Refund'; + case ProductActionType.AddToWishlist: + return 'Add to Wishlist'; + case ProductActionType.RemoveFromWishlist: + return 'Remove from Wishlist'; + default: + return 'Unknown'; + } + }, + + // these are the action names used by server and mobile SDKs when expanding a CommerceEvent + getExpansionName: (id: number) => { + switch (id) { + case ProductActionType.AddToCart: + return 'add_to_cart'; + case ProductActionType.RemoveFromCart: + return 'remove_from_cart'; + case ProductActionType.Checkout: + return 'checkout'; + case ProductActionType.CheckoutOption: + return 'checkout_option'; + case ProductActionType.Click: + return 'click'; + case ProductActionType.ViewDetail: + return 'view_detail'; + case ProductActionType.Purchase: + return 'purchase'; + case ProductActionType.Refund: + return 'refund'; + case ProductActionType.AddToWishlist: + return 'add_to_wishlist'; + case ProductActionType.RemoveFromWishlist: + return 'remove_from_wishlist'; + default: + return 'unknown'; + } + }, +}; + +export const PromotionActionType = { + Unknown: 0 as const, + PromotionView: 1 as const, + PromotionClick: 2 as const, + + getName: (id: number): string => { + switch (id) { + case PromotionActionType.PromotionView: + return 'view'; + case PromotionActionType.PromotionClick: + return 'click'; + default: + return 'unknown'; + } + }, + + // these are the names that the server and mobile SDKs use while expanding CommerceEvent + getExpansionName: (id: number): string => { + switch (id) { + case PromotionActionType.PromotionView: + return 'view'; + case PromotionActionType.PromotionClick: + return 'click'; + default: + return 'unknown'; + } + }, +}; + +export const ProfileMessageType = { + Logout: 3 as const, +}; + +export const ApplicationTransitionType = { + AppInit: 1 as const, +}; + +export default { + MessageType, + EventType, + CommerceEventType, + IdentityType, + ProfileMessageType, + ApplicationTransitionType, + ProductActionType, + PromotionActionType, + Environment: Constants.Environment, +} as const; diff --git a/src/uploaders.ts b/src/uploaders.ts index a075596e2..a7f01ea6a 100644 --- a/src/uploaders.ts +++ b/src/uploaders.ts @@ -1,6 +1,6 @@ type HTTPMethod = 'get' | 'post'; -export interface fetchPayload { +export interface IFetchPayload { method: string; headers: { Accept: string; @@ -12,7 +12,7 @@ export interface fetchPayload { export abstract class AsyncUploader { url: string; public abstract upload( - fetchPayload: fetchPayload, + fetchPayload: IFetchPayload, url?: string ): Promise; @@ -23,7 +23,7 @@ export abstract class AsyncUploader { export class FetchUploader extends AsyncUploader { public async upload( - fetchPayload: fetchPayload, + fetchPayload: IFetchPayload, _url?: string ): Promise { const url = _url || this.url; @@ -32,7 +32,7 @@ export class FetchUploader extends AsyncUploader { } export class XHRUploader extends AsyncUploader { - public async upload(fetchPayload: fetchPayload): Promise { + public async upload(fetchPayload: IFetchPayload): Promise { const response: Response = await this.makeRequest( this.url, fetchPayload.body, diff --git a/src/utils.ts b/src/utils.ts index 37f286bfe..23e53aacf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,7 +8,7 @@ type valueof = T[keyof T]; // Placeholder for Dictionary-like Types export type Dictionary = Record; -export type Environment = 'development' | 'production'; +export type Environment = valueof; const createCookieString = (value: string): string => replaceCommasWithPipes(replaceQuotesWithApostrophes(value)); @@ -180,7 +180,7 @@ const replaceMPID = (value: string, mpid: MPID): string => value.replace('%%mpid const replaceAmpWithAmpersand = (value: string): string => value.replace(/&/g, '&'); -const createCookieSyncUrl = ( +const createInitialCookieSyncUrl = ( mpid: MPID, pixelUrl: string, redirectUrl?: string @@ -358,7 +358,7 @@ const getHref = (): string => { export { createCookieString, revertCookieString, - createCookieSyncUrl, + createInitialCookieSyncUrl, valueof, converted, decoded, diff --git a/test/jest/cookieSyncManager.spec.ts b/test/jest/cookieSyncManager.spec.ts index f8bbaa0d5..0a50c7c52 100644 --- a/test/jest/cookieSyncManager.spec.ts +++ b/test/jest/cookieSyncManager.spec.ts @@ -2,7 +2,9 @@ import CookieSyncManager, { DAYS_IN_MILLISECONDS, IPixelConfiguration, CookieSyncDates, - isLastSyncDateExpired + isLastSyncDateExpired, + isTcfApiAvailable, + appendGdprConsentUrl } from '../../src/cookieSyncManager'; import { MParticleWebSDK } from '../../src/sdkRuntimeModels'; import { testMPID } from '../src/config/constants'; @@ -390,7 +392,8 @@ describe('CookieSyncManager', () => { filteringConsentRuleValues: { values: ['test'], }, - } as unknown as IPixelConfiguration; const loggerSpy = jest.fn(); + } as unknown as IPixelConfiguration; + const loggerSpy = jest.fn(); const mockMPInstance = ({ _Store: { @@ -553,4 +556,109 @@ describe('CookieSyncManager', () => { expect(isLastSyncDateExpired(frequencyCap, lastSyncDate)).toBe(false); }); }); -}); + + describe('#isTcfApiAvailable', () => { + it('should return true if window.__tcfapi exists on the page', () => { + window.__tcfapi = jest.fn(); + expect(isTcfApiAvailable()).toBe(true) + }); + }); + + describe('#appendGdprConsentUrl', () => { + const mockUrl = 'https://example.com/cookie-sync'; + + beforeEach(() => { + global.window.__tcfapi = jest.fn(); + }) + + afterEach(() => { + jest.clearAllMocks(); + }) + + it('should append GDPR parameters to the URL if __tcfapi callback succeeds', async () => { + // Mock __tcfapi to call the callback with success + (window.__tcfapi as jest.Mock).mockImplementation(( + command, + version, + callback + ) => { + expect(command).toBe('getInAppTCData'); + expect(version).toBe(2); + // Simulate a successful response + callback( + { gdprApplies: true, tcString: 'test-consent-string' }, + true + ); + }); + + const fullUrl = await appendGdprConsentUrl(mockUrl); + + expect(fullUrl).toBe( + `${mockUrl}&gdpr=1&gdpr_consent=test-consent-string`, + ); + }); + + it('should return only the base url if the __tcfapi callback fails to get tcData', async () => { + // Mock __tcfapi to call the callback with failure + (window.__tcfapi as jest.Mock).mockImplementation((command, version, callback) => { + callback(null, false); // Simulate a failure + }); + + const fullUrl = await appendGdprConsentUrl(mockUrl); + // Assert that the fallback method was called with the original URL + expect(fullUrl).toBe(mockUrl); + }); + + it('should handle exceptions thrown by __tcfapi gracefully', async () => { + // Mock __tcfapi to throw an error + (window.__tcfapi as jest.Mock).mockImplementation(() => { + throw new Error('Test Error'); + }); + + await expect(appendGdprConsentUrl(mockUrl)).rejects.toThrow('Test Error'); + }); + + describe('#processPixelConfig', () => { + it('should handle errors properly when calling attemptCookieSync and the callback fails', async () => { + (window.__tcfapi as jest.Mock).mockImplementation(() => { + throw new Error('Test Error'); + }); + const mpInstance = ({ + _Store: { + webviewBridgeEnabled: false, + pixelConfigurations: [pixelSettings], + }, + _Consent: { + isEnabledForUserConsent: jest.fn().mockReturnValue(true), + }, + Identity: { + getCurrentUser: jest.fn().mockReturnValue({ + getMPID: () => testMPID, + }), + }, + Logger: { + verbose: jest.fn(), + error: jest.fn(), + }, + } as unknown) as MParticleWebSDK; + + const cookieSyncManager = new CookieSyncManager(mpInstance); + cookieSyncManager.performCookieSync = jest.fn(); + + await cookieSyncManager.processPixelConfig( + pixelSettings, + {}, + testMPID, + true, + ); + + expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( + pixelUrlAndRedirectUrl, + '5', + testMPID, + {}, + ); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/jest/identity.spec.ts b/test/jest/identity.spec.ts index 15fa07ec6..210cc5866 100644 --- a/test/jest/identity.spec.ts +++ b/test/jest/identity.spec.ts @@ -14,7 +14,7 @@ import { SDKIdentityTypeEnum, IIdentityAPIIdentityChangeData, } from '../../src/identity.interfaces'; -import { MessageType } from '../../src/types.interfaces'; +import { MessageType } from '../../src/types'; describe('Identity', () => { describe('#types', () => { diff --git a/test/jest/kitFilterHelper.spec.ts b/test/jest/kitFilterHelper.spec.ts index e4a5724b5..ddf505fc7 100644 --- a/test/jest/kitFilterHelper.spec.ts +++ b/test/jest/kitFilterHelper.spec.ts @@ -1,5 +1,5 @@ import KitFilterHelper from "../../src/kitFilterHelper"; -import { EventTypeEnum, IdentityType } from "../../src/types.interfaces"; +import { EventType, IdentityType } from "../../src/types"; import Constants from '../../src/constants'; const { CCPAPurpose } = Constants; @@ -7,41 +7,41 @@ const { CCPAPurpose } = Constants; describe('FilterHashingUtilities', () => { describe('#hashEventType', () => { it('should hash event type Unknown', () => { - const eventTypeUnknownHash = KitFilterHelper.hashEventType(EventTypeEnum.Unknown); + const eventTypeUnknownHash = KitFilterHelper.hashEventType(EventType.Unknown); const expectedUnknownHash = 48; expect(eventTypeUnknownHash).toBe(expectedUnknownHash); }); it('should hash event type Navigation', () => { - const eventTypeNavigationHash = KitFilterHelper.hashEventType(EventTypeEnum.Navigation); + const eventTypeNavigationHash = KitFilterHelper.hashEventType(EventType.Navigation); const expectedNavigationHash = 49; expect(eventTypeNavigationHash).toBe(expectedNavigationHash); }); it('should hash event type Location', () => { - const eventTypeLocationHash = KitFilterHelper.hashEventType(EventTypeEnum.Location); + const eventTypeLocationHash = KitFilterHelper.hashEventType(EventType.Location); const expectedLocationHash = 50; expect(eventTypeLocationHash).toBe(expectedLocationHash); }); it('should hash event type Search', () => { - const eventTypeSearchHash = KitFilterHelper.hashEventType(EventTypeEnum.Search); + const eventTypeSearchHash = KitFilterHelper.hashEventType(EventType.Search); const expectedSearchHash = 51; expect(eventTypeSearchHash).toBe(expectedSearchHash); }); it('should hash event type Transaction', () => { - const eventTypeTransactionHash = KitFilterHelper.hashEventType(EventTypeEnum.Transaction); + const eventTypeTransactionHash = KitFilterHelper.hashEventType(EventType.Transaction); const expectedTransactionHash = 52; expect(eventTypeTransactionHash).toBe(expectedTransactionHash); }); it('should hash event type UserContent', () => { - const eventTypeUserContentHash = KitFilterHelper.hashEventType(EventTypeEnum.UserContent); + const eventTypeUserContentHash = KitFilterHelper.hashEventType(EventType.UserContent); const expectedUserContentHash = 53; expect(eventTypeUserContentHash).toBe(expectedUserContentHash); @@ -49,7 +49,7 @@ describe('FilterHashingUtilities', () => { }); it('should hash event type UserPreference', () => { - const eventTypeUserPreferenceHash = KitFilterHelper.hashEventType(EventTypeEnum.UserPreference); + const eventTypeUserPreferenceHash = KitFilterHelper.hashEventType(EventType.UserPreference); const expectedUserPreferenceHash = 54; expect(eventTypeUserPreferenceHash).toBe(expectedUserPreferenceHash); @@ -57,7 +57,7 @@ describe('FilterHashingUtilities', () => { }); it('should hash event type Social', () => { - const eventTypeSocialHash = KitFilterHelper.hashEventType(EventTypeEnum.Social); + const eventTypeSocialHash = KitFilterHelper.hashEventType(EventType.Social); const expectedSocialHash = 55; expect(eventTypeSocialHash).toBe(expectedSocialHash); @@ -65,7 +65,7 @@ describe('FilterHashingUtilities', () => { }); it('should hash event type Other', () => { - const eventTypeOtherHash = KitFilterHelper.hashEventType(EventTypeEnum.Other); + const eventTypeOtherHash = KitFilterHelper.hashEventType(EventType.Other); const expectedOtherHash = 56; expect(eventTypeOtherHash).toBe(expectedOtherHash); @@ -73,7 +73,7 @@ describe('FilterHashingUtilities', () => { }); it('should hash event type Media', () => { - const eventTypeMediaHash = KitFilterHelper.hashEventType(EventTypeEnum.Media); + const eventTypeMediaHash = KitFilterHelper.hashEventType(EventType.Media); const expectedMediaHash = 57; expect(eventTypeMediaHash).toBe(expectedMediaHash); @@ -85,14 +85,14 @@ describe('FilterHashingUtilities', () => { const eventName = 'foo-event-name'; it('should hash event name with event type Unknown', () => { - const eventTypeUnknownHash = KitFilterHelper.hashEventName(eventName, EventTypeEnum.Unknown); + const eventTypeUnknownHash = KitFilterHelper.hashEventName(eventName, EventType.Unknown); const expectedUnknownHash = -59445899; expect(eventTypeUnknownHash).toBe(expectedUnknownHash); }); it('should hash event name with event type Navigation', () => { - const eventTypeNavigationHash = KitFilterHelper.hashEventName(eventName, EventTypeEnum.Navigation); + const eventTypeNavigationHash = KitFilterHelper.hashEventName(eventName, EventType.Navigation); const expectedNavigationHash = 1448105910; @@ -100,7 +100,7 @@ describe('FilterHashingUtilities', () => { }); it('should hash event name with event type Location', () => { - const eventTypeLocationHash = KitFilterHelper.hashEventName(eventName, EventTypeEnum.Location); + const eventTypeLocationHash = KitFilterHelper.hashEventName(eventName, EventType.Location); const expectedLocationHash = -1339309577; @@ -108,7 +108,7 @@ describe('FilterHashingUtilities', () => { }); it('should hash event name with event type Search', () => { - const eventTypeSearchHash = KitFilterHelper.hashEventName(eventName, EventTypeEnum.Search); + const eventTypeSearchHash = KitFilterHelper.hashEventName(eventName, EventType.Search); const expectedSearchHash = 168242232; @@ -116,7 +116,7 @@ describe('FilterHashingUtilities', () => { }); it('should hash event name with event type Transaction', () => { - const eventTypeTransactionHash = KitFilterHelper.hashEventName(eventName, EventTypeEnum.Transaction); + const eventTypeTransactionHash = KitFilterHelper.hashEventName(eventName, EventType.Transaction); const expectedTransactionHash = 1675794041; @@ -124,7 +124,7 @@ describe('FilterHashingUtilities', () => { }); it('should hash event name with event type UserContent', () => { - const eventTypeUserContentHash = KitFilterHelper.hashEventName(eventName, EventTypeEnum.UserContent); + const eventTypeUserContentHash = KitFilterHelper.hashEventName(eventName, EventType.UserContent); const expectedUserContentHash = -1111621446; @@ -133,7 +133,7 @@ describe('FilterHashingUtilities', () => { }); it('should hash event name with event type UserPreference', () => { - const eventTypeUserPreferenceHash = KitFilterHelper.hashEventName(eventName, EventTypeEnum.UserPreference); + const eventTypeUserPreferenceHash = KitFilterHelper.hashEventName(eventName, EventType.UserPreference); const expectedUserPreferenceHash = 395930363; @@ -142,7 +142,7 @@ describe('FilterHashingUtilities', () => { }); it('should hash event name with event type Social', () => { - const eventTypeSocialHash = KitFilterHelper.hashEventName(eventName, EventTypeEnum.Social); + const eventTypeSocialHash = KitFilterHelper.hashEventName(eventName, EventType.Social); const expectedSocialHash = 1903482172; @@ -151,7 +151,7 @@ describe('FilterHashingUtilities', () => { }); it('should hash event name with event type Other', () => { - const eventTypeOtherHash = KitFilterHelper.hashEventName(eventName, EventTypeEnum.Other); + const eventTypeOtherHash = KitFilterHelper.hashEventName(eventName, EventType.Other); const expectedOtherHash = -883933315; @@ -160,7 +160,7 @@ describe('FilterHashingUtilities', () => { }); it('should hash event name with event type Media', () => { - const eventTypeMediaHash = KitFilterHelper.hashEventName(eventName, EventTypeEnum.Media); + const eventTypeMediaHash = KitFilterHelper.hashEventName(eventName, EventType.Media); const expectedMediaHash = 623618494; expect(eventTypeMediaHash).toBe(expectedMediaHash); @@ -170,7 +170,7 @@ describe('FilterHashingUtilities', () => { describe('#hashEventAttributeKey', () => { it('should hash event attribute key', () => { - const eventType:EventTypeEnum = EventTypeEnum.Navigation; + const eventType = EventType.Navigation; const eventName :string = 'foo-event-name'; const customAttributeName: string = 'event-attribute-key'; diff --git a/test/jest/sideloadedKit.spec.ts b/test/jest/sideloadedKit.spec.ts index 5086c70db..a846a3c6f 100644 --- a/test/jest/sideloadedKit.spec.ts +++ b/test/jest/sideloadedKit.spec.ts @@ -1,9 +1,8 @@ import MPSideloadedKit, { IMPSideloadedKit } from "../../src/sideloadedKit"; import { IMPSideloadedKitConstructor } from "../../src/sideloadedKit"; -import { EventTypeEnum, IdentityType } from "../../src/types.interfaces"; +import { EventType, IdentityType } from "../../src/types"; import { UnregisteredKit } from '../../src/forwarders.interfaces'; import { IKitFilterSettings } from '../../src/configAPIClient'; -import { mParticle } from "../src/config/constants"; const mockKitInstance: UnregisteredKit = { register: function() {} @@ -21,7 +20,7 @@ describe('MPSideloadedKit', () => { describe('#addEventTypeFilter', () => { it('should add a hashed event type to eventTypeFilters', () => { const expectedResult = [48]; - mpSideloadedKit.addEventTypeFilter(EventTypeEnum.Unknown); + mpSideloadedKit.addEventTypeFilter(EventType.Unknown); expect(filterDictionary.eventTypeFilters).toEqual(expectedResult); }); }); @@ -30,14 +29,14 @@ describe('MPSideloadedKit', () => { it('should add a hashed event name to eventNameFilters', () => { const eventName = 'foo-event-name'; const expectedResult = [-59445899]; - mpSideloadedKit.addEventNameFilter(EventTypeEnum.Unknown, eventName); + mpSideloadedKit.addEventNameFilter(EventType.Unknown, eventName); expect(filterDictionary.eventNameFilters).toEqual(expectedResult); }); }); describe('#addEventAttributeFilter', () => { it('should add a hashed event attribute to attributeFilters', () => { - const eventType: EventTypeEnum = EventTypeEnum.Navigation; + const eventType = EventType.Navigation; const eventName: string = 'foo-event-name'; const customAttributeName: string = 'event-attribute-key'; diff --git a/test/jest/type-utils.spec.ts b/test/jest/type-utils.spec.ts deleted file mode 100644 index 0152603e2..000000000 --- a/test/jest/type-utils.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getNewIdentitiesByName } from '../../src/type-utils'; -import { IdentityType } from '../../src/types.interfaces'; - -describe('getNewIdentitesByName', () => { - it('returns an identity name when passing an identity type', () => { - const { Email, CustomerId } = IdentityType; - - const newIdentitiesByType = { - [CustomerId]: 'foo', - [Email]: 'bar@gmail.com', - }; - - expect(getNewIdentitiesByName(newIdentitiesByType)).toEqual({ - customerid: 'foo', - email: 'bar@gmail.com', - }); - }); -}); diff --git a/test/jest/types.spec.ts b/test/jest/types.spec.ts new file mode 100644 index 000000000..4cefcd21d --- /dev/null +++ b/test/jest/types.spec.ts @@ -0,0 +1,604 @@ +import { + ApplicationTransitionType, + CommerceEventType, + EventType, + IdentityType, + MessageType, + ProductActionType, + ProfileMessageType, + PromotionActionType, +} from '../../src/types'; + + + +describe('MessageType', () => { + it('returns a message type', () => { + const { + SessionStart, + SessionEnd, + PageView, + PageEvent, + CrashReport, + AppStateTransition, + Profile, + Commerce, + UserAttributeChange, + UserIdentityChange, + } = MessageType; + + expect(SessionStart).toEqual(1); + expect(SessionEnd).toEqual(2); + expect(PageView).toEqual(3); + expect(PageEvent).toEqual(4); + expect(CrashReport).toEqual(5); + expect(AppStateTransition).toEqual(10); + expect(Profile).toEqual(14); + expect(Commerce).toEqual(16); + expect(UserAttributeChange).toEqual(17); + expect(UserIdentityChange).toEqual(18); + }); +}); + +describe('EventType', () => { + const { + Unknown, + Navigation, + Location, + Search, + Transaction, + UserContent, + UserPreference, + Social, + Other, + Media, + } = EventType; + + it('returns an event type', () => { + expect(Unknown).toEqual(0); + expect(Navigation).toEqual(1); + expect(Location).toEqual(2); + expect(Search).toEqual(3); + expect(Transaction).toEqual(4); + expect(UserContent).toEqual(5); + expect(UserPreference).toEqual(6); + expect(Social).toEqual(7); + expect(Other).toEqual(8); + expect(Media).toEqual(9); + }); + + describe('#getName', () => { + it('returns the name of an Event Type', () => { + const { getName } = EventType; + + expect(getName(Unknown)).toBe('Unknown'); + expect(getName(Navigation)).toBe('Navigation'); + expect(getName(Location)).toBe('Location'); + expect(getName(Search)).toBe('Search'); + expect(getName(Transaction)).toBe('Transaction'); + expect(getName(UserContent)).toBe('User Content'); + expect(getName(UserPreference)).toBe('User Preference'); + expect(getName(Social)).toBe('Social'); + expect(getName(Other)).toBe('Other'); + expect(getName(Media)).toBe('Other'); + }); + + it('returns the name of a Commerce Event Type', () => { + const { getName } = EventType; + + const { + ProductAddToCart, + ProductRemoveFromCart, + ProductCheckout, + ProductCheckoutOption, + ProductClick, + ProductViewDetail, + ProductPurchase, + ProductRefund, + PromotionView, + PromotionClick, + ProductAddToWishlist, + ProductRemoveFromWishlist, + ProductImpression, + } = CommerceEventType; + + + expect(EventType.getName(ProductAddToCart)).toBe( + 'Product Added to Cart' + ); + expect(getName(ProductRemoveFromCart)).toBe( + 'Product Removed From Cart' + ); + expect(getName(ProductCheckout)).toBe( + 'Product Checkout' + ); + expect(getName(ProductCheckoutOption)).toBe( + 'Product Checkout Options' + ); + expect(getName(ProductClick)).toBe('Product Click'); + expect(getName(ProductViewDetail)).toBe( + 'Product View Details' + ); + expect(getName(ProductPurchase)).toBe( + 'Product Purchased' + ); + expect(getName(ProductRefund)).toBe('Product Refunded'); + expect(getName(PromotionView)).toBe('Promotion View'); + expect(getName(PromotionClick)).toBe('Promotion Click'); + expect(getName(ProductAddToWishlist)).toBe( + 'Product Added to Wishlist' + ); + expect(getName(ProductRemoveFromWishlist)).toBe( + 'Product Removed from Wishlist' + ); + expect(getName(ProductImpression)).toBe('Product Impression'); + }); + + + it('returns other if the event type is not found', () => { + const { getName } = EventType; + + expect(getName('foo' as unknown as number)).toBe('Other'); + }); + + it('returns other if the commerce event type is not found', () => { + const { getName } = EventType; + + expect(getName(NaN)).toBe('Other'); + }); + }); +}); + +describe('CommerceEventType', () => { + const { + ProductAddToCart, + ProductRemoveFromCart, + ProductCheckout, + ProductCheckoutOption, + ProductClick, + ProductViewDetail, + ProductPurchase, + ProductRefund, + PromotionView, + PromotionClick, + ProductAddToWishlist, + ProductRemoveFromWishlist, + ProductImpression, + } = CommerceEventType; + + it('returns a commerce event type', () => { + expect(ProductAddToCart).toEqual(10); + expect(ProductRemoveFromCart).toEqual(11); + expect(ProductCheckout).toEqual(12); + expect(ProductCheckoutOption).toEqual(13); + expect(ProductClick).toEqual(14); + expect(ProductViewDetail).toEqual(15); + expect(ProductPurchase).toEqual(16); + expect(ProductRefund).toEqual(17); + expect(PromotionView).toEqual(18); + expect(PromotionClick).toEqual(19); + expect(ProductAddToWishlist).toEqual(20); + expect(ProductRemoveFromWishlist).toEqual(21); + expect(ProductImpression).toEqual(22); + }); +}); + +describe('IdentityType', () => { + const { + Other, + CustomerId, + Facebook, + Twitter, + Google, + Microsoft, + Yahoo, + Email, + FacebookCustomAudienceId, + Other2, + Other3, + Other4, + Other5, + Other6, + Other7, + Other8, + Other9, + Other10, + MobileNumber, + PhoneNumber2, + PhoneNumber3, + } = IdentityType; + + it('returns an identity type', () => { + expect(Other).toEqual(0); + expect(CustomerId).toEqual(1); + expect(Facebook).toEqual(2); + expect(Twitter).toEqual(3); + expect(Google).toEqual(4); + expect(Microsoft).toEqual(5); + expect(Yahoo).toEqual(6); + expect(Email).toEqual(7); + + // 8 was used for `Alias` but this is now handled by Identity directly + // and is no longer sent as an IdentityType + + expect(FacebookCustomAudienceId).toEqual(9); + expect(Other2).toEqual(10); + expect(Other3).toEqual(11); + expect(Other4).toEqual(12); + expect(Other5).toEqual(13); + expect(Other6).toEqual(14); + expect(Other7).toEqual(15); + expect(Other8).toEqual(16); + expect(Other9).toEqual(17); + expect(Other10).toEqual(18); + expect(MobileNumber).toEqual(19); + expect(PhoneNumber2).toEqual(20); + expect(PhoneNumber3).toEqual(21); + }); + + describe('#getValuesAsStrings', () => { + it('returns an array of identity types values as an array of numbers as strings', () => { + const { getValuesAsStrings } = IdentityType; + + expect(getValuesAsStrings()).toEqual([ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '19', + '20', + '21', + ]); + }); + }); + + describe('#isValid', () => { + it('returns true if the identity type is valid', () => { + const { isValid } = IdentityType; + + expect(isValid(Other)).toBe(true); + expect(isValid(CustomerId)).toBe(true); + expect(isValid(Facebook)).toBe(true); + expect(isValid(Twitter)).toBe(true); + expect(isValid(Google)).toBe(true); + expect(isValid(Microsoft)).toBe(true); + expect(isValid(Yahoo)).toBe(true); + expect(isValid(Email)).toBe(true); + expect(isValid(FacebookCustomAudienceId)).toBe(true); + expect(isValid(Other2)).toBe(true); + expect(isValid(Other3)).toBe(true); + expect(isValid(Other4)).toBe(true); + expect(isValid(Other5)).toBe(true); + expect(isValid(Other6)).toBe(true); + expect(isValid(Other7)).toBe(true); + expect(isValid(Other8)).toBe(true); + expect(isValid(Other9)).toBe(true); + expect(isValid(Other10)).toBe(true); + expect(isValid(MobileNumber)).toBe(true); + expect(isValid(PhoneNumber2)).toBe(true); + expect(isValid(PhoneNumber3)).toBe(true); + }); + + it('returns false if the identity type is not valid', () => { + const { isValid } = IdentityType; + + expect(isValid(NaN)).toBe(false); + expect(isValid('invalid' as unknown as number)).toBe(false); + }); + }); + + describe('#getNewIdentitiesByName', () => { + it('returns an identity name when passing an identity type', () => { + const { getNewIdentitiesByName } = IdentityType; + + const newIdentitiesByType = { + [CustomerId]: 'foo', + [Email]: 'bar@gmail.com', + }; + + expect(getNewIdentitiesByName(newIdentitiesByType)).toEqual({ + customerid: 'foo', + email: 'bar@gmail.com', + }); + }); + + it('returns an empty object if identity types are not found', () => { + const { getNewIdentitiesByName } = IdentityType; + + const newIdentitiesByType = { + ['invalid']: 'not-valid', + }; + + expect(getNewIdentitiesByName(newIdentitiesByType)).toEqual({}); + }); + + it('removes invalid identity types', () => { + const { getNewIdentitiesByName } = IdentityType; + + const newIdentitiesByType = { + [NaN]: 'not-a-number', + ['invalid']: 'not-valid', + [Email]: 'bar@gmail.com', + }; + + expect(getNewIdentitiesByName(newIdentitiesByType)).toEqual({ + email: 'bar@gmail.com', + }); + }); + }); + + describe('#getIdentityName', () => { + it('returns an identity name when passing an identity type', () => { + const { getIdentityName } = IdentityType; + + expect(getIdentityName(Other)).toBe('other'); + expect(getIdentityName(CustomerId)).toBe('customerid'); + expect(getIdentityName(Facebook)).toBe('facebook'); + expect(getIdentityName(Twitter)).toBe('twitter'); + expect(getIdentityName(Google)).toBe('google'); + expect(getIdentityName(Microsoft)).toBe('microsoft'); + expect(getIdentityName(Yahoo)).toBe('yahoo'); + expect(getIdentityName(Email)).toBe('email'); + expect(getIdentityName(FacebookCustomAudienceId)).toBe( + 'facebookcustomaudienceid' + ); + expect(getIdentityName(Other2)).toBe('other2'); + expect(getIdentityName(Other3)).toBe('other3'); + expect(getIdentityName(Other4)).toBe('other4'); + expect(getIdentityName(Other5)).toBe('other5'); + expect(getIdentityName(Other6)).toBe('other6'); + expect(getIdentityName(Other7)).toBe('other7'); + expect(getIdentityName(Other8)).toBe('other8'); + expect(getIdentityName(Other9)).toBe('other9'); + expect(getIdentityName(Other10)).toBe('other10'); + expect(getIdentityName(MobileNumber)).toBe('mobile_number'); + expect(getIdentityName(PhoneNumber2)).toBe('phone_number_2'); + expect(getIdentityName(PhoneNumber3)).toBe('phone_number_3'); + }); + + it('returns null if the identity type is not found', () => { + const { getIdentityName } = IdentityType; + + expect(getIdentityName('foo')).toBe(null); + }); + }); + + describe('#getName', () => { + it('returns the name of an Identity Type', () => { + const { getName } = IdentityType; + + expect(getName(CustomerId)).toBe('Customer ID'); + expect(getName(Facebook)).toBe('Facebook ID'); + expect(getName(Twitter)).toBe('Twitter ID'); + expect(getName(Google)).toBe('Google ID'); + expect(getName(Microsoft)).toBe('Microsoft ID'); + expect(getName(Yahoo)).toBe('Yahoo ID'); + expect(getName(Email)).toBe('Email'); + expect(getName(FacebookCustomAudienceId)).toBe('Facebook App User ID'); + }); + + // https://go.mparticle.com/work/SQDSDKS-6942 + it('returns other if the identity type is not found', () => { + const { getName } = IdentityType; + + expect(getName(Other)).toBe('Other ID'); + expect(getName(Other2)).toBe('Other ID'); + expect(getName(Other3)).toBe('Other ID'); + expect(getName(Other4)).toBe('Other ID'); + expect(getName(Other5)).toBe('Other ID'); + expect(getName(Other6)).toBe('Other ID'); + expect(getName(Other7)).toBe('Other ID'); + expect(getName(Other8)).toBe('Other ID'); + expect(getName(Other9)).toBe('Other ID'); + expect(getName(Other10)).toBe('Other ID'); + expect(getName(MobileNumber)).toBe('Other ID'); + expect(getName(PhoneNumber2)).toBe('Other ID'); + expect(getName(PhoneNumber3)).toBe('Other ID'); + + expect(getName(NaN)).toBe('Other ID'); + }); + }); + + describe('#getIdentityType', () => { + it('returns the identity type when passing an identity name', () => { + const { getIdentityType } = IdentityType; + + expect(getIdentityType('other')).toBe(Other); + expect(getIdentityType('customerid')).toBe(CustomerId); + expect(getIdentityType('facebook')).toBe(Facebook); + expect(getIdentityType('twitter')).toBe(Twitter); + expect(getIdentityType('google')).toBe(Google); + expect(getIdentityType('microsoft')).toBe(Microsoft); + expect(getIdentityType('yahoo')).toBe(Yahoo); + expect(getIdentityType('email')).toBe(Email); + expect(getIdentityType('facebookcustomaudienceid')).toBe( + FacebookCustomAudienceId + ); + expect(getIdentityType('other2')).toBe(Other2); + expect(getIdentityType('other3')).toBe(Other3); + expect(getIdentityType('other4')).toBe(Other4); + expect(getIdentityType('other5')).toBe(Other5); + expect(getIdentityType('other6')).toBe(Other6); + expect(getIdentityType('other7')).toBe(Other7); + expect(getIdentityType('other8')).toBe(Other8); + expect(getIdentityType('other9')).toBe(Other9); + expect(getIdentityType('other10')).toBe(Other10); + expect(getIdentityType('mobile_number')).toBe(MobileNumber); + expect(getIdentityType('phone_number_2')).toBe(PhoneNumber2); + expect(getIdentityType('phone_number_3')).toBe(PhoneNumber3); + }); + + it('returns false if the identity name is not found', () => { + const { getIdentityType } = IdentityType; + + expect(getIdentityType('foo')).toBe(false); + }); + }); +}); + +describe('ProductActionType', () => { + const { + Unknown, + AddToCart, + RemoveFromCart, + Checkout, + CheckoutOption, + Click, + ViewDetail, + Purchase, + Refund, + AddToWishlist, + RemoveFromWishlist, + } = ProductActionType; + + it('returns a product action type', () => { + expect(Unknown).toEqual(0); + expect(AddToCart).toEqual(1); + expect(RemoveFromCart).toEqual(2); + expect(Checkout).toEqual(3); + expect(CheckoutOption).toEqual(4); + expect(Click).toEqual(5); + expect(ViewDetail).toEqual(6); + expect(Purchase).toEqual(7); + expect(Refund).toEqual(8); + expect(AddToWishlist).toEqual(9); + expect(RemoveFromWishlist).toEqual(10); + }); + + describe('#getName', () => { + it('returns the name of a Product Action Type', () => { + expect(ProductActionType.getName(Unknown)).toBe('Unknown'); + expect(ProductActionType.getName(AddToCart)).toBe('Add to Cart'); + expect(ProductActionType.getName(RemoveFromCart)).toBe( + 'Remove from Cart' + ); + expect(ProductActionType.getName(Checkout)).toBe('Checkout'); + expect(ProductActionType.getName(CheckoutOption)).toBe( + 'Checkout Option' + ); + expect(ProductActionType.getName(Click)).toBe('Click'); + expect(ProductActionType.getName(ViewDetail)).toBe('View Detail'); + expect(ProductActionType.getName(Purchase)).toBe('Purchase'); + expect(ProductActionType.getName(Refund)).toBe('Refund'); + expect(ProductActionType.getName(AddToWishlist)).toBe( + 'Add to Wishlist' + ); + expect(ProductActionType.getName(RemoveFromWishlist)).toBe( + 'Remove from Wishlist' + ); + }); + + it('returns unknown if the product action type is not found', () => { + expect(ProductActionType.getName(NaN)).toBe('Unknown'); + }); + }); + + describe('#getExpansionName', () => { + it('returns the expanded name of a Product Action Type', () => { + expect(ProductActionType.getExpansionName(Unknown)).toBe('unknown'); + expect(ProductActionType.getExpansionName(AddToCart)).toBe( + 'add_to_cart' + ); + expect(ProductActionType.getExpansionName(RemoveFromCart)).toBe( + 'remove_from_cart' + ); + expect(ProductActionType.getExpansionName(Checkout)).toBe( + 'checkout' + ); + expect(ProductActionType.getExpansionName(CheckoutOption)).toBe( + 'checkout_option' + ); + expect(ProductActionType.getExpansionName(Click)).toBe('click'); + expect(ProductActionType.getExpansionName(ViewDetail)).toBe( + 'view_detail' + ); + expect(ProductActionType.getExpansionName(Purchase)).toBe( + 'purchase' + ); + expect(ProductActionType.getExpansionName(Refund)).toBe('refund'); + expect(ProductActionType.getExpansionName(AddToWishlist)).toBe( + 'add_to_wishlist' + ); + expect(ProductActionType.getExpansionName(RemoveFromWishlist)).toBe( + 'remove_from_wishlist' + ); + }); + + it('returns unknown if the product action type is not found', () => { + expect(ProductActionType.getExpansionName(NaN)).toBe('unknown'); + }); + }); +}); + +describe('PromotionActionType', () => { + it('returns a promotion action type', () => { + const { Unknown, PromotionView, PromotionClick } = PromotionActionType; + + expect(Unknown).toEqual(0); + expect(PromotionView).toEqual(1); + expect(PromotionClick).toEqual(2); + }); + + describe('#getName', () => { + it('returns the name of a Promotion Action Type', () => { + const { + PromotionView, + PromotionClick, + getName, + } = PromotionActionType; + + expect(getName(PromotionView)).toBe('view'); + expect(getName(PromotionClick)).toBe('click'); + }); + + it('returns unknown if the promotion action type is not found', () => { + const { getName } = PromotionActionType; + + expect(getName(0)).toBe('unknown'); + }); + }); + + describe('#getExpansionName', () => { + it('returns the name of a Promotion Action Type', () => { + const { + PromotionView, + PromotionClick, + getExpansionName, + } = PromotionActionType; + + expect(getExpansionName(PromotionView)).toBe('view'); + expect(getExpansionName(PromotionClick)).toBe('click'); + }); + + it('returns unknown if the promotion action type is not found', () => { + const { getExpansionName } = PromotionActionType; + + expect(getExpansionName(0)).toBe('unknown'); + }); + }); +}); + +describe('ProfileMessageType', () => { + it('returns a profile message type', () => { + expect(ProfileMessageType.Logout).toEqual(3); + }); +}); + +describe('ApplicationTransitionType', () => { + it('returns an application transition type', () => { + expect(ApplicationTransitionType.AppInit).toEqual(1); + }); +}); diff --git a/test/jest/utils.spec.ts b/test/jest/utils.spec.ts index 73494c3b8..3e5f13891 100644 --- a/test/jest/utils.spec.ts +++ b/test/jest/utils.spec.ts @@ -4,7 +4,7 @@ import { getHref, replaceMPID, replaceAmpWithAmpersand, - createCookieSyncUrl, + createInitialCookieSyncUrl, } from '../../src/utils'; import { deleteAllCookies } from './utils'; @@ -185,16 +185,16 @@ describe('Utils', () => { }); }); - describe('#createCookieSyncUrl', () => { + describe('#createInitialCookieSyncUrl', () => { const pixelUrl: string = 'https://abc.abcdex.net/ibs:exampleid=12345&exampleuuid=%%mpid%%&redir='; const redirectUrl: string = 'https://cookiesync.mparticle.com/v1/sync?esid=123456&MPID=%%mpid%%&ID=${DD_UUID}&Key=mpApiKey&env=2'; it('should return a cookieSyncUrl when both pixelUrl and redirectUrl are not null', () => { - expect(createCookieSyncUrl('testMPID', pixelUrl, redirectUrl)).toBe('https://abc.abcdex.net/ibs:exampleid=12345&exampleuuid=testMPID&redir=https%3A%2F%2Fcookiesync.mparticle.com%2Fv1%2Fsync%3Fesid%3D123456%26MPID%3DtestMPID%26ID%3D%24%7BDD_UUID%7D%26Key%3DmpApiKey%26env%3D2'); + expect(createInitialCookieSyncUrl('testMPID', pixelUrl, redirectUrl)).toBe('https://abc.abcdex.net/ibs:exampleid=12345&exampleuuid=testMPID&redir=https%3A%2F%2Fcookiesync.mparticle.com%2Fv1%2Fsync%3Fesid%3D123456%26MPID%3DtestMPID%26ID%3D%24%7BDD_UUID%7D%26Key%3DmpApiKey%26env%3D2'); }); it('should return a cookieSyncUrl when pixelUrl is not null but redirectUrl is null', () => { - expect(createCookieSyncUrl('testMPID', pixelUrl, null)).toBe('https://abc.abcdex.net/ibs:exampleid=12345&exampleuuid=testMPID&redir='); + expect(createInitialCookieSyncUrl('testMPID', pixelUrl, null)).toBe('https://abc.abcdex.net/ibs:exampleid=12345&exampleuuid=testMPID&redir='); }); }); }); \ No newline at end of file diff --git a/test/src/_test.index.ts b/test/src/_test.index.ts index 7cb1e5b00..eaf865077 100644 --- a/test/src/_test.index.ts +++ b/test/src/_test.index.ts @@ -2,6 +2,7 @@ import './config/setup'; // Import each test module +import './tests-mparticle-instance-manager'; import './tests-identity'; import './tests-batchUploader'; import './tests-core-sdk'; @@ -23,7 +24,6 @@ import './tests-mParticleUser'; import './tests-self-hosting-specific'; import './tests-runtimeToBatchEventsDTO'; import './tests-apiClient'; -import './tests-mparticle-instance-manager'; import './tests-queue-public-methods'; import './tests-batchUploader_3'; import './tests-validators'; @@ -36,7 +36,7 @@ import './tests-audience-manager'; import './tests-feature-flags'; import './tests-user'; import './tests-legacy-alias-requests'; -import './tests-aliasRequestApiClient'; +import './tests-identityApiClient'; import './tests-integration-capture'; import './tests-batchUploader_4'; diff --git a/test/src/config/utils.js b/test/src/config/utils.js index ac1d3fb29..a6582ae84 100644 --- a/test/src/config/utils.js +++ b/test/src/config/utils.js @@ -634,7 +634,8 @@ var pluses = /\+/g, hasIdentifyReturned = () => { return window.mParticle.Identity.getCurrentUser()?.getMPID() === testMPID; }, - hasIdentityCallInflightReturned = () => !mParticle.getInstance()?._Store?.identityCallInFlight; + hasIdentityCallInflightReturned = () => !mParticle.getInstance()?._Store?.identityCallInFlight, + hasConfigLoaded = () => !!mParticle.getInstance()?._Store?.configurationLoaded var TestsCore = { getLocalStorageProducts: getLocalStorageProducts, @@ -663,6 +664,7 @@ var TestsCore = { fetchMockSuccess: fetchMockSuccess, hasIdentifyReturned: hasIdentifyReturned, hasIdentityCallInflightReturned, + hasConfigLoaded, }; export default TestsCore; \ No newline at end of file diff --git a/test/src/tests-aliasRequestApiClient.ts b/test/src/tests-aliasRequestApiClient.ts deleted file mode 100644 index 10e47e6ee..000000000 --- a/test/src/tests-aliasRequestApiClient.ts +++ /dev/null @@ -1,123 +0,0 @@ -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 { 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'}); - }); -}); \ No newline at end of file diff --git a/test/src/tests-batchUploader_3.ts b/test/src/tests-batchUploader_3.ts index 6fad8868c..ba92a1c6d 100644 --- a/test/src/tests-batchUploader_3.ts +++ b/test/src/tests-batchUploader_3.ts @@ -1,19 +1,13 @@ import sinon from 'sinon'; import { urls, apiKey, MPConfig, testMPID } from './config/constants'; import { - BaseEvent, MParticleWebSDK, - SDKEvent, - SDKProductActionType, } from '../../src/sdkRuntimeModels'; -import { Batch, CustomEventData } from '@mparticle/event-models'; import Utils from './config/utils'; -import { BatchUploader } from '../../src/batchUploader'; import { expect } from 'chai'; import _BatchValidator from '../../src/mockBatchCreator'; -import Logger from '../../src/logger.js'; -import { event0, event1, event2, event3 } from '../fixtures/events'; import fetchMock from 'fetch-mock/esm/client'; +import { ProductActionType } from '../../src/types'; const { fetchMockSuccess, waitForCondition, hasIdentifyReturned } = Utils; declare global { @@ -137,7 +131,7 @@ describe('batch uploader', () => { window.mParticle.logEvent('Test Event'); var product1 = window.mParticle.eCommerce.createProduct('iphone', 'iphoneSKU', 999); - window.mParticle.eCommerce.logProductAction(SDKProductActionType.AddToCart, product1); + window.mParticle.eCommerce.logProductAction(ProductActionType.AddToCart, product1); const lastCall = fetchMock.lastCall(); const endpoint = lastCall[0]; diff --git a/test/src/tests-batchUploader_4.ts b/test/src/tests-batchUploader_4.ts index 6904c82d1..8ed667f07 100644 --- a/test/src/tests-batchUploader_4.ts +++ b/test/src/tests-batchUploader_4.ts @@ -1,19 +1,14 @@ import sinon from 'sinon'; import { urls, apiKey, MPConfig, testMPID } from './config/constants'; import { - BaseEvent, MParticleWebSDK, - SDKEvent, - SDKProductActionType, } from '../../src/sdkRuntimeModels'; import { Batch, CustomEventData } from '@mparticle/event-models'; import Utils from './config/utils'; -import { BatchUploader } from '../../src/batchUploader'; import { expect } from 'chai'; import _BatchValidator from '../../src/mockBatchCreator'; -import Logger from '../../src/logger.js'; -import { event0, event1, event2, event3 } from '../fixtures/events'; import fetchMock from 'fetch-mock/esm/client'; +import { ProductActionType } from '../../src/types'; const { fetchMockSuccess, waitForCondition, hasIdentifyReturned } = Utils; declare global { @@ -138,7 +133,7 @@ describe('batch uploader', () => { mockServer.requests.length.should.equal(1); var product1 = window.mParticle.eCommerce.createProduct('iphone', 'iphoneSKU', 999); - window.mParticle.eCommerce.logProductAction(SDKProductActionType.AddToCart, product1); + window.mParticle.eCommerce.logProductAction(ProductActionType.AddToCart, product1); // 1st request is /Identity call, 2nd request is /Event call const batch = JSON.parse(mockServer.secondRequest.requestBody); diff --git a/test/src/tests-cookie-syncing.ts b/test/src/tests-cookie-syncing.ts index 01fa95359..824474fd0 100644 --- a/test/src/tests-cookie-syncing.ts +++ b/test/src/tests-cookie-syncing.ts @@ -1206,7 +1206,7 @@ describe('cookie syncing', function() { it('should allow some cookie syncs to occur and others to not occur if there are multiple pixels with varying consent levels', function(done) { // This test has 2 pixelSettings. pixelSettings1 requires consent pixelSettings2 does not. When mparticle initializes, the pixelSettings2 should fire and pixelSettings1 shouldn't. - // After the appropriate consent is saved to the huser, pixelSettings1 will fire. + // After the appropriate consent is saved to the user, pixelSettings1 will fire. const includeOnMatch = true; // 'Only Forward' chosen in UI, 'includeOnMatch' in config const consented = true; diff --git a/test/src/tests-core-sdk.js b/test/src/tests-core-sdk.js index d0f91dddc..52d86c957 100644 --- a/test/src/tests-core-sdk.js +++ b/test/src/tests-core-sdk.js @@ -12,7 +12,13 @@ const DefaultConfig = Constants.DefaultConfig, findEventFromRequest = Utils.findEventFromRequest, findBatch = Utils.findBatch; -const { waitForCondition, fetchMockSuccess, hasIdentifyReturned, hasIdentityCallInflightReturned } = Utils; +const { + waitForCondition, + fetchMockSuccess, + hasIdentifyReturned, + hasIdentityCallInflightReturned, + hasConfigLoaded, +} = Utils; describe('core SDK', function() { beforeEach(function() { @@ -1126,7 +1132,7 @@ describe('core SDK', function() { }) }); - it('should initialize and log events even with a failed /config fetch and empty config', function async(done) { + it('should initialize and log events even with a failed /config fetch and empty config', async () => { // this instance occurs when self hosting and the user only passes an object into init mParticle._resetForTests(MPConfig); @@ -1152,12 +1158,7 @@ describe('core SDK', function() { mParticle.init(apiKey, window.mParticle.config); - waitForCondition(() => { - return ( - mParticle.getInstance()._Store.configurationLoaded === true - ); - }) - .then(() => { + await waitForCondition(hasConfigLoaded); // fetching the config is async and we need to wait for it to finish mParticle.getInstance()._Store.isInitialized.should.equal(true); @@ -1170,23 +1171,16 @@ describe('core SDK', function() { mParticle.Identity.identify({ userIdentities: { customerid: 'test' }, }); - waitForCondition(() => { - return ( - mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID1' - ); - }) - .then(() => { - mParticle.logEvent('Test Event'); - const testEvent = findEventFromRequest( - fetchMock.calls(), - 'Test Event' - ); - testEvent.should.be.ok(); + await waitForCondition(() => mParticle.Identity.getCurrentUser()?.getMPID() === 'MPID1'); - done(); - }); - }); + mParticle.logEvent('Test Event'); + const testEvent = findEventFromRequest( + fetchMock.calls(), + 'Test Event' + ); + + testEvent.should.be.ok(); }); it('should initialize without a config object passed to init', async function() { diff --git a/test/src/tests-forwarders.js b/test/src/tests-forwarders.js index 6a2bdacc1..c5ebb6776 100644 --- a/test/src/tests-forwarders.js +++ b/test/src/tests-forwarders.js @@ -2032,7 +2032,7 @@ describe('forwarders', function() { }); }); - it('should not forward event if event attribute forwarding rule is set and includeOnMatch is false', function(done) { + it('should not forward event if event attribute forwarding rule is set and includeOnMatch is false', async () => { mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); @@ -2048,12 +2048,8 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - waitForCondition(() => { - return ( - window.mParticle.getInstance()?._Store?.identityCallInFlight === false - ); - }) - .then(() => { + await waitForCondition(hasIdentityCallInflightReturned); + window.MockForwarder1.instance.receivedEvent.EventName.should.equal(1); window.MockForwarder1.instance.receivedEvent = null; @@ -2069,11 +2065,9 @@ describe('forwarders', function() { Should(event).not.be.ok(); - done(); - }); }); - it('should forward event if event attribute forwarding rule is set and includeOnMatch is false but attributes do not match', function(done) { + it('should forward event if event attribute forwarding rule is set and includeOnMatch is false but attributes do not match', async () => { mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); mockForwarder.register(window.mParticle.config); @@ -2090,12 +2084,8 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); - waitForCondition(() => { - return ( - window.mParticle.getInstance()?._Store?.identityCallInFlight === false - ); - }) - .then(() => { + await waitForCondition(hasIdentityCallInflightReturned); + window.MockForwarder1.instance.receivedEvent.EventName.should.equal(1); window.MockForwarder1.instance.receivedEvent = null; @@ -2110,9 +2100,6 @@ describe('forwarders', function() { const event = window.MockForwarder1.instance.receivedEvent; event.should.have.property('EventName', 'send this event to forwarder'); - - done(); - }); }); it('should send event to forwarder if filtering attribute and includingOnMatch is true', function(done) { diff --git a/test/src/tests-identities-attributes.ts b/test/src/tests-identities-attributes.ts index af7c84a9e..c70c0a90e 100644 --- a/test/src/tests-identities-attributes.ts +++ b/test/src/tests-identities-attributes.ts @@ -803,7 +803,7 @@ describe('identities and attributes', function() { product2 = mParticle.eCommerce.createProduct('Android', 'SKU2', 1); waitForCondition(hasIdentifyReturned) .then(() => { - mParticle.eCommerce.Cart.add([product1, product2]); + mParticle.eCommerce.Cart.add([product1, product2], false); const cartProducts = mParticle.Identity.getCurrentUser() .getCart() diff --git a/test/src/tests-identity.ts b/test/src/tests-identity.ts index b2ec0da92..ff4ca3f57 100644 --- a/test/src/tests-identity.ts +++ b/test/src/tests-identity.ts @@ -3,7 +3,7 @@ import fetchMock from 'fetch-mock/esm/client'; import { expect } from 'chai'; import Utils from './config/utils'; import Constants, { HTTP_ACCEPTED } from '../../src/constants'; -import { MParticleWebSDK } from '../../src/sdkRuntimeModels'; +import { MParticleWebSDK, SDKProduct } from '../../src/sdkRuntimeModels'; import { urls, apiKey, @@ -2029,7 +2029,7 @@ describe('identity', function() { mParticle.Identity.getCurrentUser().setUserAttribute('foo1', 'bar1'); expect(fetchMock.calls().length).to.equal(7); - const product1: Product = mParticle.eCommerce.createProduct( + const product1: SDKProduct = mParticle.eCommerce.createProduct( 'iPhone', '12345', '1000', @@ -4799,7 +4799,7 @@ describe('identity', function() { waitForCondition(hasIdentifyReturned) .then(() => { - const product = mParticle.eCommerce.createProduct( + const product: SDKProduct = mParticle.eCommerce.createProduct( 'iPhone', '12345', 400 @@ -4831,7 +4831,7 @@ describe('identity', function() { waitForCondition(hasIdentifyReturned) .then(() => { - const product = mParticle.eCommerce.createProduct( + const product: SDKProduct = mParticle.eCommerce.createProduct( 'iPhone', '12345', 400 diff --git a/test/src/tests-identityApiClient.ts b/test/src/tests-identityApiClient.ts new file mode 100644 index 000000000..80ebb9d20 --- /dev/null +++ b/test/src/tests-identityApiClient.ts @@ -0,0 +1,900 @@ +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 { + IAliasRequest, + IIdentityAPIRequestData, +} from '../../src/identity.interfaces'; +import Constants, { + HTTP_ACCEPTED, + HTTP_BAD_REQUEST, + HTTP_FORBIDDEN, + HTTP_NOT_FOUND, + HTTP_OK, + HTTP_SERVER_ERROR, + HTTP_UNAUTHORIZED, +} from '../../src/constants'; +import IdentityAPIClient, { IIdentityApiClient } from '../../src/identityApiClient'; +import { IIdentityResponse } from '../../src/identity-user-interfaces'; +import Utils from './config/utils'; +const { fetchMockSuccess } = Utils; +const { HTTPCodes } = Constants; + +declare global { + interface Window { + mParticle: MParticleWebSDK; + fetchMock: any; + } +} + +const mParticle = window.mParticle; + +declare global { + interface Window { + mParticle: MParticleWebSDK; + fetchMock: any; + } +} + + +describe('Identity Api Client', () => { + describe('#sendIdentityRequest', () => { + const identityRequest: IIdentityAPIRequestData = { + client_sdk: { + platform: 'web', + sdk_vendor: 'mparticle', + sdk_version: '1.0.0', + }, + context: 'test-context', + environment: 'development', + request_id: '123', + request_timestamp_unixtime_ms: Date.now(), + previous_mpid: null, + known_identities: { + email: 'user@mparticle.com', + }, + }; + + const originalIdentityApiData = { + userIdentities: { + other: '123456', + }, + }; + + const apiSuccessResponseBody = { + mpid: testMPID, + is_logged_in: false, + context: 'test-context', + is_ephemeral: false, + matched_identities: {}, + } + + const expectedIdentityResponse: IIdentityResponse = { + status: 200, + responseText: apiSuccessResponseBody, + cacheMaxAge: 0, + expireTimestamp: 0, + }; + + + it('should call parseIdentityResponse with the correct arguments', async () => { + fetchMockSuccess(urls.identify, apiSuccessResponseBody); + + const callbackSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: () => {}, + error: () => {}, + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: () => {}, + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + callbackSpy, + originalIdentityApiData, + parseIdentityResponseSpy, + testMPID, + identityRequest.known_identities + ); + + expect(parseIdentityResponseSpy.calledOnce, 'Call parseIdentityResponse').to.eq(true); + expect(parseIdentityResponseSpy.args[0][0]).to.deep.equal(expectedIdentityResponse); + expect(parseIdentityResponseSpy.args[0][1]).to.equal(testMPID); + expect(parseIdentityResponseSpy.args[0][2]).to.be.a('function'); + expect(parseIdentityResponseSpy.args[0][3]).to.deep.equal(originalIdentityApiData); + expect(parseIdentityResponseSpy.args[0][4]).to.equal('identify'); + expect(parseIdentityResponseSpy.args[0][5]).to.deep.equal(identityRequest.known_identities); + expect(parseIdentityResponseSpy.args[0][6]).to.equal(false); + }); + + it('should return early without calling parseIdentityResponse if the identity call is in flight', async () => { + fetchMockSuccess(urls.identify, apiSuccessResponseBody); + + const invokeCallbackSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: () => {}, + error: () => {}, + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: invokeCallbackSpy, + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + identityCallInFlight: true, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + sinon.spy(), + null, + parseIdentityResponseSpy, + testMPID, + null, + ); + + expect(invokeCallbackSpy.calledOnce, 'invokeCallbackSpy called').to.eq(true); + expect(invokeCallbackSpy.args[0][0]).to.be.a('function'); + expect(invokeCallbackSpy.args[0][1]).to.equal(-2); + expect(invokeCallbackSpy.args[0][2]).to.equal('There is currently an Identity request processing. Please wait for this to return before requesting again'); + + expect(parseIdentityResponseSpy.calledOnce, 'parseIdentityResponseSpy NOT called').to.eq(false); + + }); + + it('should call invokeCallback with an error if the fetch fails', async () => { + fetchMock.post(urls.identify, { + status: 500, + throws: { message: 'server error' }, + }, { + overwriteRoutes: true, + }); + + const callbackSpy = sinon.spy(); + const invokeCallbackSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: () => {}, + error: () => {}, + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: invokeCallbackSpy, + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + identityCallInFlight: false, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + callbackSpy, + null, + parseIdentityResponseSpy, + testMPID, + null, + ); + + expect(invokeCallbackSpy.calledOnce, 'invokeCallbackSpy called').to.eq(true); + expect(invokeCallbackSpy.args[0][0]).to.be.a('function'); + expect(invokeCallbackSpy.args[0][0]).to.equal(callbackSpy); + expect(invokeCallbackSpy.args[0][1]).to.equal(-1); + expect(invokeCallbackSpy.args[0][2]).to.equal('server error'); + }); + + it('should use XHR if fetch is not available', async () => { + const mockServer = sinon.createFakeServer(); + mockServer.respondImmediately = true; + + mockServer.respondWith(urls.identify, [ + 200, + {}, + JSON.stringify(apiSuccessResponseBody), + ]); + + const fetch = window.fetch; + delete window.fetch; + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: () => {}, + error: () => {}, + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: sinon.spy(), + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + identityCallInFlight: false, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + const originalIdentityApiData = { + userIdentities: { + other: '123456', + }, + }; + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + sinon.spy(), + originalIdentityApiData, + parseIdentityResponseSpy, + testMPID, + identityRequest.known_identities + ); + + expect(parseIdentityResponseSpy.calledOnce, 'Call parseIdentityResponse').to.eq(true); + expect(parseIdentityResponseSpy.args[0][0]).to.deep.equal(expectedIdentityResponse); + expect(parseIdentityResponseSpy.args[0][1]).to.equal(testMPID); + expect(parseIdentityResponseSpy.args[0][2]).to.be.a('function'); + expect(parseIdentityResponseSpy.args[0][3]).to.deep.equal(originalIdentityApiData); + expect(parseIdentityResponseSpy.args[0][4]).to.equal('identify'); + expect(parseIdentityResponseSpy.args[0][5]).to.deep.equal(identityRequest.known_identities); + expect(parseIdentityResponseSpy.args[0][6]).to.equal(false); + + window.fetch = fetch; + }); + + it('should construct the correct fetch payload', async () => { + fetchMockSuccess(urls.identify, apiSuccessResponseBody); + + const callbackSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: () => {}, + error: () => {}, + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: () => {}, + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + callbackSpy, + originalIdentityApiData, + parseIdentityResponseSpy, + testMPID, + identityRequest.known_identities + ); + + const expectedFetchPayload = { + method: 'post', + headers: { + Accept: 'text/plain;charset=UTF-8', + 'Content-Type': 'application/json', + 'x-mp-key': 'test_key', + }, + body: JSON.stringify(identityRequest), + }; + + expect(fetchMock.calls()[0][1].method, 'Payload Method').to.deep.equal(expectedFetchPayload.method); + expect(fetchMock.calls()[0][1].body, 'Payload Body').to.deep.equal(expectedFetchPayload.body); + expect(fetchMock.calls()[0][1].headers, 'Payload Headers').to.deep.equal(expectedFetchPayload.headers); + }); + + it('should include a detailed error message if the fetch returns a 400 (Bad Request)', async () => { + const identityRequestError = { + "Errors": [ + { + "code": "LOOKUP_ERROR", + "message": "knownIdentities is empty." + } + ], + "ErrorCode": "LOOKUP_ERROR", + "StatusCode": 400, + "RequestId": "6c6a234f-e171-4588-90a2-d7bc02530ec3" + }; + + fetchMock.post(urls.identify, { + status: HTTP_BAD_REQUEST, + body: identityRequestError, + }, { + overwriteRoutes: true, + }); + + const callbackSpy = sinon.spy(); + const invokeCallbackSpy = sinon.spy(); + const verboseSpy = sinon.spy(); + const errorSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: (message) => verboseSpy(message), + error: (message) => errorSpy(message), + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: (callback, httpCode, errorMessage) => + invokeCallbackSpy(callback, httpCode, errorMessage), + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + callbackSpy, + originalIdentityApiData, + parseIdentityResponseSpy, + testMPID, + identityRequest.known_identities + ); + + const expectedIdentityErrorRequest = { + status: 400, + responseText: identityRequestError, + cacheMAxAge: 0, + expireTimestamp: 0, + } + + expect(verboseSpy.lastCall, 'verboseSpy called').to.be.ok; + expect(verboseSpy.lastCall.firstArg).to.equal("Issue with sending Identity Request to mParticle Servers, received HTTP Code of 400 - knownIdentities is empty."); + + // A 400 will still call parseIdentityResponse + expect(parseIdentityResponseSpy.calledOnce, 'parseIdentityResponseSpy').to.eq(true); + expect(parseIdentityResponseSpy.args[0][0].status, 'Identity Error Request Status').to.equal(expectedIdentityErrorRequest.status); + expect(parseIdentityResponseSpy.args[0][0].responseText, 'Identity Error Request responseText').to.deep.equal(expectedIdentityErrorRequest.responseText); + expect(parseIdentityResponseSpy.args[0][1]).to.equal(testMPID); + expect(parseIdentityResponseSpy.args[0][2]).to.be.a('function'); + expect(parseIdentityResponseSpy.args[0][3]).to.deep.equal(originalIdentityApiData); + expect(parseIdentityResponseSpy.args[0][4]).to.equal('identify'); + expect(parseIdentityResponseSpy.args[0][5]).to.deep.equal(identityRequest.known_identities); + expect(parseIdentityResponseSpy.args[0][6]).to.equal(false); + }); + + it('should include a detailed error message if the fetch returns a 401 (Unauthorized)', async () => { + fetchMock.post(urls.identify, { + status: HTTP_UNAUTHORIZED, + body: null, + }, { + overwriteRoutes: true, + }); + + const callbackSpy = sinon.spy(); + const invokeCallbackSpy = sinon.spy(); + const verboseSpy = sinon.spy(); + const errorSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: (message) => verboseSpy(message), + error: (message) => errorSpy(message), + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: (callback, httpCode, errorMessage) => + invokeCallbackSpy(callback, httpCode, errorMessage), + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + callbackSpy, + originalIdentityApiData, + parseIdentityResponseSpy, + testMPID, + identityRequest.known_identities + ); + + expect(errorSpy.lastCall, 'errorSpy called').to.be.ok; + expect(errorSpy.lastCall.firstArg).to.equal("Error sending identity request to servers - Received HTTP Code of 401"); + + expect(invokeCallbackSpy.calledOnce, 'invokeCallbackSpy').to.eq(true); + expect(invokeCallbackSpy.args[0][0]).to.equal(callbackSpy); + expect(invokeCallbackSpy.args[0][1]).to.equal(-1); + expect(invokeCallbackSpy.args[0][2]).to.equal("Received HTTP Code of 401"); + + // A 401 should not call parseIdentityResponse + expect(parseIdentityResponseSpy.calledOnce, 'parseIdentityResponseSpy').to.eq(false); + }); + + it('should include a detailed error message if the fetch returns a 403 (Forbidden)', async () => { + fetchMock.post(urls.identify, { + status: HTTP_FORBIDDEN, + body: null, + }, { + overwriteRoutes: true, + }); + + const callbackSpy = sinon.spy(); + const invokeCallbackSpy = sinon.spy(); + const verboseSpy = sinon.spy(); + const errorSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: (message) => verboseSpy(message), + error: (message) => errorSpy(message), + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: (callback, httpCode, errorMessage) => invokeCallbackSpy(callback, httpCode, errorMessage), + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + callbackSpy, + originalIdentityApiData, + parseIdentityResponseSpy, + testMPID, + identityRequest.known_identities + ); + + expect(errorSpy.lastCall, 'errorSpy called').to.be.ok; + expect(errorSpy.lastCall.firstArg).to.equal("Error sending identity request to servers - Received HTTP Code of 403"); + + expect(invokeCallbackSpy.calledOnce, 'invokeCallbackSpy').to.eq(true); + expect(invokeCallbackSpy.args[0][0]).to.equal(callbackSpy); + expect(invokeCallbackSpy.args[0][1]).to.equal(-1); + expect(invokeCallbackSpy.args[0][2]).to.equal("Received HTTP Code of 403"); + + // A 403 should not call parseIdentityResponse + expect(parseIdentityResponseSpy.calledOnce, 'parseIdentityResponseSpy').to.eq(false); + + }); + + it('should include a detailed error message if the fetch returns a 404 (Not Found)', async () => { + fetchMock.post(urls.identify, { + status: HTTP_NOT_FOUND, + body: null, + }, { + overwriteRoutes: true, + }); + + const callbackSpy = sinon.spy(); + const invokeCallbackSpy = sinon.spy(); + const verboseSpy = sinon.spy(); + const errorSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: (message) => verboseSpy(message), + error: (message) => errorSpy(message), + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: (callback, httpCode, errorMessage) => invokeCallbackSpy(callback, httpCode, errorMessage), + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + callbackSpy, + originalIdentityApiData, + parseIdentityResponseSpy, + testMPID, + identityRequest.known_identities + ); + + expect(errorSpy.lastCall, 'errorSpy called').to.be.ok; + expect(errorSpy.lastCall.firstArg).to.equal("Error sending identity request to servers - Received HTTP Code of 404"); + + expect(invokeCallbackSpy.calledOnce, 'invokeCallbackSpy').to.eq(true); + expect(invokeCallbackSpy.args[0][0]).to.equal(callbackSpy); + expect(invokeCallbackSpy.args[0][1]).to.equal(-1); + expect(invokeCallbackSpy.args[0][2]).to.equal("Received HTTP Code of 404"); + + // A 404 should not call parseIdentityResponse + expect(parseIdentityResponseSpy.calledOnce, 'parseIdentityResponseSpy').to.eq(false); + }); + + it('should include a detailed error message if the fetch returns a 500 (Server Error)', async () => { + fetchMock.post(urls.identify, { + status: HTTP_SERVER_ERROR, + body: { + "Errors": [ + { + "code": "INTERNAL_ERROR", + "message": "An unknown error was encountered." + } + ], + "ErrorCode": "INTERNAL_ERROR", + "StatusCode": 500, + "RequestId": null + }, + }, { + overwriteRoutes: true, + }); + + const callbackSpy = sinon.spy(); + const verboseSpy = sinon.spy(); + const errorSpy = sinon.spy(); + + const mpInstance: MParticleWebSDK = ({ + Logger: { + verbose: (message) => verboseSpy(message), + error: (message) => errorSpy(message), + }, + _Helpers: { + createServiceUrl: () => + 'https://identity.mparticle.com/v1/', + + invokeCallback: () => {}, + }, + _Store: { + devToken: 'test_key', + SDKConfig: { + identityUrl: '', + }, + }, + _Persistence: {}, + } as unknown) as MParticleWebSDK; + + const identityApiClient: IIdentityApiClient = new IdentityAPIClient( + mpInstance + ); + + const parseIdentityResponseSpy = sinon.spy(); + + await identityApiClient.sendIdentityRequest( + identityRequest, + 'identify', + callbackSpy, + originalIdentityApiData, + parseIdentityResponseSpy, + testMPID, + identityRequest.known_identities + ); + + expect(errorSpy.calledOnce, 'errorSpy called').to.eq(true); + + expect(errorSpy.args[0][0]).to.equal('Error sending identity request to servers - Received HTTP Code of 500'); + }); + }); + + describe('#sendAliasRequest', () => { + const aliasUrl = 'https://jssdks.mparticle.com/v1/identity/test_key/Alias'; + + beforeEach(function() { + fetchMockSuccess(urls.events); + mParticle.init(apiKey, window.mParticle.config); + }); + + afterEach(function() { + 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 identityApiClient = new IdentityAPIClient(mpInstance); + + const aliasRequest: IAliasRequest = { + destinationMpid: '123', + sourceMpid: '456', + startTime: 10001230123, + endTime: 10001231123, + }; + + const aliasCallback = sinon.spy(); + fetchMock.post(aliasUrl, HTTP_OK); + + await identityApiClient.sendAliasRequest( + 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 identityApiClient = new IdentityAPIClient(mpInstance); + const aliasRequest: IAliasRequest = { + destinationMpid: '123', + sourceMpid: '456', + startTime: 10001230123, + endTime: 10001231123, + }; + + const aliasCallback = sinon.spy(); + fetchMock.post(aliasUrl, HTTP_ACCEPTED); + + await identityApiClient.sendAliasRequest( + 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 identityApiClient = new IdentityAPIClient(mpInstance); + const aliasRequest: IAliasRequest = { + destinationMpid: '123', + sourceMpid: '456', + startTime: 10001230123, + endTime: 10001231123, + }; + + const aliasCallback = sinon.spy(); + fetchMock.post(aliasUrl, { + status: HTTP_BAD_REQUEST, + body: { + message:"The payload was malformed JSON or had missing fields.", + code:"INVALID_DATA"} + }, { + overwriteRoutes: true + }); + + await identityApiClient.sendAliasRequest( + aliasRequest, + aliasCallback + ); + expect(aliasCallback.calledOnce).to.eq(true); + const callbackArgs = aliasCallback.getCall(0).args; + expect(callbackArgs[0]).to.deep.equal({ + httpCode: HTTP_BAD_REQUEST, + message: 'The payload was malformed JSON or had missing fields.', + }); + }); + + it('should have an httpCode and an error message passed to the callback on a 401', async () => { + const mpInstance: MParticleWebSDK = mParticle.getInstance(); + const identityApiClient = new IdentityAPIClient(mpInstance); + const aliasRequest: IAliasRequest = { + destinationMpid: '123', + sourceMpid: '456', + startTime: 10001230123, + endTime: 10001231123, + }; + + const aliasCallback = sinon.spy(); + fetchMock.post(aliasUrl, { + status: HTTP_UNAUTHORIZED, + body: null, + }); + + await identityApiClient.sendAliasRequest( + aliasRequest, + aliasCallback + ); + expect(aliasCallback.calledOnce).to.eq(true); + const callbackArgs = aliasCallback.getCall(0).args; + expect(callbackArgs[0].httpCode).to.equal(HTTPCodes.noHttpCoverage); + expect(callbackArgs[0].message).deep.equal('Received HTTP Code of 401'); + }); + + it('should have an httpCode and an error message passed to the callback on a 403', async () => { + const mpInstance: MParticleWebSDK = mParticle.getInstance(); + const identityApiClient = new IdentityAPIClient(mpInstance); + const aliasRequest: IAliasRequest = { + destinationMpid: '123', + sourceMpid: '456', + startTime: 10001230123, + endTime: 10001231123, + }; + + const aliasCallback = sinon.spy(); + fetchMock.post(aliasUrl, { + status: HTTP_FORBIDDEN, + body: null, + }); + + await identityApiClient.sendAliasRequest( + aliasRequest, + aliasCallback + ); + expect(aliasCallback.calledOnce).to.eq(true); + const callbackArgs = aliasCallback.getCall(0).args; + expect(callbackArgs[0].httpCode).to.equal(HTTPCodes.noHttpCoverage); + expect(callbackArgs[0].message).deep.equal('Received HTTP Code of 403'); + }); + + it('should have an httpCode and an error message passed to the callback on a 404', async () => { + const mpInstance: MParticleWebSDK = mParticle.getInstance(); + const identityApiClient = new IdentityAPIClient(mpInstance); + const aliasRequest: IAliasRequest = { + destinationMpid: '123', + sourceMpid: '456', + startTime: 10001230123, + endTime: 10001231123, + }; + + const aliasCallback = sinon.spy(); + fetchMock.post(aliasUrl, { + status: HTTP_NOT_FOUND, + body: null, + }); + + await identityApiClient.sendAliasRequest( + aliasRequest, + aliasCallback + ); + expect(aliasCallback.calledOnce).to.eq(true); + const callbackArgs = aliasCallback.getCall(0).args; + expect(callbackArgs[0].httpCode).to.equal(HTTPCodes.noHttpCoverage); + expect(callbackArgs[0].message).deep.equal('Received HTTP Code of 404'); + }); + + it('should have an httpCode and an error message passed to the callback on a 500', async () => { + const mpInstance: MParticleWebSDK = mParticle.getInstance(); + const identityApiClient = new IdentityAPIClient(mpInstance); + const aliasRequest: IAliasRequest = { + destinationMpid: '123', + sourceMpid: '456', + startTime: 10001230123, + endTime: 10001231123, + }; + + const aliasCallback = sinon.spy(); + fetchMock.post(aliasUrl, { + status: HTTP_SERVER_ERROR, + body: { + "Errors": [ + { + "code": "INTERNAL_ERROR", + "message": "An unknown error was encountered." + } + ], + "ErrorCode": "INTERNAL_ERROR", + "StatusCode": 500, + "RequestId": null + }, + }); + + await identityApiClient.sendAliasRequest( + aliasRequest, + aliasCallback + ); + expect(aliasCallback.calledOnce).to.eq(true); + const callbackArgs = aliasCallback.getCall(0).args; + expect(callbackArgs[0].httpCode).to.equal(HTTPCodes.noHttpCoverage); + expect(callbackArgs[0].message).deep.equal('Received HTTP Code of 500'); + }); + }); +}); diff --git a/test/src/tests-kit-blocking.ts b/test/src/tests-kit-blocking.ts index 0c9649607..c945b61d2 100644 --- a/test/src/tests-kit-blocking.ts +++ b/test/src/tests-kit-blocking.ts @@ -8,7 +8,7 @@ import Types from '../../src/types'; import { DataPlanVersion } from '@mparticle/data-planning-models'; import fetchMock from 'fetch-mock/esm/client'; import { expect } from 'chai' -const { findBatch, waitForCondition, fetchMockSuccess, hasIdentifyReturned } = Utils; +const { waitForCondition, fetchMockSuccess, hasIdentifyReturned } = Utils; let forwarderDefaultConfiguration = Utils.forwarderDefaultConfiguration, MockForwarder = Utils.MockForwarder; @@ -468,7 +468,7 @@ describe('kit blocking', () => { it('should not transform productAttributes if a product attribute is not planned, additionalProperties = false, and blok.ea = true', function(done) { event.EventName = 'eCommerce - AddToCart'; - event.EventCategory = Types.CommerceEventType.AddToCart; + event.EventCategory = Types.CommerceEventType.ProductAddToCart; event.EventDataType = Types.MessageType.Commerce; event.ProductAction = { ProductActionType: SDKProductActionType.AddToCart, diff --git a/test/src/tests-mparticle-instance-manager.js b/test/src/tests-mparticle-instance-manager.ts similarity index 54% rename from test/src/tests-mparticle-instance-manager.js rename to test/src/tests-mparticle-instance-manager.ts index 694cc37d4..4a007a225 100644 --- a/test/src/tests-mparticle-instance-manager.js +++ b/test/src/tests-mparticle-instance-manager.ts @@ -1,9 +1,22 @@ import sinon from 'sinon'; import fetchMock from 'fetch-mock/esm/client'; +import { expect } from 'chai'; import { urls, MPConfig } from './config/constants'; import Utils from './config/utils'; -const { findEventFromRequest, waitForCondition, fetchMockSuccess } = Utils; +import { MParticleWebSDK } from '../../src/sdkRuntimeModels'; +const { + findEventFromRequest, + waitForCondition, + fetchMockSuccess, +} = Utils; + +declare global { + interface Window { + mParticle: MParticleWebSDK; + } +} +const mParticle = window.mParticle as MParticleWebSDK; let mockServer; function returnEventForMPInstance(calls, apiKey, eventName) { @@ -13,39 +26,43 @@ function returnEventForMPInstance(calls, apiKey, eventName) { return findEventFromRequest(requestsPerApiKey, eventName); } -describe('mParticle instance manager', function() { - it('has all public apis on it', function(done) { - mParticle.ProductActionType.should.have.properties([ - 'Unknown', - 'AddToCart', - 'RemoveFromCart', - 'Checkout', - 'CheckoutOption', - 'Click', - 'ViewDetail', - 'Purchase', - 'Refund', - 'AddToWishlist', - 'RemoveFromWishlist', - 'getName', - 'getExpansionName', - ]); - mParticle.CommerceEventType.should.have.properties([ - 'ProductAddToCart', - 'ProductRemoveFromCart', - 'ProductCheckout', - 'ProductCheckoutOption', - 'ProductClick', - 'ProductViewDetail', - 'ProductPurchase', - 'ProductRefund', - 'PromotionView', - 'PromotionClick', - 'ProductAddToWishlist', - 'ProductRemoveFromWishlist', - 'ProductImpression', - ]); - mParticle.EventType.should.have.properties([ +describe('mParticle instance manager', () => { + it('has all public apis on it', () => { + expect(mParticle.ProductActionType, 'Product Action Type').to.have.keys( + [ + 'Unknown', + 'AddToCart', + 'RemoveFromCart', + 'Checkout', + 'CheckoutOption', + 'Click', + 'ViewDetail', + 'Purchase', + 'Refund', + 'AddToWishlist', + 'RemoveFromWishlist', + 'getName', + 'getExpansionName', + ] + ); + expect(mParticle.CommerceEventType, 'Commerce Event Type').to.have.keys( + [ + 'ProductAddToCart', + 'ProductRemoveFromCart', + 'ProductCheckout', + 'ProductCheckoutOption', + 'ProductClick', + 'ProductViewDetail', + 'ProductPurchase', + 'ProductRefund', + 'PromotionView', + 'PromotionClick', + 'ProductAddToWishlist', + 'ProductRemoveFromWishlist', + 'ProductImpression', + ] + ); + expect(mParticle.EventType, 'Event Type').to.have.keys([ 'Unknown', 'Navigation', 'Location', @@ -58,14 +75,15 @@ describe('mParticle instance manager', function() { 'Media', 'getName', ]); - mParticle.PromotionType.should.have.properties([ + expect(mParticle.PromotionType, 'Promotion Type').to.have.keys([ 'Unknown', 'PromotionView', 'PromotionClick', 'getName', 'getExpansionName', ]); - mParticle.IdentityType.should.have.properties([ + + expect(mParticle.IdentityType, 'Identity Type').to.have.keys([ 'Other', 'CustomerId', 'Facebook', @@ -78,12 +96,23 @@ describe('mParticle instance manager', function() { 'Other2', 'Other3', 'Other4', + 'Other5', + 'Other6', + 'Other7', + 'Other8', + 'Other9', + 'Other10', + 'MobileNumber', + 'PhoneNumber2', + 'PhoneNumber3', 'isValid', 'getName', 'getIdentityType', 'getIdentityName', + 'getValuesAsStrings', + 'getNewIdentitiesByName', ]); - mParticle.Identity.should.have.properties([ + expect(mParticle.Identity, 'Identity').to.have.keys([ 'HTTPCodes', 'identify', 'logout', @@ -95,7 +124,7 @@ describe('mParticle instance manager', function() { 'aliasUsers', 'createAliasRequest', ]); - mParticle.Identity.HTTPCodes.should.have.properties([ + expect(mParticle.Identity.HTTPCodes, 'HTTP Codes').to.have.keys([ 'noHttpCoverage', 'activeIdentityRequest', 'activeSession', @@ -104,7 +133,7 @@ describe('mParticle instance manager', function() { 'loggingDisabledOrMissingAPIKey', 'tooManyRequests', ]); - mParticle.eCommerce.should.have.properties([ + expect(mParticle.eCommerce, 'eCommerce').to.have.keys([ 'Cart', 'setCurrencyCode', 'createProduct', @@ -119,14 +148,17 @@ describe('mParticle instance manager', function() { 'logRefund', 'expandCommerceEvent', ]); - mParticle.Consent.should.have.properties([ + expect(mParticle.Consent, 'Consent').to.have.keys([ 'createGDPRConsent', 'createCCPAConsent', 'createConsentState', ]); - mParticle.sessionManager.should.have.property('getSession'); - mParticle.should.have.properties([ + expect(mParticle.sessionManager, 'Session Manager').to.have.keys([ + 'getSession', + ]); + + expect(mParticle, 'mParticle global').to.have.all.keys([ 'Store', 'getDeviceId', 'generateHash', @@ -170,13 +202,22 @@ describe('mParticle instance manager', function() { '_getActiveForwarders', '_getIntegrationDelays', '_setIntegrationDelay', + '_BatchValidator', + '_instances', + '_isTestEnv', + '_setWrapperSDKInfo', + 'MPSideloadedKit', + 'config', + 'getInstance', + 'setDeviceId', + 'isInitialized', + 'getEnvironment', + 'upload', ]); - - done(); }); - describe('multiple instances testing', function() { - beforeEach(function() { + describe('multiple instances testing', () => { + beforeEach(() => { //remove each of the instance's localStorage localStorage.removeItem('mprtcl-v4_wtTest1'); localStorage.removeItem('mprtcl-v4_wtTest2'); @@ -191,22 +232,31 @@ describe('mParticle instance manager', function() { mockServer.respondImmediately = true; //config default instance - fetchMock.get('https://jssdkcdns.mparticle.com/JS/v2/apiKey1/config?env=0', { - status: 200, - body: JSON.stringify({ workspaceToken: 'wtTest1' }), - }); + fetchMock.get( + 'https://jssdkcdns.mparticle.com/JS/v2/apiKey1/config?env=0', + { + status: 200, + body: JSON.stringify({ workspaceToken: 'wtTest1' }), + } + ); //config instance 2 - fetchMock.get('https://jssdkcdns.mparticle.com/JS/v2/apiKey2/config?env=0', { - status: 200, - body: JSON.stringify({ workspaceToken: 'wtTest2' }), - }); + fetchMock.get( + 'https://jssdkcdns.mparticle.com/JS/v2/apiKey2/config?env=0', + { + status: 200, + body: JSON.stringify({ workspaceToken: 'wtTest2' }), + } + ); //config instance 3 - fetchMock.get('https://jssdkcdns.mparticle.com/JS/v2/apiKey3/config?env=0', { - status: 200, - body: JSON.stringify({ workspaceToken: 'wtTest3' }), - }); + fetchMock.get( + 'https://jssdkcdns.mparticle.com/JS/v2/apiKey3/config?env=0', + { + status: 200, + body: JSON.stringify({ workspaceToken: 'wtTest3' }), + } + ); // default instance event mock fetchMock.post( @@ -224,7 +274,8 @@ describe('mParticle instance manager', function() { // identity mock fetchMockSuccess(urls.identify, { - mpid: 'testMPID', is_logged_in: false + mpid: 'testMPID', + is_logged_in: false, }); window.mParticle.config.requestConfig = true; @@ -240,104 +291,109 @@ describe('mParticle instance manager', function() { fetchMock.restore(); }); - it('creates multiple instances with their own cookies', function(done) { + it('creates multiple instances with their own cookies', done => { // setTimeout to allow config to come back from the beforeEach initialization setTimeout(() => { - const cookies1 = window.localStorage.getItem('mprtcl-v4_wtTest1'); - const cookies2 = window.localStorage.getItem('mprtcl-v4_wtTest2'); - const cookies3 = window.localStorage.getItem('mprtcl-v4_wtTest3'); - + const cookies1 = window.localStorage.getItem( + 'mprtcl-v4_wtTest1' + ); + const cookies2 = window.localStorage.getItem( + 'mprtcl-v4_wtTest2' + ); + const cookies3 = window.localStorage.getItem( + 'mprtcl-v4_wtTest3' + ); + cookies1.includes('apiKey1').should.equal(true); cookies2.includes('apiKey2').should.equal(true); cookies3.includes('apiKey3').should.equal(true); - + done(); - }, 50) + }, 50); }); - it('logs events to their own instances', function(done) { + it('logs events to their own instances', async () => { // setTimeout to allow config to come back from the beforeEach initialization - waitForCondition(() => { + await waitForCondition(() => { return ( - mParticle.getInstance('default_instance')._Store.configurationLoaded === true && - mParticle.getInstance('instance2')._Store.configurationLoaded === true && - mParticle.getInstance('instance3')._Store.configurationLoaded === true - ); - }) - .then(() => { - mParticle.getInstance('default_instance').logEvent('hi1'); - mParticle.getInstance('instance2').logEvent('hi2'); - mParticle.getInstance('instance3').logEvent('hi3'); - - const instance1Event = returnEventForMPInstance( - fetchMock.calls(), - 'apiKey1', - 'hi1' - ); - instance1Event.should.be.ok(); - - const instance2Event = returnEventForMPInstance( - fetchMock.calls(), - 'apiKey2', - 'hi2' + mParticle.getInstance('default_instance')._Store + .configurationLoaded === true && + mParticle.getInstance('instance2')._Store + .configurationLoaded === true && + mParticle.getInstance('instance3')._Store + .configurationLoaded === true ); - instance2Event.should.be.ok(); + }); + mParticle.getInstance('default_instance').logEvent('hi1'); + mParticle.getInstance('instance2').logEvent('hi2'); + mParticle.getInstance('instance3').logEvent('hi3'); + + const instance1Event = returnEventForMPInstance( + fetchMock.calls(), + 'apiKey1', + 'hi1' + ); + instance1Event.should.be.ok(); - const instance3Event = returnEventForMPInstance( - fetchMock.calls(), - 'apiKey3', - 'hi3' - ); - instance3Event.should.be.ok(); + const instance2Event = returnEventForMPInstance( + fetchMock.calls(), + 'apiKey2', + 'hi2' + ); + instance2Event.should.be.ok(); - const instance1EventsFail1 = returnEventForMPInstance( - fetchMock.calls(), - 'apiKey1', - 'hi2' - ); + const instance3Event = returnEventForMPInstance( + fetchMock.calls(), + 'apiKey3', + 'hi3' + ); + instance3Event.should.be.ok(); - Should(instance1EventsFail1).not.be.ok(); + const instance1EventsFail1 = returnEventForMPInstance( + fetchMock.calls(), + 'apiKey1', + 'hi2' + ); - const instance1EventsFail2 = returnEventForMPInstance( - fetchMock.calls(), - 'apiKey1', - 'hi3' - ); - Should(instance1EventsFail2).not.be.ok(); + expect(instance1EventsFail1).not.be.ok; - const instance2EventsFail1 = returnEventForMPInstance( - fetchMock.calls(), - 'apiKey2', - 'hi1' - ); - Should(instance2EventsFail1).not.be.ok(); + const instance1EventsFail2 = returnEventForMPInstance( + fetchMock.calls(), + 'apiKey1', + 'hi3' + ); + expect(instance1EventsFail2).not.be.ok; - const instance2EventsFail2 = returnEventForMPInstance( - fetchMock.calls(), - 'apiKey2', - 'hi3' - ); - Should(instance2EventsFail2).not.be.ok(); + const instance2EventsFail1 = returnEventForMPInstance( + fetchMock.calls(), + 'apiKey2', + 'hi1' + ); + expect(instance2EventsFail1).not.be.ok; - const instance3EventsFail1 = returnEventForMPInstance( - fetchMock.calls(), - 'apiKey3', - 'hi1' - ); - Should(instance3EventsFail1).not.be.ok(); + const instance2EventsFail2 = returnEventForMPInstance( + fetchMock.calls(), + 'apiKey2', + 'hi3' + ); + expect(instance2EventsFail2).not.be.ok; - const instance3EventsFail2 = returnEventForMPInstance( - fetchMock.calls(), - 'apiKey3', - 'hi2' - ); - Should(instance3EventsFail2).not.be.ok(); + const instance3EventsFail1 = returnEventForMPInstance( + fetchMock.calls(), + 'apiKey3', + 'hi1' + ); + expect(instance3EventsFail1).not.be.ok; - done(); - }) + const instance3EventsFail2 = returnEventForMPInstance( + fetchMock.calls(), + 'apiKey3', + 'hi2' + ); + expect(instance3EventsFail2).not.be.ok; }); - it('logs purchase events to their own instances', function(done) { + it('logs purchase events to their own instances', done => { const prodattr1 = { journeyType: 'testjourneytype1', eventMetric1: 'metric2', @@ -401,13 +457,19 @@ describe('mParticle instance manager', function() { 'apiKey3', 'purchase' ); - Should(instance1Event).be.ok(); - Should(instance2Event).not.be.ok(); - Should(instance3Event).not.be.ok(); + expect(instance1Event).be.ok; + expect(instance2Event).not.be.ok; + expect(instance3Event).not.be.ok; mParticle .getInstance('instance2') - .eCommerce.logPurchase(ta, [product1, product2]); + .eCommerce.logPurchase( + ta, + [product1, product2], + false, + {}, + {} + ); instance2Event = returnEventForMPInstance( fetchMock.calls(), @@ -421,11 +483,17 @@ describe('mParticle instance manager', function() { ); instance2Event.should.be.ok(); - Should(instance3Event).not.be.ok(); + expect(instance3Event).not.be.ok; mParticle .getInstance('instance3') - .eCommerce.logPurchase(ta, [product1, product2]); + .eCommerce.logPurchase( + ta, + [product1, product2], + false, + {}, + {} + ); instance3Event = returnEventForMPInstance( fetchMock.calls(), @@ -436,7 +504,7 @@ describe('mParticle instance manager', function() { instance3Event.should.be.ok(); done(); - }, 50) + }, 50); }); }); -}); \ No newline at end of file +}); diff --git a/test/src/tests-serverModel.ts b/test/src/tests-serverModel.ts index c84faceee..7e3fc4015 100644 --- a/test/src/tests-serverModel.ts +++ b/test/src/tests-serverModel.ts @@ -1,5 +1,4 @@ import Types from '../../src/types'; -import sinon from 'sinon'; import { urls, testMPID, apiKey } from './config/constants'; import { expect } from 'chai'; import { IUploadObject } from '../../src/serverModel'; @@ -669,7 +668,7 @@ describe('ServerModel', () => { const event: BaseEvent = { name: 'Test Event', sourceMessageId: null, - messageType: Types.MessageType.CustomEvent, + messageType: Types.MessageType.PageEvent, eventType: Types.EventType.Other, }; @@ -689,7 +688,7 @@ describe('ServerModel', () => { const event: BaseEvent = { name: 'Test Opt Out Event', - messageType: Types.MessageType.CustomEvent, + messageType: Types.MessageType.PageEvent, }; const actualEventObject = mParticle @@ -707,7 +706,7 @@ describe('ServerModel', () => { const event: BaseEvent = { name: 'Test Opt Out Event', - messageType: Types.MessageType.CustomEvent, + messageType: Types.MessageType.PageEvent, }; const actualEventObject = mParticle