Skip to content

Commit

Permalink
feat: support custom global analytics key (#928)
Browse files Browse the repository at this point in the history
  • Loading branch information
oscb authored Sep 12, 2023
1 parent a435d66 commit 7f4232c
Show file tree
Hide file tree
Showing 18 changed files with 210 additions and 50 deletions.
6 changes: 6 additions & 0 deletions .changeset/mean-apricots-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@segment/analytics-next': minor
'@segment/analytics-core': minor
---

Adds `globalAnalyticsKey` option for setting custom global window buffers
2 changes: 1 addition & 1 deletion examples/with-next-js/pages/partytown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/browser/__tests__/cdn.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
})
54 changes: 53 additions & 1 deletion packages/browser/src/browser/__tests__/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
highEntropyTestData,
lowEntropyTestData,
} from '../../test-helpers/fixtures/client-hints'
import { getGlobalAnalytics } from '../..'

let fetchCalls: ReturnType<typeof parseFetchCall>[] = []

Expand Down Expand Up @@ -200,7 +201,7 @@ describe('Initialization', () => {
{
...xt,
load: async () => {
expect(window.analytics).toBeUndefined()
expect(getGlobalAnalytics()).toBeUndefined()
expect(getCDN()).toContain(overriddenCDNUrl)
},
},
Expand All @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -142,7 +143,7 @@ describe('standalone bundle', () => {
.mockImplementation((): Promise<Response> => fetchSettings)
const mockCdn = 'http://my-overridden-cdn.com'

window.analytics._cdn = mockCdn
getGlobalAnalytics()!._cdn = mockCdn
await loadLegacySettings(segmentDotCom)

expect(unfetch).toHaveBeenCalledWith(expect.stringContaining(mockCdn))
Expand Down Expand Up @@ -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()
Expand Down
7 changes: 4 additions & 3 deletions packages/browser/src/browser/__tests__/standalone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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)
})
})
3 changes: 3 additions & 0 deletions packages/browser/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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)

Expand Down
36 changes: 12 additions & 24 deletions packages/browser/src/browser/standalone-analytics.ts
Original file line number Diff line number Diff line change
@@ -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.*/
Expand Down Expand Up @@ -59,16 +48,15 @@ function getWriteKey(): string | undefined {

export async function install(): Promise<void> {
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.'
)
return
}

window.analytics = (await AnalyticsBrowser.standalone(
writeKey,
options
)) as AnalyticsSnippet
setGlobalAnalytics(
(await AnalyticsBrowser.standalone(writeKey, options)) as AnalyticsSnippet
)
}
11 changes: 11 additions & 0 deletions packages/browser/src/browser/standalone-interface.ts
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 7 additions & 1 deletion packages/browser/src/core/analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -536,7 +542,7 @@ export class Analytics

noConflict(): Analytics {
console.warn(deprecationWarning)
window.analytics = _analytics ?? this
setGlobalAnalytics(_analytics ?? this)
return this
}

Expand Down
9 changes: 6 additions & 3 deletions packages/browser/src/core/buffer/snippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
PreInitMethodName,
PreInitMethodParams,
} from '.'
import { getGlobalAnalytics } from '../../lib/global-analytics-helper'

export function transformSnippetCall([
methodName,
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
2 changes: 2 additions & 0 deletions packages/browser/src/lib/__tests__/parse-cdn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ it('should return the overridden cdn if window.analytics._cdn is mutated', () =>
withTag(`
<script src="https://cdn.segment.com/analytics.js/v1/gA5MBlJXrtZaB5sMMZvCF6czfBcfzNO6/analytics.min.js" />
`)
// @ts-ignore
;(window.analytics as any) = {
_cdn: 'http://foo.cdn.com',
}
Expand All @@ -59,6 +60,7 @@ it('should return the overridden cdn if window.analytics._cdn is mutated', () =>

it('if analytics is not loaded yet, should still return cdn', () => {
// is this an impossible state?
// @ts-ignore
window.analytics = undefined as any
withTag(`
<script src="https://cdn.segment.com/analytics.js/v1/gA5MBlJXrtZaB5sMMZvCF6czfBcfzNO6/analytics.min.js" />
Expand Down
31 changes: 31 additions & 0 deletions packages/browser/src/lib/global-analytics-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { AnalyticsSnippet } from '../browser/standalone-interface'

/**
* Stores the global window analytics key
*/
let _globalAnalyticsKey = 'analytics'

/**
* Gets the global analytics/buffer
* @param key name of the window property where the buffer is stored (default: analytics)
* @returns AnalyticsSnippet
*/
export function getGlobalAnalytics(): AnalyticsSnippet | undefined {
return (window as any)[_globalAnalyticsKey]
}

/**
* Replaces the global window key for the analytics/buffer object
* @param key key name
*/
export function setGlobalAnalyticsKey(key: string) {
_globalAnalyticsKey = key
}

/**
* Sets the global analytics object
* @param analytics analytics snippet
*/
export function setGlobalAnalytics(analytics: AnalyticsSnippet): void {
;(window as any)[_globalAnalyticsKey] = analytics
}
Loading

0 comments on commit 7f4232c

Please sign in to comment.