diff --git a/src/apiClient.ts b/src/apiClient.ts index 3a6695ad2..98ee8ff29 100644 --- a/src/apiClient.ts +++ b/src/apiClient.ts @@ -110,6 +110,13 @@ export default function APIClient( mpInstance._Store.integrationDelayTimeoutStart, Date.now() ); + + // set the batch timestamp override in the store if provided as an event option + // setting this to undefined/null will cause no override to be applied + if (options.hasOwnProperty('batchTimestampUnixtimeMsOverride')) { + mpInstance._Store.batchTimestampUnixtimeMsOverride = options.batchTimestampUnixtimeMsOverride; + } + // We queue events if there is no MPID (MPID is null, or === 0), or there are integrations that that require this to stall because integration attributes // need to be set, or if we are still fetching the config (self hosted only), and so require delaying events if ( diff --git a/src/cookieSyncManager.interfaces.ts b/src/cookieSyncManager.interfaces.ts new file mode 100644 index 000000000..1f471a5b9 --- /dev/null +++ b/src/cookieSyncManager.interfaces.ts @@ -0,0 +1,23 @@ +import { MPID } from "@mparticle/web-sdk"; +import { Dictionary } from "./utils"; +import { IConsentRules } from "./consent"; + +export interface ICookieSyncManager { + attemptCookieSync: (previousMPID: MPID, mpid: MPID, mpidIsNotInCookies: boolean) => void; + performCookieSync: ( + url: string, + moduleId: number, + mpid: MPID, + cookieSyncDates: Dictionary, + filteringConsentRuleValues: IConsentRules, + mpidIsNotInCookies: boolean, + requiresConsent: boolean + ) => void; + replaceAmpWithAmpersand: (string: string) => string; + replaceMPID: (string: string, mpid: MPID) => string; + + /** + * @deprecated replaceAmp has been deprecated, use replaceAmpersandWithAmp instead + */ + replaceAmp: (string: string) => string; +} \ No newline at end of file diff --git a/src/cookieSyncManager.js b/src/cookieSyncManager.js index 0eadf9201..b5026149a 100644 --- a/src/cookieSyncManager.js +++ b/src/cookieSyncManager.js @@ -146,8 +146,15 @@ export default function cookieSyncManager(mpInstance) { }; // Private - // TODO: Rename function to replaceAmpWithAmpersand + /** + * @deprecated replaceAmp has been deprecated, use replaceAmpersandWithAmp instead + */ this.replaceAmp = function(string) { + return this.replaceAmpWithAmpersand(string); + }; + + // Private + this.replaceAmpWithAmpersand = function(string) { return string.replace(/&/g, '&'); }; diff --git a/src/mockBatchCreator.ts b/src/mockBatchCreator.ts index dfb050c36..73776ec3a 100644 --- a/src/mockBatchCreator.ts +++ b/src/mockBatchCreator.ts @@ -6,11 +6,26 @@ import { convertEvents } from './sdkToEventsApiConverter'; import * as EventsApi from '@mparticle/event-models'; import { Batch } from '@mparticle/event-models'; import { IMPSideloadedKit } from './sideloadedKit'; +import { IStore, SDKConfig } from './store'; const mockFunction = function() { return null; }; export default class _BatchValidator { + private configOverride?: Partial>; + private storeOverride?: Partial>; + + constructor({ + configOverride = {}, + storeOverride = {} + }: { + configOverride?: Partial>, + storeOverride?: Partial> + } = {}) { + this.configOverride = configOverride + this.storeOverride = storeOverride + } + private getMPInstance() { return ({ // Certain Helper, Store, and Identity properties need to be mocked to be used in the `returnBatch` method @@ -87,7 +102,9 @@ export default class _BatchValidator { SDKConfig: { isDevelopmentMode: false, onCreateBatch: mockFunction, + omitBatchTimestamp: this.configOverride?.omitBatchTimestamp, }, + batchTimestampUnixtimeMsOverride: this.storeOverride?.batchTimestampUnixtimeMsOverride }, config: null, eCommerce: null, diff --git a/src/sdkRuntimeModels.ts b/src/sdkRuntimeModels.ts index 8bf74bfbd..021f9e3be 100644 --- a/src/sdkRuntimeModels.ts +++ b/src/sdkRuntimeModels.ts @@ -31,6 +31,7 @@ import { } from './identity-user-interfaces'; import { IIdentityType } from './types.interfaces'; import IntegrationCapture from './integrationCapture'; +import { ICookieSyncManager } from './cookieSyncManager.interfaces'; // TODO: Resolve this with version in @mparticle/web-sdk export type SDKEventCustomFlags = Dictionary; @@ -158,6 +159,7 @@ export interface MParticleWebSDK { Logger: SDKLoggerApi; MPSideloadedKit: IMPSideloadedKit; _APIClient: any; // TODO: Set up API Client + _CookieSyncManager: ICookieSyncManager; _Store: IStore; _Forwarders: any; _Helpers: SDKHelpersApi; @@ -248,6 +250,7 @@ export interface SDKInitConfig workspaceToken?: string; isDevelopmentMode?: boolean; + omitBatchTimestamp?: boolean; // https://go.mparticle.com/work/SQDSDKS-6460 identityCallback?: IdentityCallback; diff --git a/src/sdkToEventsApiConverter.ts b/src/sdkToEventsApiConverter.ts index f4c51b310..155dfab2c 100644 --- a/src/sdkToEventsApiConverter.ts +++ b/src/sdkToEventsApiConverter.ts @@ -13,7 +13,7 @@ import { SDKCCPAConsentState, } from './consent'; import Types from './types'; -import { isEmpty } from './utils'; +import { isEmpty, isNumber } from './utils'; import { ISDKUserIdentity } from './identity-user-interfaces'; import { SDKIdentityTypeEnum } from './identity.interfaces'; @@ -56,10 +56,25 @@ export function convertEvents( currentConsentState = user.getConsentState(); } + // determine what timestamp, if any, to use for the batch + const { omitBatchTimestamp } = mpInstance._Store.SDKConfig; + const { batchTimestampUnixtimeMsOverride } = mpInstance._Store; + + let timestamp_unixtime_ms: number | null; + + if (isNumber(batchTimestampUnixtimeMsOverride)) { + timestamp_unixtime_ms = batchTimestampUnixtimeMsOverride; + } else if (omitBatchTimestamp) { + timestamp_unixtime_ms = null; + } else { + timestamp_unixtime_ms = new Date().getTime(); + } + + const upload: EventsApi.Batch = { source_request_id: mpInstance._Helpers.generateUniqueId(), mpid, - timestamp_unixtime_ms: new Date().getTime(), + timestamp_unixtime_ms, environment: lastEvent.Debug ? EventsApi.BatchEnvironmentEnum.development : EventsApi.BatchEnvironmentEnum.production, diff --git a/src/store.ts b/src/store.ts index d48158e10..ea09c12f2 100644 --- a/src/store.ts +++ b/src/store.ts @@ -89,6 +89,7 @@ export interface SDKConfig { webviewBridgeName?: string; workspaceToken?: string; requiredWebviewBridgeName?: string; + omitBatchTimestamp?: boolean; } function createSDKConfig(config: SDKInitConfig): SDKConfig { @@ -182,6 +183,7 @@ export interface IStore { integrationDelayTimeoutStart: number; // UNIX Timestamp webviewBridgeEnabled?: boolean; wrapperSDKInfo: WrapperSDKInfo; + batchTimestampUnixtimeMsOverride?: number persistenceData?: IPersistenceMinified; @@ -477,6 +479,10 @@ export default function Store( this.SDKConfig.onCreateBatch = undefined; } } + + if (config.hasOwnProperty('omitBatchTimestamp')) { + this.SDKConfig.omitBatchTimestamp = config.omitBatchTimestamp; + } } this._getFromPersistence = (mpid: MPID, key: string): T | null => { diff --git a/test/src/tests-apiClient.ts b/test/src/tests-apiClient.ts index 796771c74..8cf6c6e02 100644 --- a/test/src/tests-apiClient.ts +++ b/test/src/tests-apiClient.ts @@ -115,4 +115,34 @@ describe('Api Client', () => { done(); }); + + [undefined, null, new Date().getTime()].forEach( + (batchTimestampUnixtimeMsOverride) => { + it("sendEventToServer should update batchTimestampUnixtimeMsOverride", (done) => { + const event = { + messageType: Types.MessageType.PageEvent, + name: "foo page", + data: { "foo-attr": "foo-val" }, + eventType: Types.EventType.Navigation, + customFlags: { "foo-flag": "foo-flag-val" }, + }; + + const options = { batchTimestampUnixtimeMsOverride }; + + expect(mParticle.getInstance()._Store).to.be.ok; + + mParticle + .getInstance() + ._APIClient.sendEventToServer(event, options); + + expect( + mParticle.getInstance()._Store.batchTimestampUnixtimeMsOverride, + ).to.equal(batchTimestampUnixtimeMsOverride); + + done(); + }); + }, + ); + + }); \ No newline at end of file diff --git a/test/src/tests-mockBatchCreator.ts b/test/src/tests-mockBatchCreator.ts index e91154603..3163df465 100644 --- a/test/src/tests-mockBatchCreator.ts +++ b/test/src/tests-mockBatchCreator.ts @@ -3,19 +3,21 @@ import { BaseEvent } from '../../src/sdkRuntimeModels'; import { expect } from 'chai'; describe('Create a batch from a base event', () => { - const batchValidator = new _BatchValidator(); const baseEvent: BaseEvent = { messageType: 4, name: 'testEvent' } - + it('creates a batch with base event ', done => { + const now = new Date().getTime(); + const batchValidator = new _BatchValidator() + let batch = batchValidator.returnBatch(baseEvent); expect(batch).to.have.property('environment').equal('production'); expect(batch).to.have.property('source_request_id').equal('mockId'); expect(batch).to.have.property('mpid').equal('0'); - expect(batch).to.have.property('timestamp_unixtime_ms') + expect(batch).to.have.property('timestamp_unixtime_ms').greaterThanOrEqual(now); expect(batch).to.have.property('mp_deviceid'); expect(batch).to.have.property('sdk_version') expect(batch).to.have.property('application_info'); @@ -47,7 +49,7 @@ describe('Create a batch from a base event', () => { batch = batchValidator.returnBatch(baseEvent); expect(batch.events[0].data).to.have.property('custom_attributes'); expect(batch.events[0].data.custom_attributes).to.have.property('attrFoo', 'attrBar'); - + baseEvent.customFlags = { flagFoo: 'flagBar' } batch = batchValidator.returnBatch(baseEvent); expect(batch.events[0].data).to.have.property('custom_flags'); @@ -55,4 +57,63 @@ describe('Create a batch from a base event', () => { done(); }); + + [undefined, null, false, true].forEach(omitBatchTimestamp => { + it(`respects an omitBatchTimestamp config value of ${omitBatchTimestamp}`, done => { + const now = new Date().getTime(); + const batchValidator = new _BatchValidator({ + configOverride: {omitBatchTimestamp} + }); + + const batch = batchValidator.returnBatch(baseEvent); + + if (omitBatchTimestamp) { + expect(batch).to.have.property('timestamp_unixtime_ms', null); + } else { + expect(batch).to.have.property('timestamp_unixtime_ms').greaterThanOrEqual(now); + } + + done(); + }); + }) + + it(`can use a batch timestamp override value`, done => { + const oneDayAgo = new Date().getTime() - (24 * 3600 * 1000); + const batchValidator = new _BatchValidator({ + storeOverride: {batchTimestampUnixtimeMsOverride: oneDayAgo} + }); + const batch = batchValidator.returnBatch(baseEvent); + + expect(batch).to.have.property('timestamp_unixtime_ms').greaterThanOrEqual(oneDayAgo); + + done(); + }); + + it(`a batch timestamp override takes precedence over the sdk config`, done => { + const oneDayAgo = new Date().getTime() - (24 * 3600 * 1000); + const batchValidator = new _BatchValidator({ + configOverride: {omitBatchTimestamp: true}, + storeOverride: {batchTimestampUnixtimeMsOverride: oneDayAgo} + }); + const batch = batchValidator.returnBatch(baseEvent); + + expect(batch).to.have.property('timestamp_unixtime_ms').greaterThanOrEqual(oneDayAgo); + + done(); + }); + + [undefined, null].forEach(batchTimestampUnixtimeMsOverride => { + it(`a non-number batch timestamp override of ${batchTimestampUnixtimeMsOverride} is not sent`, done => { + const now = new Date().getTime(); + + const batchValidator = new _BatchValidator({ + storeOverride: {batchTimestampUnixtimeMsOverride} + }); + const batch = batchValidator.returnBatch(baseEvent); + + expect(batch).to.have.property('timestamp_unixtime_ms').greaterThanOrEqual(now); + + done(); + }); + }) }); \ No newline at end of file