diff --git a/src/apiClient.ts b/src/apiClient.ts index 5e0a2fe7..f621f6f3 100644 --- a/src/apiClient.ts +++ b/src/apiClient.ts @@ -10,6 +10,7 @@ import { IMParticleUser, ISDKUserAttributes } from './identity-user-interfaces'; import { AsyncUploader, FetchUploader, XHRUploader } from './uploaders'; import { IMParticleWebSDKInstance } from './mp-instance'; import { appendUserInfo } from './user-utils'; +import { ErrorCodes } from './logging/errorCodes'; export interface IAPIClient { uploader: BatchUploader | null; @@ -172,7 +173,8 @@ export default function APIClient( } } catch (e) { mpInstance.Logger.error( - 'Error sending forwarding stats to mParticle servers.' + 'Error sending forwarding stats to mParticle servers.', + ErrorCodes.API_CLIENT_ERROR ); } }; diff --git a/src/audienceManager.ts b/src/audienceManager.ts index 4771bde4..d9cbcedf 100644 --- a/src/audienceManager.ts +++ b/src/audienceManager.ts @@ -6,6 +6,7 @@ import { IFetchPayload } from './uploaders'; import Audience from './audience'; +import { ErrorCodes } from './logging/errorCodes'; export interface IAudienceMembershipsServerResponse { dt: 'cam'; // current audience memberships @@ -81,7 +82,8 @@ export default class AudienceManager { } } catch (e) { this.logger.error( - `Error retrieving audiences. ${e}` + `Error retrieving audiences. ${e}`, + ErrorCodes.AUDIENCE_MANAGER_ERROR ); } } diff --git a/src/batchUploader.ts b/src/batchUploader.ts index b9b598d9..36831e5d 100644 --- a/src/batchUploader.ts +++ b/src/batchUploader.ts @@ -15,6 +15,7 @@ import { IMParticleUser } from './identity-user-interfaces'; import { IMParticleWebSDKInstance } from './mp-instance'; import { appendUserInfo } from './user-utils'; import { IntegrationAttributes } from './store'; +import { ErrorCodes } from './logging/errorCodes'; /** * BatchUploader contains all the logic to store/retrieve events and batches * to/from persistence, and upload batches to mParticle. @@ -489,13 +490,15 @@ export class BatchUploader { response.status === 429 ) { logger.error( - `HTTP error status ${response.status} received` + `HTTP error status ${response.status} received`, + ErrorCodes.BATCH_UPLOADER_ERROR ); // Server error, add back current batches and try again later return uploads.slice(i, uploads.length); } else if (response.status >= 401) { logger.error( - `HTTP error status ${response.status} while uploading - please verify your API key.` + `HTTP error status ${response.status} while uploading - please verify your API key.`, + ErrorCodes.BATCH_UPLOADER_ERROR ); //if we're getting a 401, assume we'll keep getting a 401 and clear the uploads. return null; @@ -512,7 +515,8 @@ export class BatchUploader { } } catch (e) { logger.error( - `Error sending event to mParticle servers. ${e}` + `Error sending event to mParticle servers. ${e}`, + ErrorCodes.BATCH_UPLOADER_ERROR ); return uploads.slice(i, uploads.length); } diff --git a/src/consent.ts b/src/consent.ts index 81ae944c..e36dcdb8 100644 --- a/src/consent.ts +++ b/src/consent.ts @@ -9,6 +9,7 @@ import KitFilterHelper from './kitFilterHelper'; import Constants from './constants'; import { IMParticleUser } from './identity-user-interfaces'; import { IMParticleWebSDKInstance } from './mp-instance'; +import { ErrorCodes } from './logging/errorCodes'; const { CCPAPurpose } = Constants; @@ -183,31 +184,36 @@ export default function Consent(this: IConsent, mpInstance: IMParticleWebSDKInst ): PrivacyConsentState | null { if (typeof consented !== 'boolean') { mpInstance.Logger.error( - 'Consented boolean is required when constructing a Consent object.' + 'Consented boolean is required when constructing a Consent object.', + ErrorCodes.CONSENT_ERROR ); return null; } if (timestamp && isNaN(timestamp)) { mpInstance.Logger.error( - 'Timestamp must be a valid number when constructing a Consent object.' + 'Timestamp must be a valid number when constructing a Consent object.', + ErrorCodes.CONSENT_ERROR ); return null; } if (consentDocument && typeof consentDocument !== 'string') { mpInstance.Logger.error( - 'Document must be a valid string when constructing a Consent object.' + 'Document must be a valid string when constructing a Consent object.', + ErrorCodes.CONSENT_ERROR ); return null; } if (location && typeof location !== 'string') { mpInstance.Logger.error( - 'Location must be a valid string when constructing a Consent object.' + 'Location must be a valid string when constructing a Consent object.', + ErrorCodes.CONSENT_ERROR ); return null; } if (hardwareId && typeof hardwareId !== 'string') { mpInstance.Logger.error( - 'Hardware ID must be a valid string when constructing a Consent object.' + 'Hardware ID must be a valid string when constructing a Consent object.', + ErrorCodes.CONSENT_ERROR ); return null; } @@ -381,13 +387,14 @@ export default function Consent(this: IConsent, mpInstance: IMParticleWebSDKInst ): ConsentState { const normalizedPurpose = canonicalizeForDeduplication(purpose); if (!normalizedPurpose) { - mpInstance.Logger.error('Purpose must be a string.'); + mpInstance.Logger.error('Purpose must be a string.', ErrorCodes.CONSENT_ERROR); return this; } if (!isObject(gdprConsent)) { mpInstance.Logger.error( - 'Invoked with a bad or empty consent object.' + 'Invoked with a bad or empty consent object.', + ErrorCodes.CONSENT_ERROR ); return this; } @@ -466,7 +473,8 @@ export default function Consent(this: IConsent, mpInstance: IMParticleWebSDKInst ) { if (!isObject(ccpaConsent)) { mpInstance.Logger.error( - 'Invoked with a bad or empty CCPA consent object.' + 'Invoked with a bad or empty CCPA consent object.', + ErrorCodes.CONSENT_ERROR ); return this; } diff --git a/src/ecommerce.js b/src/ecommerce.js index c492e102..0c0c5b6a 100644 --- a/src/ecommerce.js +++ b/src/ecommerce.js @@ -1,5 +1,6 @@ import Types from './types'; import Constants from './constants'; +import { ErrorCodes } from './logging/errorCodes'; var Messages = Constants.Messages; @@ -116,7 +117,8 @@ export default function Ecommerce(mpInstance) { mpInstance.Logger.error( 'Could not convert product action type ' + productActionType + - ' to event type' + ' to event type', + ErrorCodes.ECOMMERCE_ERROR ); return null; } @@ -132,7 +134,8 @@ export default function Ecommerce(mpInstance) { mpInstance.Logger.error( 'Could not convert promotion action type ' + promotionActionType + - ' to event type' + ' to event type', + ErrorCodes.ECOMMERCE_ERROR ); return null; } @@ -262,20 +265,25 @@ export default function Ecommerce(mpInstance) { attributes = mpInstance._Helpers.sanitizeAttributes(attributes, name); if (typeof name !== 'string') { - mpInstance.Logger.error('Name is required when creating a product'); + mpInstance.Logger.error( + 'Name is required when creating a product', + ErrorCodes.ECOMMERCE_ERROR + ); return null; } if (!mpInstance._Helpers.Validators.isStringOrNumber(sku)) { mpInstance.Logger.error( - 'SKU is required when creating a product, and must be a string or a number' + 'SKU is required when creating a product, and must be a string or a number', + ErrorCodes.ECOMMERCE_ERROR ); return null; } if (!mpInstance._Helpers.Validators.isStringOrNumber(price)) { mpInstance.Logger.error( - 'Price is required when creating a product, and must be a string or a number' + 'Price is required when creating a product, and must be a string or a number', + ErrorCodes.ECOMMERCE_ERROR ); return null; } else { @@ -284,7 +292,8 @@ export default function Ecommerce(mpInstance) { if (position && !mpInstance._Helpers.Validators.isNumber(position)) { mpInstance.Logger.error( - 'Position must be a number, it will be set to null.' + 'Position must be a number, it will be set to null.', + ErrorCodes.ECOMMERCE_ERROR ); position = null; } @@ -312,7 +321,10 @@ export default function Ecommerce(mpInstance) { this.createPromotion = function(id, creative, name, position) { if (!mpInstance._Helpers.Validators.isStringOrNumber(id)) { - mpInstance.Logger.error(Messages.ErrorMessages.PromotionIdRequired); + mpInstance.Logger.error( + Messages.ErrorMessages.PromotionIdRequired, + ErrorCodes.ECOMMERCE_ERROR + ); return null; } @@ -327,14 +339,16 @@ export default function Ecommerce(mpInstance) { this.createImpression = function(name, product) { if (typeof name !== 'string') { mpInstance.Logger.error( - 'Name is required when creating an impression.' + 'Name is required when creating an impression.', + ErrorCodes.ECOMMERCE_ERROR ); return null; } if (!product) { mpInstance.Logger.error( - 'Product is required when creating an impression.' + 'Product is required when creating an impression.', + ErrorCodes.ECOMMERCE_ERROR ); return null; } @@ -355,7 +369,8 @@ export default function Ecommerce(mpInstance) { ) { if (!mpInstance._Helpers.Validators.isStringOrNumber(id)) { mpInstance.Logger.error( - Messages.ErrorMessages.TransactionIdRequired + Messages.ErrorMessages.TransactionIdRequired, + ErrorCodes.ECOMMERCE_ERROR ); return null; } diff --git a/src/events.js b/src/events.js index 73969b5d..33c3e303 100644 --- a/src/events.js +++ b/src/events.js @@ -1,5 +1,6 @@ import Types from './types'; import Constants from './constants'; +import { ErrorCodes } from './logging/errorCodes'; var Messages = Constants.Messages; @@ -67,9 +68,10 @@ export default function Events(mpInstance) { } } catch (e) { mpInstance.Logger.error( - 'Error invoking the callback passed to startTrackingLocation.' + 'Error invoking the callback passed to startTrackingLocation.', + ErrorCodes.EVENTS_ERROR ); - mpInstance.Logger.error(e); + mpInstance.Logger.error(e, ErrorCodes.EVENTS_ERROR); } } } @@ -224,7 +226,10 @@ export default function Events(mpInstance) { customFlags ) { if (!transactionAttributes) { - mpInstance.Logger.error(Messages.ErrorMessages.TransactionRequired); + mpInstance.Logger.error( + Messages.ErrorMessages.TransactionRequired, + ErrorCodes.EVENTS_ERROR + ); return; } @@ -329,7 +334,8 @@ export default function Events(mpInstance) { commerceEvent.EventCategory === null ) { mpInstance.Logger.error( - 'Commerce event not sent. The mParticle.ProductActionType you passed was invalid. Re-check your code.' + 'Commerce event not sent. The mParticle.ProductActionType you passed was invalid. Re-check your code.', + ErrorCodes.EVENTS_ERROR ); return; } @@ -414,7 +420,10 @@ export default function Events(mpInstance) { i; if (!selector) { - mpInstance.Logger.error("Can't bind event, selector is required"); + mpInstance.Logger.error( + "Can't bind event, selector is required", + ErrorCodes.EVENTS_ERROR + ); return; } diff --git a/src/forwarders.js b/src/forwarders.js index be03512b..4c1674ef 100644 --- a/src/forwarders.js +++ b/src/forwarders.js @@ -7,6 +7,8 @@ import APIClient from './apiClient'; const { Modify, Identify, Login, Logout } = Constants.IdentityMethods; +import { ErrorCodes } from './logging/errorCodes'; + export default function Forwarders(mpInstance, kitBlocker) { var self = this; this.forwarderStatsUploader = new APIClient( @@ -443,7 +445,7 @@ export default function Forwarders(mpInstance, kitBlocker) { mpInstance.Logger.verbose(result); } } catch (e) { - mpInstance.Logger.error(e); + mpInstance.Logger.error(e, ErrorCodes.FORWARDERS_ERROR); } }); }; @@ -592,7 +594,8 @@ export default function Forwarders(mpInstance, kitBlocker) { } catch (e) { mpInstance.Logger.error( 'MP Kits not configured propertly. Kits may not be initialized. ' + - e + e, + ErrorCodes.FORWARDERS_ERROR ); } }; @@ -702,7 +705,8 @@ export default function Forwarders(mpInstance, kitBlocker) { } catch (e) { mpInstance.Logger.error( 'Sideloaded Kits not configured propertly. Kits may not be initialized. ' + - e + e, + ErrorCodes.FORWARDERS_ERROR ); } }; @@ -772,7 +776,8 @@ export default function Forwarders(mpInstance, kitBlocker) { } catch (e) { mpInstance.Logger.error( 'Cookie Sync configs not configured propertly. Cookie Sync may not be initialized. ' + - e + e, + ErrorCodes.FORWARDERS_ERROR ); } }; diff --git a/src/helpers.js b/src/helpers.js index 3eec10db..b7bb30fe 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -3,6 +3,7 @@ import Constants from './constants'; import * as utils from './utils'; import Validators from './validators'; import KitFilterHelper from './kitFilterHelper'; +import { ErrorCodes } from './logging/errorCodes'; var StorageNames = Constants.StorageNames; @@ -73,7 +74,8 @@ export default function Helpers(mpInstance) { } } catch (e) { mpInstance.Logger.error( - 'There was an error with your callback: ' + e + 'There was an error with your callback: ' + e, + ErrorCodes.HELPERS_CALLBACK_ERROR ); } }; @@ -94,7 +96,8 @@ export default function Helpers(mpInstance) { } } catch (e) { mpInstance.Logger.error( - 'There was an error with your callback: ' + e + 'There was an error with your callback: ' + e, + ErrorCodes.HELPERS_CALLBACK_ERROR ); } }; @@ -256,7 +259,10 @@ export default function Helpers(mpInstance) { try { xhr = new window.XMLHttpRequest(); } catch (e) { - mpInstance.Logger.error('Error creating XMLHttpRequest object.'); + mpInstance.Logger.error( + 'Error creating XMLHttpRequest object.', + ErrorCodes.HELPERS_XHR_ERROR + ); } if (xhr && cb && 'withCredentials' in xhr) { @@ -268,7 +274,10 @@ export default function Helpers(mpInstance) { xhr = new window.XDomainRequest(); xhr.onload = cb; } catch (e) { - mpInstance.Logger.error('Error creating XDomainRequest object'); + mpInstance.Logger.error( + 'Error creating XDomainRequest object', + ErrorCodes.HELPERS_XHR_ERROR + ); } } diff --git a/src/identity.js b/src/identity.js index a4dba476..d92e1d3a 100644 --- a/src/identity.js +++ b/src/identity.js @@ -18,6 +18,7 @@ import { } from './utils'; import { hasMPIDAndUserLoginChanged, hasMPIDChanged } from './user-utils'; import { processReadyQueue } from './pre-init-utils'; +import { ErrorCodes } from './logging/errorCodes'; export default function Identity(mpInstance) { const { getFeatureFlag, extend } = mpInstance._Helpers; @@ -46,7 +47,8 @@ export default function Identity(mpInstance) { if (!identityValidationResult.valid) { mpInstance.Logger.error( - 'ERROR: ' + identityValidationResult.error + 'ERROR: ' + identityValidationResult.error, + ErrorCodes.IDENTITY_ERROR ); return { valid: false, @@ -61,7 +63,7 @@ export default function Identity(mpInstance) { var error = 'The optional callback must be a function. You tried entering a(n) ' + typeof callback; - mpInstance.Logger.error(error); + mpInstance.Logger.error(error, ErrorCodes.IDENTITY_ERROR); return { valid: false, error: error, @@ -748,7 +750,8 @@ export default function Identity(mpInstance) { try { if (!destinationUser || !sourceUser) { mpInstance.Logger.error( - "'destinationUser' and 'sourceUser' must both be present" + "'destinationUser' and 'sourceUser' must both be present", + ErrorCodes.IDENTITY_ERROR ); return null; } @@ -791,7 +794,8 @@ export default function Identity(mpInstance) { }; } catch (e) { mpInstance.Logger.error( - 'There was a problem with creating an alias request: ' + e + 'There was a problem with creating an alias request: ' + e, + ErrorCodes.IDENTITY_ERROR ); return null; } @@ -845,7 +849,10 @@ export default function Identity(mpInstance) { */ setUserTag: function(tagName) { if (!mpInstance._Helpers.Validators.isValidKeyValue(tagName)) { - mpInstance.Logger.error(Messages.ErrorMessages.BadKey); + mpInstance.Logger.error( + Messages.ErrorMessages.BadKey, + ErrorCodes.IDENTITY_ERROR + ); return; } @@ -858,7 +865,10 @@ export default function Identity(mpInstance) { */ removeUserTag: function(tagName) { if (!mpInstance._Helpers.Validators.isValidKeyValue(tagName)) { - mpInstance.Logger.error(Messages.ErrorMessages.BadKey); + mpInstance.Logger.error( + Messages.ErrorMessages.BadKey, + ErrorCodes.IDENTITY_ERROR + ); return; } @@ -882,13 +892,17 @@ export default function Identity(mpInstance) { ) ) { mpInstance.Logger.error( - Messages.ErrorMessages.BadAttribute + Messages.ErrorMessages.BadAttribute, + ErrorCodes.IDENTITY_ERROR ); return; } if (!mpInstance._Helpers.Validators.isValidKeyValue(key)) { - mpInstance.Logger.error(Messages.ErrorMessages.BadKey); + mpInstance.Logger.error( + Messages.ErrorMessages.BadKey, + ErrorCodes.IDENTITY_ERROR + ); return; } if (mpInstance._Store.webviewBridgeEnabled) { @@ -961,7 +975,8 @@ export default function Identity(mpInstance) { } else { mpInstance.Logger.error( 'Must pass an object into setUserAttributes. You passed a ' + - typeof userAttributes + typeof userAttributes, + ErrorCodes.IDENTITY_ERROR ); } }, @@ -975,7 +990,10 @@ export default function Identity(mpInstance) { mpInstance._SessionManager.resetSessionTimer(); if (!mpInstance._Helpers.Validators.isValidKeyValue(key)) { - mpInstance.Logger.error(Messages.ErrorMessages.BadKey); + mpInstance.Logger.error( + Messages.ErrorMessages.BadKey, + ErrorCodes.IDENTITY_ERROR + ); return; } @@ -1040,14 +1058,18 @@ export default function Identity(mpInstance) { mpInstance._SessionManager.resetSessionTimer(); if (!mpInstance._Helpers.Validators.isValidKeyValue(key)) { - mpInstance.Logger.error(Messages.ErrorMessages.BadKey); + mpInstance.Logger.error( + Messages.ErrorMessages.BadKey, + ErrorCodes.IDENTITY_ERROR + ); return; } if (!Array.isArray(newValue)) { mpInstance.Logger.error( 'The value you passed in to setUserAttributeList must be an array. You passed in a ' + - typeof value + typeof value, + ErrorCodes.IDENTITY_ERROR ); return; } @@ -1262,7 +1284,8 @@ export default function Identity(mpInstance) { ) ) { mpInstance.Logger.error( - ErrorMessages.AudienceAPINotEnabled + ErrorMessages.AudienceAPINotEnabled, + ErrorCodes.IDENTITY_ERROR ); return; } @@ -1553,7 +1576,8 @@ export default function Identity(mpInstance) { 'Received HTTP response code of ' + identityResponse.status + ' - ' + - identityApiResult.errors[0].message + identityApiResult.errors[0].message, + ErrorCodes.IDENTITY_ERROR ); } @@ -1570,7 +1594,8 @@ export default function Identity(mpInstance) { ); } mpInstance.Logger.error( - 'Error parsing JSON response from Identity server: ' + e + 'Error parsing JSON response from Identity server: ' + e, + ErrorCodes.IDENTITY_ERROR ); } mpInstance._Store.isInitialized = true; @@ -1740,7 +1765,8 @@ function tryOnUserAlias(previousUser, newUser, identityApiData, logger) { identityApiData.onUserAlias(previousUser, newUser); } catch (e) { logger.error( - 'There was an error with your onUserAlias function - ' + e + 'There was an error with your onUserAlias function - ' + e, + ErrorCodes.IDENTITY_ERROR ); } } diff --git a/src/identityApiClient.ts b/src/identityApiClient.ts index e357a645..01d060ae 100644 --- a/src/identityApiClient.ts +++ b/src/identityApiClient.ts @@ -25,6 +25,7 @@ import { IIdentityResponse, } from './identity-user-interfaces'; import { IMParticleWebSDKInstance } from './mp-instance'; +import { ErrorCodes } from './logging/errorCodes'; const { HTTPCodes, Messages, IdentityMethods } = Constants; @@ -175,7 +176,10 @@ export default function IdentityAPIClient( invokeAliasCallback(aliasCallback, response.status, errorMessage); } catch (e) { const errorMessage = (e as Error).message || e.toString(); - Logger.error('Error sending alias request to mParticle servers. ' + errorMessage); + Logger.error( + 'Error sending alias request to mParticle servers. ' + errorMessage, + ErrorCodes.IDENTITY_API_CLIENT_ERROR + ); invokeAliasCallback( aliasCallback, HTTPCodes.noHttpCoverage, @@ -197,7 +201,7 @@ export default function IdentityAPIClient( const { Logger } = mpInstance; Logger.verbose(Messages.InformationMessages.SendIdentityBegin); if (!identityApiRequest) { - Logger.error(Messages.ErrorMessages.APIRequestEmpty); + Logger.error(Messages.ErrorMessages.APIRequestEmpty, ErrorCodes.IDENTITY_API_CLIENT_ERROR); return; } Logger.verbose(Messages.InformationMessages.SendIdentityHttp); @@ -303,7 +307,7 @@ export default function IdentityAPIClient( const errorMessage = (err as Error).message || err.toString(); - Logger.error('Error sending identity request to servers' + ' - ' + errorMessage); + Logger.error('Error sending identity request to servers' + ' - ' + errorMessage, ErrorCodes.IDENTITY_API_CLIENT_ERROR); invokeCallback( callback, HTTPCodes.noHttpCoverage, diff --git a/src/kitBlocking.ts b/src/kitBlocking.ts index 17622fac..71531738 100644 --- a/src/kitBlocking.ts +++ b/src/kitBlocking.ts @@ -4,6 +4,7 @@ import { BaseEvent, EventTypeEnum, CommerceEvent, ScreenViewEvent, CustomEvent } import Types from './types' import { DataPlanPoint } from '@mparticle/data-planning-models'; import { IMParticleWebSDKInstance } from './mp-instance'; +import { ErrorCodes } from './logging/errorCodes'; /* TODO: Including this as a workaround because attempting to import it from @@ -66,14 +67,14 @@ export default class KitBlocker { } } catch(e) { - this.mpInstance.Logger.error('There was an issue with the data plan: ' + e); + this.mpInstance.Logger.error('There was an issue with the data plan: ' + e, ErrorCodes.KIT_BLOCKING); } } } addToMatchLookups(point: DataPlanPoint) { if (!point.match || !point.validator) { - this.mpInstance.Logger.warning(`Data Plan Point is not valid' + ${point}`); + this.mpInstance.Logger.warning(`Data Plan Point is not valid' + ${point}`, ErrorCodes.KIT_BLOCKING); return; } diff --git a/src/logger.ts b/src/logger.ts index 6ec19c05..8715f4b9 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,4 +1,6 @@ import { LogLevelType, SDKInitConfig, SDKLoggerApi } from './sdkRuntimeModels'; +import { IReportingLogger } from './logging/reportingLogger'; +import { ErrorCodes } from './logging/errorCodes'; export type ILoggerConfig = Pick; export type IConsoleLogger = Partial>; @@ -6,10 +8,14 @@ export type IConsoleLogger = Partial { if(!(message.methodName in this) || !isFunction(this[message.methodName])) { - this.logger?.error(`RoktManager: Method ${message.methodName} not found`); + this.logger?.error(`RoktManager: Method ${message.methodName} not found`, ErrorCodes.ROKT_MANAGER); return; } diff --git a/src/sdkRuntimeModels.ts b/src/sdkRuntimeModels.ts index e1dc97bb..a4c5d88c 100644 --- a/src/sdkRuntimeModels.ts +++ b/src/sdkRuntimeModels.ts @@ -41,7 +41,8 @@ import { IErrorLogMessage, IMParticleWebSDKInstance, IntegrationDelays } from '. import Constants from './constants'; import RoktManager, { IRoktLauncherOptions } from './roktManager'; import { IConsoleLogger } from './logger'; - +import { ErrorCodes } from './logging/errorCodes'; +import { IReportingLogger } from './logging/reportingLogger'; // TODO: Resolve this with version in @mparticle/web-sdk export type SDKEventCustomFlags = Dictionary; @@ -179,6 +180,7 @@ export interface MParticleWebSDK { MPConfig?: SDKInitConfig, keepPersistence?: boolean, instance?: IMParticleWebSDKInstance, + reportingLogger?: IReportingLogger, ): void; configurePixel(config: IPixelConfiguration): void; endSession(): void; @@ -365,9 +367,9 @@ export interface SDKHelpersApi { } export interface SDKLoggerApi { - error(arg0: string): void; + error(arg0: string, code?: ErrorCodes): void; verbose(arg0: string): void; - warning(arg0: string): void; + warning(arg0: string, code?: ErrorCodes): void; setLogLevel(logLevel: LogLevelType): void; } diff --git a/src/store.ts b/src/store.ts index 43e1b413..a3d28676 100644 --- a/src/store.ts +++ b/src/store.ts @@ -42,6 +42,7 @@ import { import { CookieSyncDates, IPixelConfiguration } from './cookieSyncManager'; import { IMParticleWebSDKInstance } from './mp-instance'; import ForegroundTimer from './foregroundTimeTracker'; +import { ErrorCodes } from './logging/errorCodes'; const { Messages } = Constants; @@ -439,7 +440,8 @@ export default function Store( this.SDKConfig.dataPlan.PlanId = dataPlan.planId; } else { mpInstance.Logger.error( - 'Your data plan id must be a string and match the data plan slug format (i.e. under_case_slug)' + 'Your data plan id must be a string and match the data plan slug format (i.e. under_case_slug)', + ErrorCodes.STORE ); } } @@ -449,7 +451,8 @@ export default function Store( this.SDKConfig.dataPlan.PlanVersion = dataPlan.planVersion; } else { mpInstance.Logger.error( - 'Your data plan version must be a number' + 'Your data plan version must be a number', + ErrorCodes.STORE ); } } @@ -490,7 +493,8 @@ export default function Store( !dataPlanOptions.hasOwnProperty('blockUserIdentities') ) { mpInstance.Logger.error( - 'Ensure your config.dataPlanOptions object has the following keys: a "dataPlanVersion" object, and "blockUserAttributes", "blockEventAttributes", "blockEvents", "blockUserIdentities" booleans' + 'Ensure your config.dataPlanOptions object has the following keys: a "dataPlanVersion" object, and "blockUserAttributes", "blockEventAttributes", "blockEvents", "blockUserIdentities" booleans', + ErrorCodes.STORE ); } } @@ -500,7 +504,8 @@ export default function Store( this.SDKConfig.onCreateBatch = config.onCreateBatch; } else { mpInstance.Logger.error( - 'config.onCreateBatch must be a function' + 'config.onCreateBatch must be a function', + ErrorCodes.STORE ); // set to undefined because all items are set on createSDKConfig this.SDKConfig.onCreateBatch = undefined; @@ -754,7 +759,8 @@ export default function Store( } } else { mpInstance.Logger.warning( - 'You should have a workspaceToken on your config object for security purposes.' + 'You should have a workspaceToken on your config object for security purposes.', + ErrorCodes.STORE ); } // add a new function to apply items to the store that require config to be returned diff --git a/test/jest/logger.spec.ts b/test/jest/logger.spec.ts index def3a637..efb17b80 100644 --- a/test/jest/logger.spec.ts +++ b/test/jest/logger.spec.ts @@ -1,9 +1,12 @@ import { Logger, ConsoleLogger } from '../../src/logger'; import { LogLevelType } from '../../src/sdkRuntimeModels'; +import { IReportingLogger } from '../../src/logging/reportingLogger'; +import { ErrorCodes } from '../../src/logging/errorCodes'; describe('Logger', () => { let mockConsole: any; let logger: Logger; + let mockReportingLogger: IReportingLogger; beforeEach(() => { mockConsole = { @@ -12,6 +15,11 @@ describe('Logger', () => { error: jest.fn() }; (global as any).console = mockConsole; + + mockReportingLogger = { + error: jest.fn(), + warning: jest.fn() + }; }); afterEach(() => { @@ -19,31 +27,35 @@ describe('Logger', () => { }); it('should call verbose, warning, and error methods on ConsoleLogger at correct log levels', () => { - logger = new Logger({ logLevel: LogLevelType.Verbose }); + logger = new Logger({ logLevel: LogLevelType.Verbose }, mockReportingLogger); logger.verbose('message1'); - logger.warning('message2'); - logger.error('message3'); + logger.warning('message2', ErrorCodes.UNHANDLED_EXCEPTION); + logger.error('message3', ErrorCodes.API_CLIENT_ERROR); expect(mockConsole.info).toHaveBeenCalledWith('message1'); expect(mockConsole.warn).toHaveBeenCalledWith('message2'); expect(mockConsole.error).toHaveBeenCalledWith('message3'); + expect(mockReportingLogger.warning).toHaveBeenCalledWith('message2', ErrorCodes.UNHANDLED_EXCEPTION); + expect(mockReportingLogger.error).toHaveBeenCalledWith('message3', ErrorCodes.API_CLIENT_ERROR); }); it('should only call warning and error at warning log level', () => { - logger = new Logger({ logLevel: LogLevelType.Warning }); + logger = new Logger({ logLevel: LogLevelType.Warning }, mockReportingLogger); logger.verbose('message1'); - logger.warning('message2'); - logger.error('message3'); + logger.warning('message2', ErrorCodes.IDENTITY_ERROR); + logger.error('message3', ErrorCodes.BATCH_UPLOADER_ERROR); expect(mockConsole.info).not.toHaveBeenCalled(); expect(mockConsole.warn).toHaveBeenCalledWith('message2'); expect(mockConsole.error).toHaveBeenCalledWith('message3'); + expect(mockReportingLogger.warning).toHaveBeenCalledWith('message2', ErrorCodes.IDENTITY_ERROR); + expect(mockReportingLogger.error).toHaveBeenCalledWith('message3', ErrorCodes.BATCH_UPLOADER_ERROR); }); it('should not call any log methods at none log level', () => { - logger = new Logger({ logLevel: LogLevelType.None }); + logger = new Logger({ logLevel: LogLevelType.None }, mockReportingLogger); logger.verbose('message1'); logger.warning('message2'); @@ -52,18 +64,22 @@ describe('Logger', () => { expect(mockConsole.info).not.toHaveBeenCalled(); expect(mockConsole.warn).not.toHaveBeenCalled(); expect(mockConsole.error).not.toHaveBeenCalled(); + expect(mockReportingLogger.warning).not.toHaveBeenCalled(); + expect(mockReportingLogger.error).not.toHaveBeenCalled(); }); it('should only call error at error log level', () => { - logger = new Logger({ logLevel: LogLevelType.Error }); + logger = new Logger({ logLevel: LogLevelType.Error }, mockReportingLogger); logger.verbose('message1'); - logger.warning('message2'); - logger.error('message3'); + logger.warning('message2', ErrorCodes.CONSENT_ERROR); + logger.error('message3', ErrorCodes.PERSISTENCE_ERROR); expect(mockConsole.info).not.toHaveBeenCalled(); expect(mockConsole.warn).not.toHaveBeenCalled(); - expect(mockConsole.error).toHaveBeenCalledWith('message3'); + expect(mockConsole.error).toHaveBeenCalledWith('message3'); + expect(mockReportingLogger.warning).not.toHaveBeenCalled(); + expect(mockReportingLogger.error).toHaveBeenCalledWith('message3', ErrorCodes.PERSISTENCE_ERROR); }); it('should allow providing a custom logger', () => { @@ -73,35 +89,41 @@ describe('Logger', () => { error: jest.fn() }; - logger = new Logger({ logLevel: 'verbose' as any, logger: customLogger }); + logger = new Logger({ logLevel: 'verbose' as any, logger: customLogger }, mockReportingLogger); logger.verbose('test-verbose'); - logger.warning('test-warning'); - logger.error('test-error'); + logger.warning('test-warning', ErrorCodes.ECOMMERCE_ERROR); + logger.error('test-error', ErrorCodes.EVENTS_ERROR); expect(customLogger.verbose).toHaveBeenCalledWith('test-verbose'); expect(customLogger.warning).toHaveBeenCalledWith('test-warning'); expect(customLogger.error).toHaveBeenCalledWith('test-error'); + expect(mockReportingLogger.warning).toHaveBeenCalledWith('test-warning', ErrorCodes.ECOMMERCE_ERROR); + expect(mockReportingLogger.error).toHaveBeenCalledWith('test-error', ErrorCodes.EVENTS_ERROR); }); it('should change log level with setLogLevel', () => { - logger = new Logger({ logLevel: 'none' as any }); + logger = new Logger({ logLevel: 'none' as any }, mockReportingLogger); logger.verbose('one'); - logger.warning('two'); - logger.error('three'); + logger.warning('two', ErrorCodes.FORWARDERS_ERROR); + logger.error('three', ErrorCodes.HELPERS_CALLBACK_ERROR); expect(mockConsole.info).not.toHaveBeenCalled(); expect(mockConsole.warn).not.toHaveBeenCalled(); expect(mockConsole.error).not.toHaveBeenCalled(); + expect(mockReportingLogger.warning).not.toHaveBeenCalled(); + expect(mockReportingLogger.error).not.toHaveBeenCalled(); logger.setLogLevel('verbose' as any); logger.verbose('a'); - logger.warning('b'); - logger.error('c'); + logger.warning('b', ErrorCodes.STORE); + logger.error('c', ErrorCodes.VAULT_ERROR); expect(mockConsole.info).toHaveBeenCalledWith('a'); expect(mockConsole.warn).toHaveBeenCalledWith('b'); expect(mockConsole.error).toHaveBeenCalledWith('c'); + expect(mockReportingLogger.warning).toHaveBeenCalledWith('b', ErrorCodes.STORE); + expect(mockReportingLogger.error).toHaveBeenCalledWith('c', ErrorCodes.VAULT_ERROR); }); }); diff --git a/test/jest/roktManager.spec.ts b/test/jest/roktManager.spec.ts index 69085d04..f6fd33ea 100644 --- a/test/jest/roktManager.spec.ts +++ b/test/jest/roktManager.spec.ts @@ -1,6 +1,7 @@ import { IKitConfigs } from "../../src/configAPIClient"; import { IMParticleUser } from "../../src/identity-user-interfaces"; import { SDKIdentityApi } from "../../src/identity.interfaces"; +import { ErrorCodes } from "../../src/logging/errorCodes"; import { IMParticleWebSDKInstance } from "../../src/mp-instance"; import RoktManager, { IRoktKit, IRoktSelectPlacementsOptions } from "../../src/roktManager"; import { testMPID } from '../src/config/constants'; @@ -554,7 +555,8 @@ describe('RoktManager', () => { // Verify error was logged for non-existent method expect(mockMPInstance.Logger.error).toHaveBeenCalledWith( - 'RoktManager: Method nonExistentMethod not found' + 'RoktManager: Method nonExistentMethod not found', + ErrorCodes.ROKT_MANAGER ); // Verify message was removed from queue even though method didn't exist @@ -1030,8 +1032,10 @@ describe('RoktManager', () => { } }, expect.any(Function)); expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith( - 'Email mismatch detected. Current email differs from email passed to selectPlacements call. Proceeding to call identify with email from selectPlacements call. Please verify your implementation.' + 'Email mismatch detected. Current email differs from email passed to selectPlacements call. Proceeding to call identify with email from selectPlacements call. Please verify your implementation.', + ErrorCodes.EMAIL_MISMATCH ); + expect(mockMPInstance.Logger.error).not.toHaveBeenCalled(); }); it('should not call identify when email matches current user email', () => { @@ -1217,7 +1221,8 @@ describe('RoktManager', () => { } }, expect.any(Function)); expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith( - "emailsha256 mismatch detected. Current mParticle hashedEmail differs from hashedEmail passed to selectPlacements call. Proceeding to call identify with hashedEmail from selectPlacements call. Please verify your implementation." + "emailsha256 mismatch detected. Current mParticle hashedEmail differs from hashedEmail passed to selectPlacements call. Proceeding to call identify with hashedEmail from selectPlacements call. Please verify your implementation.", + ErrorCodes.EMAILSHA256_MISMATCH ); }); @@ -1307,7 +1312,8 @@ describe('RoktManager', () => { } }, expect.any(Function)); expect(mockMPInstance.Logger.warning).toHaveBeenCalledWith( - "emailsha256 mismatch detected. Current mParticle hashedEmail differs from hashedEmail passed to selectPlacements call. Proceeding to call identify with hashedEmail from selectPlacements call. Please verify your implementation." + "emailsha256 mismatch detected. Current mParticle hashedEmail differs from hashedEmail passed to selectPlacements call. Proceeding to call identify with hashedEmail from selectPlacements call. Please verify your implementation.", + ErrorCodes.EMAILSHA256_MISMATCH ); }); @@ -1396,7 +1402,8 @@ describe('RoktManager', () => { // Verify error was logged expect(mockMPInstance.Logger.error).toHaveBeenCalledWith( - 'Failed to identify user with new email: ' + JSON.stringify(mockError) + 'Failed to identify user with new email: ' + JSON.stringify(mockError), + ErrorCodes.ROKT_MANAGER ); // Verify selectPlacements was still called diff --git a/test/src/tests-audience-manager.ts b/test/src/tests-audience-manager.ts index db6111fc..739449fb 100644 --- a/test/src/tests-audience-manager.ts +++ b/test/src/tests-audience-manager.ts @@ -8,6 +8,8 @@ import AudienceManager, { IAudienceMemberships, IAudienceMembershipsServerResponse } from '../../src/audienceManager'; import { Logger } from '../../src/logger'; +import { IReportingLogger } from '../../src/logging/reportingLogger'; +import { ErrorCodes } from '../../src/logging/errorCodes'; import Utils from './config/utils'; const { fetchMockSuccess } = Utils; @@ -18,6 +20,11 @@ declare global { } } +const mockReportingLogger: IReportingLogger = { + error: sinon.spy(), + warning: sinon.spy() +}; + const userAudienceUrl = `https://${Constants.DefaultBaseUrls.userAudienceUrl}${apiKey}/audience`; describe('AudienceManager', () => { @@ -26,6 +33,9 @@ describe('AudienceManager', () => { fetchMockSuccess(urls.identify, { mpid: testMPID, is_logged_in: false }); + (mockReportingLogger.error as sinon.SinonSpy).resetHistory(); + (mockReportingLogger.warning as sinon.SinonSpy).resetHistory(); + window.mParticle.config.logLevel = 'error'; }); afterEach(() => { @@ -35,7 +45,7 @@ describe('AudienceManager', () => { describe('initialization', () => { it('should have proper properties on AudienceManager', () => { - const newLogger: SDKLoggerApi = new Logger(window.mParticle.config); + const newLogger: SDKLoggerApi = new Logger(window.mParticle.config, mockReportingLogger); const audienceManager = new AudienceManager( Constants.DefaultBaseUrls.userAudienceUrl, apiKey, @@ -45,6 +55,7 @@ describe('AudienceManager', () => { expect(audienceManager.logger).to.be.ok; expect(audienceManager.url).to.equal(userAudienceUrl); expect(audienceManager.userAudienceAPI).to.be.ok; + expect((mockReportingLogger.error as sinon.SinonSpy).called).to.eq(false); }); }); @@ -52,7 +63,7 @@ describe('AudienceManager', () => { let newLogger: SDKLoggerApi; let audienceManager: AudienceManager; beforeEach(() => { - newLogger = new Logger(window.mParticle.config); + newLogger = new Logger(window.mParticle.config, mockReportingLogger); audienceManager = new AudienceManager( Constants.DefaultBaseUrls.userAudienceUrl, apiKey, @@ -101,6 +112,7 @@ describe('AudienceManager', () => { expect(callback.getCall(0).lastArg).to.deep.equal( expectedAudienceMembership ); + expect((mockReportingLogger.error as sinon.SinonSpy).called).to.eq(false); }); it('should change the URL endpoint to a new MPID when switching users and attempting to retrieve audiences', async () => { @@ -159,6 +171,22 @@ describe('AudienceManager', () => { expect(callback.getCall(1).lastArg).to.deep.equal( expectedAudienceMembership2 ); + expect((mockReportingLogger.error as sinon.SinonSpy).called).to.eq(false); + }); + + it('should call reportingLogger.error when an HTTP error occurs', async () => { + const callback = sinon.spy(); + + fetchMock.get(`${userAudienceUrl}?mpid=${testMPID}`, { + status: 500, + body: JSON.stringify({ error: 'Internal Server Error' }) + }); + + await audienceManager.sendGetUserAudienceRequest(testMPID, callback); + + expect((mockReportingLogger.error as sinon.SinonSpy).calledOnce).to.eq(true); + expect((mockReportingLogger.error as sinon.SinonSpy).getCall(0).args[1]).to.eq(ErrorCodes.AUDIENCE_MANAGER_ERROR); + expect(callback.called).to.eq(false); }); }); }); \ No newline at end of file diff --git a/test/src/tests-batchUploader.ts b/test/src/tests-batchUploader.ts index 7d1a9819..a0b0ca13 100644 --- a/test/src/tests-batchUploader.ts +++ b/test/src/tests-batchUploader.ts @@ -11,6 +11,8 @@ import { BatchUploader } from '../../src/batchUploader'; import { expect } from 'chai'; import _BatchValidator from '../../src/mockBatchCreator'; import { Logger } from '../../src/logger'; +import { IReportingLogger } from '../../src/logging/reportingLogger'; +import { ErrorCodes } from '../../src/logging/errorCodes'; import { event0, event1, event2, event3 } from '../fixtures/events'; import fetchMock from 'fetch-mock/esm/client'; const { @@ -28,6 +30,11 @@ declare global { } } +const mockReportingLogger: IReportingLogger = { + error: sinon.spy(), + warning: sinon.spy() +}; + const enableBatchingConfigFlags = { eventBatchingIntervalMillis: 1000, }; @@ -46,6 +53,9 @@ describe('batch uploader', () => { window.mParticle.config.flags = { eventBatchingIntervalMillis: 1000, }; + window.mParticle.config.logLevel = 'error'; + (mockReportingLogger.error as sinon.SinonSpy).resetHistory(); + (mockReportingLogger.warning as sinon.SinonSpy).resetHistory(); }); afterEach(() => { @@ -512,7 +522,7 @@ describe('batch uploader', () => { fetchMock.post(urls.events, 200); - const newLogger = new Logger(window.mParticle.config); + const newLogger = new Logger(window.mParticle.config, mockReportingLogger); const mpInstance = window.mParticle.getInstance(); const uploader = new BatchUploader(mpInstance, 1000); @@ -545,6 +555,7 @@ describe('batch uploader', () => { expect(actualBatchResult.events.length).to.equal(1); expect(actualBatchResult.events).to.eql(actualBatch.events); + expect((mockReportingLogger.error as sinon.SinonSpy).called).to.eq(false); }); it('should return batches that fail to upload with 500 errors', async () => { @@ -554,7 +565,7 @@ describe('batch uploader', () => { fetchMock.post(urls.events, 500); - const newLogger = new Logger(window.mParticle.config); + const newLogger = new Logger(window.mParticle.config, mockReportingLogger); const mpInstance = window.mParticle.getInstance(); const uploader = new BatchUploader(mpInstance, 1000); @@ -596,6 +607,8 @@ describe('batch uploader', () => { expect( batchesNotUploaded[2].events[0].data.event_name ).to.equal('Test Event 3'); + expect((mockReportingLogger.error as sinon.SinonSpy).calledOnce).to.eq(true); + expect((mockReportingLogger.error as sinon.SinonSpy).getCall(0).args[1]).to.eq(ErrorCodes.BATCH_UPLOADER_ERROR); }); it('should return batches that fail to upload with 429 errors', async () => { @@ -605,7 +618,7 @@ describe('batch uploader', () => { fetchMock.post(urls.events, 429); - const newLogger = new Logger(window.mParticle.config); + const newLogger = new Logger(window.mParticle.config, mockReportingLogger); const mpInstance = window.mParticle.getInstance(); const uploader = new BatchUploader(mpInstance, 1000); @@ -647,6 +660,8 @@ describe('batch uploader', () => { expect( batchesNotUploaded[2].events[0].data.event_name ).to.equal('Test Event 3'); + expect((mockReportingLogger.error as sinon.SinonSpy).calledOnce).to.eq(true); + expect((mockReportingLogger.error as sinon.SinonSpy).getCall(0).args[1]).to.eq(ErrorCodes.BATCH_UPLOADER_ERROR); }); it('should return null if batches fail to upload with 401 errors', async () => { @@ -656,7 +671,7 @@ describe('batch uploader', () => { fetchMock.post(urls.events, 401); - const newLogger = new Logger(window.mParticle.config); + const newLogger = new Logger(window.mParticle.config, mockReportingLogger); const mpInstance = window.mParticle.getInstance(); const uploader = new BatchUploader(mpInstance, 1000); @@ -685,6 +700,8 @@ describe('batch uploader', () => { ); expect(batchesNotUploaded === null).to.equal(true); + expect((mockReportingLogger.error as sinon.SinonSpy).calledOnce).to.eq(true); + expect((mockReportingLogger.error as sinon.SinonSpy).getCall(0).args[1]).to.eq(ErrorCodes.BATCH_UPLOADER_ERROR); }); it('should not throw an error when upload is called while storage has not been created yet', async () => { @@ -708,7 +725,7 @@ describe('batch uploader', () => { fetchMock.post(urls.events, 400); - const newLogger = new Logger(window.mParticle.config); + const newLogger = new Logger(window.mParticle.config, mockReportingLogger); const mpInstance = window.mParticle.getInstance(); const uploader = new BatchUploader(mpInstance, 1000); @@ -737,6 +754,8 @@ describe('batch uploader', () => { ); expect(batchesNotUploaded).to.be.ok; + expect((mockReportingLogger.error as sinon.SinonSpy).calledOnce).to.eq(true); + expect((mockReportingLogger.error as sinon.SinonSpy).getCall(0).args[1]).to.eq(ErrorCodes.BATCH_UPLOADER_ERROR); expect( batchesNotUploaded.length, diff --git a/test/src/tests-eCommerce.js b/test/src/tests-eCommerce.js index 78d951b8..846ee62f 100644 --- a/test/src/tests-eCommerce.js +++ b/test/src/tests-eCommerce.js @@ -4,13 +4,20 @@ import fetchMock from 'fetch-mock/esm/client'; import { urls, apiKey, MPConfig, testMPID, ProductActionType, PromotionActionType } from './config/constants'; const { waitForCondition, fetchMockSuccess, hasIdentifyReturned } = Utils; +const mockReportingLogger = { + error: sinon.spy(), + warning: sinon.spy() +}; + const forwarderDefaultConfiguration = Utils.forwarderDefaultConfiguration, findEventFromRequest = Utils.findEventFromRequest, MockForwarder = Utils.MockForwarder; describe('eCommerce', function() { beforeEach(function() { - mParticle._resetForTests(MPConfig); + mockReportingLogger.error.resetHistory(); + mockReportingLogger.warning.resetHistory(); + mParticle._resetForTests(MPConfig, false, undefined, mockReportingLogger); delete mParticle._instances['default_instance']; fetchMock.config.overwriteRoutes = true; fetchMock.post(urls.events, 200); @@ -1343,7 +1350,7 @@ describe('eCommerce', function() { it('should deprecate add', async () => { await waitForCondition(hasIdentifyReturned); - mParticle._resetForTests(MPConfig); + mParticle._resetForTests(MPConfig, false, undefined, mockReportingLogger); const bond = sinon.spy(mParticle.getInstance().Logger, 'warning'); const product = mParticle.eCommerce.createProduct(