From 7f4232cbdb60a4475c565e5d262b25182e47baf4 Mon Sep 17 00:00:00 2001 From: Oscar Bazaldua <511911+oscb@users.noreply.github.com> Date: Tue, 12 Sep 2023 11:56:40 -0700 Subject: [PATCH] feat: support custom global analytics key (#928) --- .changeset/mean-apricots-hammer.md | 6 +++ .../with-next-js/pages/partytown/index.tsx | 2 +- .../analytics-pre-init.integration.test.ts | 25 +++++++++ .../browser/src/browser/__tests__/cdn.test.ts | 4 +- .../src/browser/__tests__/integration.test.ts | 54 ++++++++++++++++++- .../__tests__/standalone-analytics.test.ts | 9 ++-- .../src/browser/__tests__/standalone.test.ts | 7 +-- packages/browser/src/browser/index.ts | 3 ++ .../src/browser/standalone-analytics.ts | 36 +++++-------- .../src/browser/standalone-interface.ts | 11 ++++ packages/browser/src/core/analytics/index.ts | 8 ++- packages/browser/src/core/buffer/snippet.ts | 9 ++-- packages/browser/src/index.ts | 3 +- .../src/lib/__tests__/parse-cdn.test.ts | 2 + .../src/lib/global-analytics-helper.ts | 31 +++++++++++ packages/browser/src/lib/parse-cdn.ts | 10 ++-- packages/browser/src/tester/ajs-tester.ts | 14 ++--- .../src/task/__tests__/task-group.test.ts | 26 +++++++++ 18 files changed, 210 insertions(+), 50 deletions(-) create mode 100644 .changeset/mean-apricots-hammer.md create mode 100644 packages/browser/src/browser/standalone-interface.ts create mode 100644 packages/browser/src/lib/global-analytics-helper.ts create mode 100644 packages/core/src/task/__tests__/task-group.test.ts diff --git a/.changeset/mean-apricots-hammer.md b/.changeset/mean-apricots-hammer.md new file mode 100644 index 000000000..76b8b9065 --- /dev/null +++ b/.changeset/mean-apricots-hammer.md @@ -0,0 +1,6 @@ +--- +'@segment/analytics-next': minor +'@segment/analytics-core': minor +--- + +Adds `globalAnalyticsKey` option for setting custom global window buffers diff --git a/examples/with-next-js/pages/partytown/index.tsx b/examples/with-next-js/pages/partytown/index.tsx index 26b0a5287..e9493162a 100644 --- a/examples/with-next-js/pages/partytown/index.tsx +++ b/examples/with-next-js/pages/partytown/index.tsx @@ -44,7 +44,7 @@ const WebWorker: React.FC = () => { }} value="Track!" onClick={() => { - void window.analytics.track( + void (window as any).analytics.track( 'Party Town Click', { myProp: 'hello' }, { traits: { age: 8 } } 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..10384fca6 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,31 @@ 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, { + globalAnalyticsKey: '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/__tests__/cdn.test.ts b/packages/browser/src/browser/__tests__/cdn.test.ts index 76b15b694..a1d0e3ddc 100644 --- a/packages/browser/src/browser/__tests__/cdn.test.ts +++ b/packages/browser/src/browser/__tests__/cdn.test.ts @@ -1,4 +1,4 @@ -import { AnalyticsBrowser } from '../..' +import { AnalyticsBrowser, getGlobalAnalytics } from '../..' import unfetch from 'unfetch' import { createSuccess } from '../../test-helpers/factories' import { setGlobalCDNUrl } from '../../lib/parse-cdn' @@ -45,5 +45,5 @@ it('if CDN is overridden, sets the overridden CDN global variable', async () => writeKey, cdnURL: mockCdn, }) - expect(window.analytics._cdn).toBe(mockCdn) + expect(getGlobalAnalytics()?._cdn).toBe(mockCdn) }) diff --git a/packages/browser/src/browser/__tests__/integration.test.ts b/packages/browser/src/browser/__tests__/integration.test.ts index e9b751902..f66471157 100644 --- a/packages/browser/src/browser/__tests__/integration.test.ts +++ b/packages/browser/src/browser/__tests__/integration.test.ts @@ -23,6 +23,7 @@ import { highEntropyTestData, lowEntropyTestData, } from '../../test-helpers/fixtures/client-hints' +import { getGlobalAnalytics } from '../..' let fetchCalls: ReturnType[] = [] @@ -200,7 +201,7 @@ describe('Initialization', () => { { ...xt, load: async () => { - expect(window.analytics).toBeUndefined() + expect(getGlobalAnalytics()).toBeUndefined() expect(getCDN()).toContain(overriddenCDNUrl) }, }, @@ -212,6 +213,57 @@ describe('Initialization', () => { }) }) + describe('globalAnalyticsKey', () => { + const overrideKey = 'myKey' + const buffer = { + foo: 'bar', + } + + beforeEach(() => { + ;(window as any)[overrideKey] = buffer + }) + afterEach(() => { + delete (window as any)[overrideKey] + }) + it('should default to window.analytics', async () => { + const defaultObj = { original: 'default' } + ;(window as any)['analytics'] = defaultObj + + await AnalyticsBrowser.load({ + writeKey, + plugins: [ + { + ...xt, + load: async () => { + expect(getGlobalAnalytics()).toBe(defaultObj) + }, + }, + ], + }) + expect.assertions(1) + }) + + it('should set the global window key for the analytics buffer with the setting option', async () => { + await AnalyticsBrowser.load( + { + writeKey, + plugins: [ + { + ...xt, + load: async () => { + expect(getGlobalAnalytics()).toBe(buffer) + }, + }, + ], + }, + { + globalAnalyticsKey: overrideKey, + } + ) + expect.assertions(1) + }) + }) + describe('Load options', () => { it('gets high entropy client hints if set', async () => { ;(window.navigator as any).userAgentData = { diff --git a/packages/browser/src/browser/__tests__/standalone-analytics.test.ts b/packages/browser/src/browser/__tests__/standalone-analytics.test.ts index ede23fc4b..c13c5f426 100644 --- a/packages/browser/src/browser/__tests__/standalone-analytics.test.ts +++ b/packages/browser/src/browser/__tests__/standalone-analytics.test.ts @@ -1,13 +1,14 @@ import jsdom, { JSDOM } from 'jsdom' -import { InitOptions } from '../../' +import { InitOptions, getGlobalAnalytics } from '../../' import { AnalyticsBrowser, loadLegacySettings } from '../../browser' import { snippet } from '../../tester/__fixtures__/segment-snippet' -import { install, AnalyticsStandalone } from '../standalone-analytics' +import { install } from '../standalone-analytics' import unfetch from 'unfetch' import { PersistedPriorityQueue } from '../../lib/priority-queue/persisted' import { sleep } from '../../lib/sleep' import * as Factory from '../../test-helpers/factories' import { EventQueue } from '../../core/queue/event-queue' +import { AnalyticsStandalone } from '../standalone-interface' const track = jest.fn() const identify = jest.fn() @@ -142,7 +143,7 @@ describe('standalone bundle', () => { .mockImplementation((): Promise => fetchSettings) const mockCdn = 'http://my-overridden-cdn.com' - window.analytics._cdn = mockCdn + getGlobalAnalytics()!._cdn = mockCdn await loadLegacySettings(segmentDotCom) expect(unfetch).toHaveBeenCalledWith(expect.stringContaining(mockCdn)) @@ -262,7 +263,7 @@ describe('standalone bundle', () => { // register is called after flushPreBuffer in `loadAnalytics` register.mockImplementationOnce(() => - window.analytics.track('race conditions', { foo: 'bar' }) + getGlobalAnalytics()?.track('race conditions', { foo: 'bar' }) ) await install() diff --git a/packages/browser/src/browser/__tests__/standalone.test.ts b/packages/browser/src/browser/__tests__/standalone.test.ts index a163ad8fb..0f2780b5e 100644 --- a/packages/browser/src/browser/__tests__/standalone.test.ts +++ b/packages/browser/src/browser/__tests__/standalone.test.ts @@ -4,6 +4,7 @@ import { LegacySettings } from '..' import { pWhile } from '../../lib/p-while' import { snippet } from '../../tester/__fixtures__/segment-snippet' import * as Factory from '../../test-helpers/factories' +import { getGlobalAnalytics } from '../..' const cdnResponse: LegacySettings = { integrations: { @@ -86,11 +87,11 @@ describe('standalone bundle', () => { await import('../standalone') await pWhile( - () => window.analytics?.initialized !== true, + () => getGlobalAnalytics()?.initialized !== true, () => {} ) - expect(window.analytics).not.toBeUndefined() - expect(window.analytics.initialized).toBe(true) + expect(getGlobalAnalytics()).not.toBeUndefined() + expect(getGlobalAnalytics()?.initialized).toBe(true) }) }) diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index 095b8ae99..1c59af549 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -30,6 +30,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 { setGlobalAnalyticsKey } from '../lib/global-analytics-helper' export interface LegacyIntegrationConfiguration { /* @deprecated - This does not indicate browser types anymore */ @@ -303,6 +304,8 @@ async function loadAnalytics( options: InitOptions = {}, preInitBuffer: PreInitMethodCallBuffer ): Promise<[Analytics, Context]> { + if (options.globalAnalyticsKey) + setGlobalAnalyticsKey(options.globalAnalyticsKey) // this is an ugly side-effect, but it's for the benefits of the plugins that get their cdn via getCDN() if (settings.cdnURL) setGlobalCDNUrl(settings.cdnURL) diff --git a/packages/browser/src/browser/standalone-analytics.ts b/packages/browser/src/browser/standalone-analytics.ts index b89488006..b4f52c9c1 100644 --- a/packages/browser/src/browser/standalone-analytics.ts +++ b/packages/browser/src/browser/standalone-analytics.ts @@ -1,30 +1,19 @@ -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, + setGlobalAnalytics, +} from '../lib/global-analytics-helper' 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 +48,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,8 +56,7 @@ export async function install(): Promise { return } - window.analytics = (await AnalyticsBrowser.standalone( - writeKey, - options - )) as AnalyticsSnippet + setGlobalAnalytics( + (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/core/analytics/index.ts b/packages/browser/src/core/analytics/index.ts index 767e7338e..8b88630c8 100644 --- a/packages/browser/src/core/analytics/index.ts +++ b/packages/browser/src/core/analytics/index.ts @@ -53,6 +53,7 @@ import { isArrayOfStoreType, } from '../storage' import { PluginFactory } from '../../plugins/remote-loader' +import { setGlobalAnalytics } from '../../lib/global-analytics-helper' const deprecationWarning = 'This is being deprecated and will be not be available in future releases of Analytics JS' @@ -121,6 +122,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[] + /** + * When using the snippet, this is the key that points to the global analytics instance (e.g. window.analytics). + * default: analytics + */ + globalAnalyticsKey?: string } /* analytics-classic stubs */ @@ -536,7 +542,7 @@ export class Analytics noConflict(): Analytics { console.warn(deprecationWarning) - window.analytics = _analytics ?? this + setGlobalAnalytics(_analytics ?? this) return this } diff --git a/packages/browser/src/core/buffer/snippet.ts b/packages/browser/src/core/buffer/snippet.ts index 3015dd144..73612b3f6 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 '../../lib/global-analytics-helper' 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..625d8e91a 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 './lib/global-analytics-helper' diff --git a/packages/browser/src/lib/__tests__/parse-cdn.test.ts b/packages/browser/src/lib/__tests__/parse-cdn.test.ts index 2ba92a124..78d449286 100644 --- a/packages/browser/src/lib/__tests__/parse-cdn.test.ts +++ b/packages/browser/src/lib/__tests__/parse-cdn.test.ts @@ -51,6 +51,7 @@ it('should return the overridden cdn if window.analytics._cdn is mutated', () => withTag(`