diff --git a/.changeset/mean-apricots-hammer.md b/.changeset/mean-apricots-hammer.md new file mode 100644 index 000000000..f20545905 --- /dev/null +++ b/.changeset/mean-apricots-hammer.md @@ -0,0 +1,6 @@ +--- +'@segment/analytics-next': minor +'@internal/consent-tools-integration-tests': minor +--- + +Adds `bufferKey` option for setting custom global window buffers diff --git a/packages/browser/src/browser/__tests__/analytics-pre-init.integration.test.ts b/packages/browser/src/browser/__tests__/analytics-pre-init.integration.test.ts index 7fd14c276..d045cac26 100644 --- a/packages/browser/src/browser/__tests__/analytics-pre-init.integration.test.ts +++ b/packages/browser/src/browser/__tests__/analytics-pre-init.integration.test.ts @@ -278,6 +278,29 @@ describe('Pre-initialization', () => { expect(onTrackCb).toBeCalledWith('foo', {}, undefined) expect(onTrackCb).toBeCalledWith('bar', {}, undefined) }) + test('events can be buffered under a custom window key', async () => { + const onTrackCb = jest.fn() + const onTrack = ['on', 'track', onTrackCb] + const track = ['track', 'foo'] + const track2 = ['track', 'bar'] + const identify = ['identify'] + + ;(window as any).segment = [onTrack, track, track2, identify] + + await AnalyticsBrowser.standalone(writeKey, { bufferKey: 'segment' }) + + await sleep(100) // the snippet does not return a promise (pre-initialization) ... it sometimes has a callback as the third argument. + expect(trackSpy).toBeCalledWith('foo') + expect(trackSpy).toBeCalledWith('bar') + expect(trackSpy).toBeCalledTimes(2) + + expect(identifySpy).toBeCalledTimes(1) + + expect(getOnSpyCalls('track').length).toBe(1) + expect(onTrackCb).toBeCalledTimes(2) // gets called once for each track event + expect(onTrackCb).toBeCalledWith('foo', {}, undefined) + expect(onTrackCb).toBeCalledWith('bar', {}, undefined) + }) }) describe('Emitter methods', () => { diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index 8ee803b3a..c4fcd939c 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -26,6 +26,7 @@ import { popSnippetWindowBuffer } from '../core/buffer/snippet' import { ClassicIntegrationSource } from '../plugins/ajs-destination/types' import { attachInspector } from '../core/inspector' import { Stats } from '../core/stats' +import { getGlobalAnalytics } from './utils' export interface LegacyIntegrationConfiguration { /* @deprecated - This does not indicate browser types anymore */ @@ -148,9 +149,11 @@ function hasTsubMiddleware(settings: LegacySettings): boolean { */ function flushPreBuffer( analytics: Analytics, - buffer: PreInitMethodCallBuffer + buffer: PreInitMethodCallBuffer, + bufferKey?: string ): void { - buffer.push(...popSnippetWindowBuffer()) + const calls = getGlobalAnalytics(bufferKey) + buffer.push(...popSnippetWindowBuffer(calls)) flushSetAnonymousID(analytics, buffer) flushOn(analytics, buffer) } @@ -160,13 +163,15 @@ function flushPreBuffer( */ async function flushFinalBuffer( analytics: Analytics, - buffer: PreInitMethodCallBuffer + buffer: PreInitMethodCallBuffer, + bufferKey?: string ): Promise { + const calls = getGlobalAnalytics(bufferKey) // Call popSnippetWindowBuffer before each flush task since there may be // analytics calls during async function calls. - buffer.push(...popSnippetWindowBuffer()) + buffer.push(...popSnippetWindowBuffer(calls)) await flushAddSourceMiddleware(analytics, buffer) - buffer.push(...popSnippetWindowBuffer()) + buffer.push(...popSnippetWindowBuffer(calls)) flushAnalyticsCallsInNewTask(analytics, buffer) // Clear buffer, just in case analytics is loaded twice; we don't want to fire events off again. buffer.clear() @@ -312,7 +317,7 @@ async function loadAnalytics( Stats.initRemoteMetrics(legacySettings.metrics) // needs to be flushed before plugins are registered - flushPreBuffer(analytics, preInitBuffer) + flushPreBuffer(analytics, preInitBuffer, options.bufferKey) const ctx = await registerPlugins( settings.writeKey, @@ -340,7 +345,7 @@ async function loadAnalytics( analytics.page().catch(console.error) } - await flushFinalBuffer(analytics, preInitBuffer) + await flushFinalBuffer(analytics, preInitBuffer, options.bufferKey) return [analytics, ctx] } diff --git a/packages/browser/src/browser/standalone-analytics.ts b/packages/browser/src/browser/standalone-analytics.ts index b89488006..a0bee2e0b 100644 --- a/packages/browser/src/browser/standalone-analytics.ts +++ b/packages/browser/src/browser/standalone-analytics.ts @@ -1,30 +1,16 @@ -import { Analytics, InitOptions } from '../core/analytics' import { AnalyticsBrowser } from '.' import { embeddedWriteKey } from '../lib/embedded-write-key' - -export interface AnalyticsSnippet extends AnalyticsStandalone { - load: (writeKey: string, options?: InitOptions) => void -} - -export interface AnalyticsStandalone extends Analytics { - _loadOptions?: InitOptions - _writeKey?: string - _cdn?: string -} - -declare global { - interface Window { - analytics: AnalyticsSnippet - } -} +import { AnalyticsSnippet } from './standalone-interface' +import { getGlobalAnalytics } from './utils' function getWriteKey(): string | undefined { if (embeddedWriteKey()) { return embeddedWriteKey() } - if (window.analytics._writeKey) { - return window.analytics._writeKey + const analytics = getGlobalAnalytics() + if (analytics?._writeKey) { + return analytics._writeKey } const regex = /http.*\/analytics\.js\/v1\/([^/]*)(\/platform)?\/analytics.*/ @@ -59,7 +45,7 @@ function getWriteKey(): string | undefined { export async function install(): Promise { const writeKey = getWriteKey() - const options = window.analytics?._loadOptions ?? {} + const options = getGlobalAnalytics()?._loadOptions ?? {} if (!writeKey) { console.error( 'Failed to load Write Key. Make sure to use the latest version of the Segment snippet, which can be found in your source settings.' @@ -67,7 +53,7 @@ export async function install(): Promise { return } - window.analytics = (await AnalyticsBrowser.standalone( + ;(window as any).analytics = (await AnalyticsBrowser.standalone( writeKey, options )) as AnalyticsSnippet diff --git a/packages/browser/src/browser/standalone-interface.ts b/packages/browser/src/browser/standalone-interface.ts new file mode 100644 index 000000000..3288399ab --- /dev/null +++ b/packages/browser/src/browser/standalone-interface.ts @@ -0,0 +1,11 @@ +import { Analytics, InitOptions } from '../core/analytics' + +export interface AnalyticsSnippet extends AnalyticsStandalone { + load: (writeKey: string, options?: InitOptions) => void +} + +export interface AnalyticsStandalone extends Analytics { + _loadOptions?: InitOptions + _writeKey?: string + _cdn?: string +} diff --git a/packages/browser/src/browser/utils.ts b/packages/browser/src/browser/utils.ts new file mode 100644 index 000000000..2051aed38 --- /dev/null +++ b/packages/browser/src/browser/utils.ts @@ -0,0 +1,12 @@ +import { AnalyticsSnippet } from './standalone-interface' + +/** + * Gets the global analytics instance/buffer + * @param key name of the window property where the buffer is stored (default: analytics) + * @returns AnalyticsSnippet + */ +export function getGlobalAnalytics( + key = 'analytics' +): AnalyticsSnippet | undefined { + return (window as any)[key] +} diff --git a/packages/browser/src/core/analytics/index.ts b/packages/browser/src/core/analytics/index.ts index 0ffe4eb9b..706433db1 100644 --- a/packages/browser/src/core/analytics/index.ts +++ b/packages/browser/src/core/analytics/index.ts @@ -117,6 +117,11 @@ export interface InitOptions { * Array of high entropy Client Hints to request. These may be rejected by the user agent - only required hints should be requested. */ highEntropyValuesClientHints?: HighEntropyHint[] + /** + * Key for the global window property storing the buffered calls + * default: analytics + */ + bufferKey?: string } /* analytics-classic stubs */ @@ -489,7 +494,7 @@ export class Analytics noConflict(): Analytics { console.warn(deprecationWarning) - window.analytics = _analytics ?? this + ;(window as any).analytics = _analytics ?? this return this } diff --git a/packages/browser/src/core/buffer/snippet.ts b/packages/browser/src/core/buffer/snippet.ts index 3015dd144..b6eaf0c80 100644 --- a/packages/browser/src/core/buffer/snippet.ts +++ b/packages/browser/src/core/buffer/snippet.ts @@ -3,6 +3,7 @@ import type { PreInitMethodName, PreInitMethodParams, } from '.' +import { getGlobalAnalytics } from '../../browser/utils' export function transformSnippetCall([ methodName, @@ -29,14 +30,16 @@ type SnippetWindowBufferedMethodCall< * A list of the method calls before initialization for snippet users * For example, [["track", "foo", {bar: 123}], ["page"], ["on", "ready", function(){..}] */ -type SnippetBuffer = SnippetWindowBufferedMethodCall[] +export type SnippetBuffer = SnippetWindowBufferedMethodCall[] /** * Fetch the buffered method calls from the window object and normalize them. * This removes existing buffered calls from the window object. */ -export const popSnippetWindowBuffer = (): PreInitMethodCall[] => { - const wa = window.analytics +export const popSnippetWindowBuffer = ( + buffer: unknown = getGlobalAnalytics() +): PreInitMethodCall[] => { + const wa = buffer if (!Array.isArray(wa)) return [] const buffered = wa.splice(0, wa.length) return normalizeSnippetBuffer(buffered) diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index abe33d652..b10ab96eb 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -7,5 +7,6 @@ export * from './core/events' export * from './core/plugin' export * from './core/user' -export type { AnalyticsSnippet } from './browser/standalone-analytics' +export type { AnalyticsSnippet } from './browser/standalone-interface' export type { MiddlewareFunction } from './plugins/middleware' +export { getGlobalAnalytics } from './browser/utils' diff --git a/packages/browser/src/lib/parse-cdn.ts b/packages/browser/src/lib/parse-cdn.ts index 07ae62bb3..6776bfc9c 100644 --- a/packages/browser/src/lib/parse-cdn.ts +++ b/packages/browser/src/lib/parse-cdn.ts @@ -1,3 +1,4 @@ +import { getGlobalAnalytics } from '../browser/utils' import { embeddedWriteKey } from './embedded-write-key' const analyticsScriptRegex = @@ -20,13 +21,14 @@ const getCDNUrlFromScriptTag = (): string | undefined => { let _globalCDN: string | undefined // set globalCDN as in-memory singleton const getGlobalCDNUrl = (): string | undefined => { - const result = _globalCDN ?? window.analytics?._cdn + const result = _globalCDN ?? getGlobalAnalytics()?._cdn return result } export const setGlobalCDNUrl = (cdn: string) => { - if (window.analytics) { - window.analytics._cdn = cdn + const globalAnalytics = getGlobalAnalytics() + if (globalAnalytics) { + globalAnalytics._cdn = cdn } _globalCDN = cdn } @@ -60,7 +62,7 @@ export const getNextIntegrationsURL = () => { * @returns the path to Analytics JS 1.0 **/ export function getLegacyAJSPath(): string { - const writeKey = embeddedWriteKey() ?? window.analytics._writeKey + const writeKey = embeddedWriteKey() ?? getGlobalAnalytics()?._writeKey const scripts = Array.prototype.slice.call( document.querySelectorAll('script') diff --git a/packages/browser/src/tester/ajs-tester.ts b/packages/browser/src/tester/ajs-tester.ts index b8d034484..796bac013 100644 --- a/packages/browser/src/tester/ajs-tester.ts +++ b/packages/browser/src/tester/ajs-tester.ts @@ -1,3 +1,4 @@ +import { getGlobalAnalytics } from '../browser/utils' import { Analytics } from '../core/analytics' import { SerializedContext } from '../core/context' import mem from 'micro-memoize' @@ -13,7 +14,7 @@ function makeStub(page: playwright.Page) { ): Promise { return await page.evaluate((innerArgs) => { // @ts-ignore - return window.analytics + return getGlobalAnalytics() .register(...innerArgs) .then((ctx) => ctx.toJSON()) // @ts-ignore @@ -25,9 +26,11 @@ function makeStub(page: playwright.Page) { // @ts-expect-error const ctx = await page.evaluate((innerArgs) => { // @ts-ignore - return window.analytics.track(...innerArgs).then((ctx) => { - return ctx.toJSON() - }) + return getGlobalAnalytics() + .track(...innerArgs) + .then((ctx) => { + return ctx.toJSON() + }) // @ts-ignore }, args) @@ -38,9 +41,11 @@ function makeStub(page: playwright.Page) { ): Promise { const ctx = await page.evaluate(async (innerArgs) => { // @ts-ignore - return window.analytics.page(...innerArgs).then((ctx) => { - return ctx.toJSON() - }) + return getGlobalAnalytics() + .page(...innerArgs) + .then((ctx) => { + return ctx.toJSON() + }) // @ts-ignore }, args) @@ -52,9 +57,11 @@ function makeStub(page: playwright.Page) { ): Promise { const ctx = await page.evaluate((innerArgs) => { // @ts-ignore - return window.analytics.identify(...innerArgs).then((ctx) => { - return ctx.toJSON() - }) + return getGlobalAnalytics() + .identify(...innerArgs) + .then((ctx) => { + return ctx.toJSON() + }) // @ts-ignore }, args) diff --git a/packages/consent/consent-tools-integration-tests/src/page-bundles/onetrust/index.ts b/packages/consent/consent-tools-integration-tests/src/page-bundles/onetrust/index.ts index 7a0b2be2f..4da0704ff 100644 --- a/packages/consent/consent-tools-integration-tests/src/page-bundles/onetrust/index.ts +++ b/packages/consent/consent-tools-integration-tests/src/page-bundles/onetrust/index.ts @@ -1,4 +1,4 @@ -import { AnalyticsBrowser } from '@segment/analytics-next' +import { AnalyticsBrowser, getGlobalAnalytics } from '@segment/analytics-next' import { oneTrust } from '@segment/analytics-consent-wrapper-onetrust' export const analytics = new AnalyticsBrowser() @@ -12,4 +12,4 @@ oneTrust(analytics, { ;(window as any).analytics = analytics analytics.load({ writeKey: '9lSrez3BlfLAJ7NOChrqWtILiATiycoc' }) -void window.analytics.page().then(console.log) +void getGlobalAnalytics().page().then(console.log) diff --git a/packages/consent/consent-tools-integration-tests/src/page-bundles/snippet/index.ts b/packages/consent/consent-tools-integration-tests/src/page-bundles/snippet/index.ts index da6c6948e..7768c44c9 100644 --- a/packages/consent/consent-tools-integration-tests/src/page-bundles/snippet/index.ts +++ b/packages/consent/consent-tools-integration-tests/src/page-bundles/snippet/index.ts @@ -1,12 +1,14 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ +import { AnyAnalytics } from '@segment/analytics-consent-tools' import { oneTrust } from '@segment/analytics-consent-wrapper-onetrust' +import { getGlobalAnalytics } from '@segment/analytics-next' -oneTrust(window.analytics, { +oneTrust(getGlobalAnalytics() as AnyAnalytics, { integrationCategoryMappings: { Fullstory: ['C0001'], 'Actions Amplitude': ['C0004'], }, }) -window.analytics.load('9lSrez3BlfLAJ7NOChrqWtILiATiycoc') -window.analytics.track('Hello from the snippet') +getGlobalAnalytics()?.load('9lSrez3BlfLAJ7NOChrqWtILiATiycoc') +getGlobalAnalytics()?.track('Hello from the snippet')