diff --git a/.changeset/khaki-mayflies-punch.md b/.changeset/khaki-mayflies-punch.md new file mode 100644 index 000000000..2a9da8977 --- /dev/null +++ b/.changeset/khaki-mayflies-punch.md @@ -0,0 +1,5 @@ +--- +'@segment/analytics-next': minor +--- + +Adds a new load option `disableAutoISOConversions` that turns off converting ISO strings in event fields to Dates for integrations. diff --git a/packages/browser/src/browser/__tests__/integration.test.ts b/packages/browser/src/browser/__tests__/integration.test.ts index b03740945..1d582f340 100644 --- a/packages/browser/src/browser/__tests__/integration.test.ts +++ b/packages/browser/src/browser/__tests__/integration.test.ts @@ -2,7 +2,7 @@ import { Context } from '@/core/context' import { Plugin } from '@/core/plugin' import { JSDOM } from 'jsdom' -import { Analytics } from '../../core/analytics' +import { Analytics, InitOptions } from '../../core/analytics' import { LegacyDestination } from '../../plugins/ajs-destination' import { PersistedPriorityQueue } from '../../lib/priority-queue/persisted' // @ts-ignore loadLegacySettings mocked dependency is accused as unused @@ -998,3 +998,137 @@ describe('.Integrations', () => { `) }) }) + +describe('Options', () => { + beforeEach(async () => { + jest.restoreAllMocks() + jest.resetAllMocks() + + const html = ` + + + + + + + + `.trim() + + const jsd = new JSDOM(html, { + runScripts: 'dangerously', + resources: 'usable', + url: 'https://localhost', + }) + + const windowSpy = jest.spyOn(global, 'window', 'get') + windowSpy.mockImplementation( + () => jsd.window as unknown as Window & typeof globalThis + ) + }) + + describe('disableAutoISOConversion', () => { + it('converts iso strings to dates be default', async () => { + const [analytics] = await AnalyticsBrowser.load({ + writeKey, + }) + + const amplitude = new LegacyDestination( + 'amplitude', + 'latest', + { + apiKey: AMPLITUDE_WRITEKEY, + }, + {} + ) + + await analytics.register(amplitude) + await amplitude.ready() + + const integrationMock = jest.spyOn(amplitude.integration!, 'track') + await analytics.track('Hello!', { + date: new Date(), + iso: '2020-10-10', + }) + + const [integrationEvent] = integrationMock.mock.lastCall + + expect(integrationEvent.properties()).toEqual({ + date: expect.any(Date), + iso: expect.any(Date), + }) + expect(integrationEvent.timestamp()).toBeInstanceOf(Date) + }) + + it('converts iso strings to dates be default', async () => { + const initOptions: InitOptions = { disableAutoISOConversion: false } + const [analytics] = await AnalyticsBrowser.load( + { + writeKey, + }, + initOptions + ) + + const amplitude = new LegacyDestination( + 'amplitude', + 'latest', + { + apiKey: AMPLITUDE_WRITEKEY, + }, + initOptions + ) + + await analytics.register(amplitude) + await amplitude.ready() + + const integrationMock = jest.spyOn(amplitude.integration!, 'track') + await analytics.track('Hello!', { + date: new Date(), + iso: '2020-10-10', + }) + + const [integrationEvent] = integrationMock.mock.lastCall + + expect(integrationEvent.properties()).toEqual({ + date: expect.any(Date), + iso: expect.any(Date), + }) + expect(integrationEvent.timestamp()).toBeInstanceOf(Date) + }) + + it('does not convert iso strings to dates when `true`', async () => { + const initOptions: InitOptions = { disableAutoISOConversion: true } + const [analytics] = await AnalyticsBrowser.load( + { + writeKey, + }, + initOptions + ) + + const amplitude = new LegacyDestination( + 'amplitude', + 'latest', + { + apiKey: AMPLITUDE_WRITEKEY, + }, + initOptions + ) + + await analytics.register(amplitude) + await amplitude.ready() + + const integrationMock = jest.spyOn(amplitude.integration!, 'track') + await analytics.track('Hello!', { + date: new Date(), + iso: '2020-10-10', + }) + + const [integrationEvent] = integrationMock.mock.lastCall + + expect(integrationEvent.properties()).toEqual({ + date: expect.any(Date), + iso: '2020-10-10', + }) + expect(integrationEvent.timestamp()).toBeInstanceOf(Date) + }) + }) +}) diff --git a/packages/browser/src/core/analytics/index.ts b/packages/browser/src/core/analytics/index.ts index 5414dc679..cfc26e937 100644 --- a/packages/browser/src/core/analytics/index.ts +++ b/packages/browser/src/core/analytics/index.ts @@ -67,6 +67,13 @@ export interface InitOptions { * */ disableClientPersistence?: boolean + /** + * Disables automatically converting ISO string event properties into Dates. + * ISO string to Date conversions occur right before sending events to a classic device mode integration, + * after any destination middleware have been ran. + * Defaults to `false`. + */ + disableAutoISOConversion?: boolean initialPageview?: boolean cookie?: CookieOptions user?: UserOptions diff --git a/packages/browser/src/lib/klona.ts b/packages/browser/src/lib/klona.ts deleted file mode 100644 index 4a325be7b..000000000 --- a/packages/browser/src/lib/klona.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { SegmentEvent } from '../core/events' - -export const klona = (evt: SegmentEvent): SegmentEvent => - JSON.parse(JSON.stringify(evt)) diff --git a/packages/browser/src/plugins/ajs-destination/index.ts b/packages/browser/src/plugins/ajs-destination/index.ts index 441f30408..6bf2e8d99 100644 --- a/packages/browser/src/plugins/ajs-destination/index.ts +++ b/packages/browser/src/plugins/ajs-destination/index.ts @@ -65,6 +65,7 @@ export class LegacyDestination implements Plugin { private _initialized = false private onReady: Promise | undefined private onInitialize: Promise | undefined + private disableAutoISOConversion: boolean integration: LegacyIntegration | undefined @@ -80,6 +81,7 @@ export class LegacyDestination implements Plugin { this.name = name this.version = version this.settings = { ...settings } + this.disableAutoISOConversion = options.disableAutoISOConversion || false // AJS-Renderer sets an extraneous `type` setting that clobbers // existing type defaults. We need to remove it if it's present @@ -228,7 +230,9 @@ export class LegacyDestination implements Plugin { return ctx } - const event = new clz(afterMiddleware, {}) + const event = new clz(afterMiddleware, { + traverse: !this.disableAutoISOConversion, + }) ctx.stats.increment('analytics_js.integration.invoke', 1, [ `method:${eventType}`, diff --git a/packages/browser/src/plugins/middleware/index.ts b/packages/browser/src/plugins/middleware/index.ts index 3a89953c1..2771492c7 100644 --- a/packages/browser/src/plugins/middleware/index.ts +++ b/packages/browser/src/plugins/middleware/index.ts @@ -3,7 +3,6 @@ import { SegmentEvent } from '../../core/events' import { Plugin } from '../../core/plugin' import { asPromise } from '../../lib/as-promise' import { SegmentFacade, toFacade } from '../../lib/to-facade' -import { klona } from '../../lib/klona' export interface MiddlewareParams { payload: SegmentFacade @@ -28,7 +27,11 @@ export async function applyDestinationMiddleware( evt: SegmentEvent, middleware: DestinationMiddlewareFunction[] ): Promise { - let modifiedEvent = klona(evt) + // Clone the event so mutations are localized to a single destination. + let modifiedEvent = toFacade(evt, { + clone: true, + traverse: false, + }).rawEvent() as SegmentEvent async function applyMiddleware( event: SegmentEvent, fn: DestinationMiddlewareFunction