From 22de419d742750270b5952238835b59f1a00cea6 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Sun, 23 Oct 2022 17:24:26 -0500 Subject: [PATCH 01/16] delay init poc --- .changeset/friendly-mails-heal.md | 5 +++ packages/browser/package.json | 2 +- .../analytics-pre-init.integration.test.ts | 12 +++++ .../typedef-tests/analytics-browser.ts | 6 +++ packages/browser/src/browser/index.ts | 45 ++++++++++++++++--- 5 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 .changeset/friendly-mails-heal.md diff --git a/.changeset/friendly-mails-heal.md b/.changeset/friendly-mails-heal.md new file mode 100644 index 000000000..c0ae28ad7 --- /dev/null +++ b/.changeset/friendly-mails-heal.md @@ -0,0 +1,5 @@ +--- +'@segment/analytics-next': patch +--- + +Add ability to delay initialization diff --git a/packages/browser/package.json b/packages/browser/package.json index 4cebd3278..8e7e886df 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -43,7 +43,7 @@ "size-limit": [ { "path": "dist/umd/index.js", - "limit": "27.1 KB" + "limit": "27.2 KB" } ], "dependencies": { 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 34ec737a0..2e9314e9e 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 @@ -398,4 +398,16 @@ describe('Pre-initialization', () => { await ajsBrowser2 }) }) + + describe('Delayed initialization', () => { + it('Should be able to delay initialization ', async () => { + const analytics = new AnalyticsBrowser({ writeKey: 'foo' }) + const track = analytics.track('foo') + await sleep(100) + expect(trackSpy).not.toBeCalled() + analytics.load() + await track + expect(trackSpy).toBeCalledWith('foo') + }) + }) }) diff --git a/packages/browser/src/browser/__tests__/typedef-tests/analytics-browser.ts b/packages/browser/src/browser/__tests__/typedef-tests/analytics-browser.ts index 2d92c14f0..49730dc24 100644 --- a/packages/browser/src/browser/__tests__/typedef-tests/analytics-browser.ts +++ b/packages/browser/src/browser/__tests__/typedef-tests/analytics-browser.ts @@ -9,6 +9,12 @@ import { Group, User } from '../../../core/user' * They aren't meant to be run by anything but the typescript compiler. */ export default { + 'AnalyticsBrowser should accept settings': () => { + const analytics = new AnalyticsBrowser({ writeKey: 'foo' }) + assertNotAny(analytics) + assertIs(analytics) + void analytics.track('foo') + }, 'AnalyticsBrowser should return the correct type': () => { const result = AnalyticsBrowser.load({ writeKey: 'abc' }) assertNotAny(result) diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index b8fe6e078..4e1f2865b 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -75,6 +75,13 @@ export interface AnalyticsBrowserSettings extends AnalyticsSettings { * If provided, will override the default Segment CDN (https://cdn.segment.com) for this application. */ cdnURL?: string + + /** + * Wait for .load() call. + * Default is "false" for instantiation via AnalyticsBrowser.load (invokes .load immediately) + * and "true" for the AnalyticsBrowser constructor + */ + lazy?: boolean } export function loadLegacySettings( @@ -308,13 +315,43 @@ async function loadAnalytics( return [analytics, ctx] } +/** + * Return a promise that can be externally resolve + */ +const createDeferred = () => { + let resolve!: () => void + const promise = new Promise((_resolve) => { + resolve = () => _resolve(undefined) + }) + return { + promise, + resolve, + } +} + /** * The public browser interface for this package. * Use AnalyticsBrowser.load to create an instance. */ export class AnalyticsBrowser extends AnalyticsBuffered { - private constructor(loader: AnalyticsLoader) { - super(loader) + private _resolve: () => void + + constructor(...args: Parameters) { + const [settings, options] = args + const lazy = settings.lazy ?? true + const { promise, resolve } = createDeferred() + super((buffer) => + promise.then(() => loadAnalytics(settings, options, buffer)) + ) + if (!lazy) { + resolve() + } + this._resolve = resolve + } + + load(): void { + // if user wants to invoke .load immediately after instantiation want to resolve immediately + this._resolve() } /** @@ -331,9 +368,7 @@ export class AnalyticsBrowser extends AnalyticsBuffered { settings: AnalyticsBrowserSettings, options: InitOptions = {} ): AnalyticsBrowser { - return new this((preInitBuffer) => - loadAnalytics(settings, options, preInitBuffer) - ) + return new this({ ...settings, lazy: false }, options) } static standalone( From 0e51bc52522499f7a56dadf4485fe1aeab097a48 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 25 Oct 2022 10:34:45 -0500 Subject: [PATCH 02/16] polish --- packages/browser/src/browser/index.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index 4e1f2865b..7b15dbd8d 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -18,7 +18,6 @@ import { PreInitMethodCallBuffer, flushAnalyticsCallsInNewTask, flushAddSourceMiddleware, - AnalyticsLoader, flushSetAnonymousID, flushOn, } from '../core/buffer' @@ -316,7 +315,7 @@ async function loadAnalytics( } /** - * Return a promise that can be externally resolve + * Return a promise that can be externally resolved */ const createDeferred = () => { let resolve!: () => void @@ -334,24 +333,23 @@ const createDeferred = () => { * Use AnalyticsBrowser.load to create an instance. */ export class AnalyticsBrowser extends AnalyticsBuffered { - private _resolve: () => void + private _resolveLoadStart: () => void constructor(...args: Parameters) { const [settings, options] = args const lazy = settings.lazy ?? true - const { promise, resolve } = createDeferred() + const { promise: loadStart, resolve: resolveLoadStart } = createDeferred() super((buffer) => - promise.then(() => loadAnalytics(settings, options, buffer)) + loadStart.then(() => loadAnalytics(settings, options, buffer)) ) if (!lazy) { - resolve() + resolveLoadStart() } - this._resolve = resolve + this._resolveLoadStart = resolveLoadStart } load(): void { - // if user wants to invoke .load immediately after instantiation want to resolve immediately - this._resolve() + this._resolveLoadStart() } /** @@ -368,7 +366,7 @@ export class AnalyticsBrowser extends AnalyticsBuffered { settings: AnalyticsBrowserSettings, options: InitOptions = {} ): AnalyticsBrowser { - return new this({ ...settings, lazy: false }, options) + return new this({ lazy: false, ...settings }, options) } static standalone( From 1266dfe1fd3842fed8f8020ddfad2370dd6bf95a Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 25 Oct 2022 16:32:21 -0500 Subject: [PATCH 03/16] support passing settings in .load --- packages/browser/src/browser/index.ts | 37 +++++++++++++++++---------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index 7b15dbd8d..2660f1cf5 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -317,10 +317,10 @@ async function loadAnalytics( /** * Return a promise that can be externally resolved */ -const createDeferred = () => { - let resolve!: () => void - const promise = new Promise((_resolve) => { - resolve = () => _resolve(undefined) +const createDeferred = () => { + let resolve!: (promiseValue?: T) => void + const promise = new Promise((_resolve) => { + resolve = (promiseValue?: T) => _resolve(promiseValue as T) }) return { promise, @@ -333,23 +333,32 @@ const createDeferred = () => { * Use AnalyticsBrowser.load to create an instance. */ export class AnalyticsBrowser extends AnalyticsBuffered { - private _resolveLoadStart: () => void + private _resolveLoadStart: ( + settings: AnalyticsBrowserSettings | undefined, + options?: InitOptions + ) => void - constructor(...args: Parameters) { - const [settings, options] = args - const lazy = settings.lazy ?? true - const { promise: loadStart, resolve: resolveLoadStart } = createDeferred() + constructor(settings?: AnalyticsBrowserSettings, options: InitOptions = {}) { + const lazy = settings?.lazy ?? true + const { promise: loadStart, resolve: resolveLoadStart } = + createDeferred<[AnalyticsBrowserSettings, InitOptions]>() super((buffer) => - loadStart.then(() => loadAnalytics(settings, options, buffer)) + loadStart.then(([settings, options]) => + loadAnalytics(settings, options, buffer) + ) ) if (!lazy) { - resolveLoadStart() + resolveLoadStart([settings!, options]) } - this._resolveLoadStart = resolveLoadStart + + this._resolveLoadStart = ( + _overrideSettings = settings, + _overrideOptions = options + ) => resolveLoadStart([_overrideSettings!, _overrideOptions]) } - load(): void { - this._resolveLoadStart() + load(settings?: AnalyticsBrowserSettings, options?: InitOptions): void { + return this._resolveLoadStart(settings, options) } /** From f24375effc816e031f93991ab1ce754d3f1588b5 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Wed, 26 Oct 2022 10:19:21 -0500 Subject: [PATCH 04/16] pass settings into load --- .../analytics-pre-init.integration.test.ts | 4 +-- packages/browser/src/browser/index.ts | 32 +++++++------------ 2 files changed, 13 insertions(+), 23 deletions(-) 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 2e9314e9e..4807a5ddf 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 @@ -401,11 +401,11 @@ describe('Pre-initialization', () => { describe('Delayed initialization', () => { it('Should be able to delay initialization ', async () => { - const analytics = new AnalyticsBrowser({ writeKey: 'foo' }) + const analytics = new AnalyticsBrowser() const track = analytics.track('foo') await sleep(100) expect(trackSpy).not.toBeCalled() - analytics.load() + analytics.load({ writeKey: 'abc' }) await track expect(trackSpy).toBeCalledWith('foo') }) diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index 2660f1cf5..28f2970b0 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -74,13 +74,6 @@ export interface AnalyticsBrowserSettings extends AnalyticsSettings { * If provided, will override the default Segment CDN (https://cdn.segment.com) for this application. */ cdnURL?: string - - /** - * Wait for .load() call. - * Default is "false" for instantiation via AnalyticsBrowser.load (invokes .load immediately) - * and "true" for the AnalyticsBrowser constructor - */ - lazy?: boolean } export function loadLegacySettings( @@ -334,30 +327,25 @@ const createDeferred = () => { */ export class AnalyticsBrowser extends AnalyticsBuffered { private _resolveLoadStart: ( - settings: AnalyticsBrowserSettings | undefined, - options?: InitOptions + settings: AnalyticsBrowserSettings, + options: InitOptions ) => void - constructor(settings?: AnalyticsBrowserSettings, options: InitOptions = {}) { - const lazy = settings?.lazy ?? true + constructor() { const { promise: loadStart, resolve: resolveLoadStart } = createDeferred<[AnalyticsBrowserSettings, InitOptions]>() + super((buffer) => loadStart.then(([settings, options]) => loadAnalytics(settings, options, buffer) ) ) - if (!lazy) { - resolveLoadStart([settings!, options]) - } - - this._resolveLoadStart = ( - _overrideSettings = settings, - _overrideOptions = options - ) => resolveLoadStart([_overrideSettings!, _overrideOptions]) + + this._resolveLoadStart = (settings, options) => + resolveLoadStart([settings, options]) } - load(settings?: AnalyticsBrowserSettings, options?: InitOptions): void { + load(settings: AnalyticsBrowserSettings, options: InitOptions = {}): void { return this._resolveLoadStart(settings, options) } @@ -375,7 +363,9 @@ export class AnalyticsBrowser extends AnalyticsBuffered { settings: AnalyticsBrowserSettings, options: InitOptions = {} ): AnalyticsBrowser { - return new this({ lazy: false, ...settings }, options) + const ajs = new AnalyticsBrowser() + ajs.load(settings, options) + return ajs } static standalone( From 4bea175de1399e72e1e69dc04282c634b4d7409f Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Wed, 26 Oct 2022 11:29:09 -0500 Subject: [PATCH 05/16] clean up --- .../analytics-lazy-init.integration.test.ts | 37 +++++++++++++++++++ .../analytics-pre-init.integration.test.ts | 12 ------ .../typedef-tests/analytics-browser.ts | 18 ++++++--- packages/browser/src/browser/index.ts | 20 ++-------- packages/browser/src/lib/create-deferred.ts | 16 ++++++++ 5 files changed, 69 insertions(+), 34 deletions(-) create mode 100644 packages/browser/src/browser/__tests__/analytics-lazy-init.integration.test.ts create mode 100644 packages/browser/src/lib/create-deferred.ts diff --git a/packages/browser/src/browser/__tests__/analytics-lazy-init.integration.test.ts b/packages/browser/src/browser/__tests__/analytics-lazy-init.integration.test.ts new file mode 100644 index 000000000..f664c9113 --- /dev/null +++ b/packages/browser/src/browser/__tests__/analytics-lazy-init.integration.test.ts @@ -0,0 +1,37 @@ +import { sleep } from '@segment/analytics-core' +import unfetch from 'unfetch' +import { AnalyticsBrowser } from '..' +import { Analytics } from '../../core/analytics' +import { createSuccess } from '../../test-helpers/factories' + +jest.mock('unfetch') + +const mockFetchSettingsSuccessResponse = () => { + jest + .mocked(unfetch) + .mockImplementation(() => createSuccess({ integrations: {} })) +} + +describe('Lazy initialization', () => { + let trackSpy: jest.SpiedFunction + beforeEach(() => { + trackSpy = jest.spyOn(Analytics.prototype, 'track') + mockFetchSettingsSuccessResponse() + }) + + it('Should be able to delay initialization ', async () => { + const analytics = new AnalyticsBrowser() + const track = analytics.track('foo') + await sleep(100) + expect(trackSpy).not.toBeCalled() + analytics.load({ writeKey: 'abc' }) + await track + expect(trackSpy).toBeCalledWith('foo') + }) + it('load method return an analytics instance', async () => { + const analytics = new AnalyticsBrowser().load({ writeKey: 'foo' }) + expect(analytics instanceof AnalyticsBrowser).toBeTruthy() + await analytics.track('foo') + expect(trackSpy).toBeCalledWith('foo') + }) +}) 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 4807a5ddf..34ec737a0 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 @@ -398,16 +398,4 @@ describe('Pre-initialization', () => { await ajsBrowser2 }) }) - - describe('Delayed initialization', () => { - it('Should be able to delay initialization ', async () => { - const analytics = new AnalyticsBrowser() - const track = analytics.track('foo') - await sleep(100) - expect(trackSpy).not.toBeCalled() - analytics.load({ writeKey: 'abc' }) - await track - expect(trackSpy).toBeCalledWith('foo') - }) - }) }) diff --git a/packages/browser/src/browser/__tests__/typedef-tests/analytics-browser.ts b/packages/browser/src/browser/__tests__/typedef-tests/analytics-browser.ts index 49730dc24..23bb20d82 100644 --- a/packages/browser/src/browser/__tests__/typedef-tests/analytics-browser.ts +++ b/packages/browser/src/browser/__tests__/typedef-tests/analytics-browser.ts @@ -9,12 +9,6 @@ import { Group, User } from '../../../core/user' * They aren't meant to be run by anything but the typescript compiler. */ export default { - 'AnalyticsBrowser should accept settings': () => { - const analytics = new AnalyticsBrowser({ writeKey: 'foo' }) - assertNotAny(analytics) - assertIs(analytics) - void analytics.track('foo') - }, 'AnalyticsBrowser should return the correct type': () => { const result = AnalyticsBrowser.load({ writeKey: 'abc' }) assertNotAny(result) @@ -116,4 +110,16 @@ export default { } void AnalyticsBrowser.load({ writeKey: 'foo' }).track('foo', {} as User) }, + 'Lazy instantiation should be supported': () => { + const analytics = new AnalyticsBrowser() + assertNotAny(analytics) + assertIs(analytics) + analytics.load({ writeKey: 'foo' }) + void analytics.track('foo') + }, + '.load should return this': () => { + const analytics = new AnalyticsBrowser().load({ writeKey: 'foo' }) + assertNotAny(analytics) + assertIs(analytics) + }, } diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index 28f2970b0..2cc9f9e2a 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -8,6 +8,7 @@ import { Plan } from '../core/events' import { Plugin } from '../core/plugin' import { MetricsOptions } from '../core/stats/remote-metrics' import { mergedOptions } from '../lib/merged-options' +import { createDeferred } from '../lib/create-deferred' import { pageEnrichment } from '../plugins/page-enrichment' import { remoteLoader, RemotePlugin } from '../plugins/remote-loader' import type { RoutingRule } from '../plugins/routing-middleware' @@ -307,20 +308,6 @@ async function loadAnalytics( return [analytics, ctx] } -/** - * Return a promise that can be externally resolved - */ -const createDeferred = () => { - let resolve!: (promiseValue?: T) => void - const promise = new Promise((_resolve) => { - resolve = (promiseValue?: T) => _resolve(promiseValue as T) - }) - return { - promise, - resolve, - } -} - /** * The public browser interface for this package. * Use AnalyticsBrowser.load to create an instance. @@ -345,8 +332,9 @@ export class AnalyticsBrowser extends AnalyticsBuffered { resolveLoadStart([settings, options]) } - load(settings: AnalyticsBrowserSettings, options: InitOptions = {}): void { - return this._resolveLoadStart(settings, options) + load(settings: AnalyticsBrowserSettings, options: InitOptions = {}): this { + this._resolveLoadStart(settings, options) + return this } /** diff --git a/packages/browser/src/lib/create-deferred.ts b/packages/browser/src/lib/create-deferred.ts new file mode 100644 index 000000000..66c3b5d7a --- /dev/null +++ b/packages/browser/src/lib/create-deferred.ts @@ -0,0 +1,16 @@ +/** + * Return a promise that can be externally resolved + */ +export const createDeferred = () => { + let resolve!: (value: T | PromiseLike) => void + let reject!: (reason: any) => void + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve + reject = _reject + }) + return { + resolve, + reject, + promise, + } +} From a67cfc21a1b67208154a3cf72026be858180f680 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Wed, 26 Oct 2022 11:52:07 -0500 Subject: [PATCH 06/16] add doc strings --- packages/browser/src/browser/index.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index 2cc9f9e2a..adcd78990 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -309,8 +309,12 @@ async function loadAnalytics( } /** - * The public browser interface for this package. - * Use AnalyticsBrowser.load to create an instance. + * The public browser interface for Segment Analytics + * ```ts + * export const analytics = new AnalyticsBrowser() + * analytics.load({ writeKey: 'foo' }) + * ``` + * @link https://github.com/segmentio/analytics-next/#readme */ export class AnalyticsBrowser extends AnalyticsBuffered { private _resolveLoadStart: ( @@ -332,6 +336,16 @@ export class AnalyticsBrowser extends AnalyticsBuffered { resolveLoadStart([settings, options]) } + /** + * Starts loading an analytics instance including: + * * Fetching all destinations configured by the user (if applicable). + * * Loading all middleware. + * * Flushing any analytics events that were captured before this method was called. + * ```ts + * export const analytics = new AnalyticsBrowser() // nothing loaded yet + * analytics.load({ writeKey: 'foo' }) + * ``` + */ load(settings: AnalyticsBrowserSettings, options: InitOptions = {}): this { this._resolveLoadStart(settings, options) return this @@ -351,9 +365,7 @@ export class AnalyticsBrowser extends AnalyticsBuffered { settings: AnalyticsBrowserSettings, options: InitOptions = {} ): AnalyticsBrowser { - const ajs = new AnalyticsBrowser() - ajs.load(settings, options) - return ajs + return new AnalyticsBrowser().load(settings, options) } static standalone( From 2f931d601bc0ab18857c16341ec2e8a287393541 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Wed, 26 Oct 2022 12:00:29 -0500 Subject: [PATCH 07/16] tweak ts sig --- packages/browser/src/browser/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index adcd78990..f1335bd67 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -324,7 +324,7 @@ export class AnalyticsBrowser extends AnalyticsBuffered { constructor() { const { promise: loadStart, resolve: resolveLoadStart } = - createDeferred<[AnalyticsBrowserSettings, InitOptions]>() + createDeferred>() super((buffer) => loadStart.then(([settings, options]) => From 9e20ac9acc04c6c04ca09551ef7f406b82794dd6 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 1 Nov 2022 12:18:19 -0500 Subject: [PATCH 08/16] update readme --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index af50ebfae..8e8befe31 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,33 @@ document.body?.addEventListener('click', () => { }) ``` +## Lazy / Delayed Loading +You can load a buffered version of analytics that requires `.load` to be explicitly called before initatiating any network activity. This can be useful if you want to wait for a user to consent before fetching any tracking destinations or sending buffered events to segment. + +- ⚠️ ️`.load` should only be called _once_. + +```ts +export const analytics = new AnalyticsBrowser() + +analytics.identify("hello world") + +if (userConsentsToBeingTracked) { + analytics.load({ writeKey: '' }) // destinations loaded, enqueued events are flushed +} +``` +This strategy also comes in handy if you have some settings that are fetched asynchronously. +```ts +const analytics = new AnalyticsBrowser() + +fetchSettings().then(writeKey => analytics.load({ writeKey })) + +analytics.identify("hello world") +document.body?.addEventListener('click', () => { + analytics.track('document body clicked!') +}) + +``` +## Usage in Common Frameworks ### using `React` (Simple) ```tsx From c4dbf057b4b2c95c17abcc203ace55c62a207064 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 1 Nov 2022 12:31:44 -0500 Subject: [PATCH 09/16] add test that verifies that extra load calls are ignored --- .../analytics-lazy-init.integration.test.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/browser/src/browser/__tests__/analytics-lazy-init.integration.test.ts b/packages/browser/src/browser/__tests__/analytics-lazy-init.integration.test.ts index f664c9113..bafed5cbc 100644 --- a/packages/browser/src/browser/__tests__/analytics-lazy-init.integration.test.ts +++ b/packages/browser/src/browser/__tests__/analytics-lazy-init.integration.test.ts @@ -7,16 +7,17 @@ import { createSuccess } from '../../test-helpers/factories' jest.mock('unfetch') const mockFetchSettingsSuccessResponse = () => { - jest + return jest .mocked(unfetch) .mockImplementation(() => createSuccess({ integrations: {} })) } describe('Lazy initialization', () => { let trackSpy: jest.SpiedFunction + let fetched: jest.MockedFn beforeEach(() => { + fetched = mockFetchSettingsSuccessResponse() trackSpy = jest.spyOn(Analytics.prototype, 'track') - mockFetchSettingsSuccessResponse() }) it('Should be able to delay initialization ', async () => { @@ -34,4 +35,14 @@ describe('Lazy initialization', () => { await analytics.track('foo') expect(trackSpy).toBeCalledWith('foo') }) + + it('should ignore subsequent .load calls', async () => { + const analytics = new AnalyticsBrowser() + await analytics.load({ writeKey: 'abc' }) + await analytics.load({ writeKey: 'abc' }) + expect(fetched).toBeCalledTimes(1) + expect(fetched).toBeCalledWith( + expect.stringContaining('https://cdn.segment.com/v1/projects/') + ) + }) }) From 0894ef2d5c831088969ea1b9ad56bcc5608c579e Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 1 Nov 2022 12:42:46 -0500 Subject: [PATCH 10/16] update readme --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8e8befe31..b197377ba 100644 --- a/README.md +++ b/README.md @@ -70,15 +70,11 @@ if (userConsentsToBeingTracked) { This strategy also comes in handy if you have some settings that are fetched asynchronously. ```ts const analytics = new AnalyticsBrowser() - -fetchSettings().then(writeKey => analytics.load({ writeKey })) +fetchWriteKey().then(writeKey => analytics.load({ writeKey })) analytics.identify("hello world") -document.body?.addEventListener('click', () => { - analytics.track('document body clicked!') -}) - ``` + ## Usage in Common Frameworks ### using `React` (Simple) From 0b5ce4d7f72a6d85447b1656d607f0ff5be88e66 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 1 Nov 2022 12:49:05 -0500 Subject: [PATCH 11/16] update changeset --- .changeset/friendly-mails-heal.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/friendly-mails-heal.md b/.changeset/friendly-mails-heal.md index c0ae28ad7..7ed543b6e 100644 --- a/.changeset/friendly-mails-heal.md +++ b/.changeset/friendly-mails-heal.md @@ -1,5 +1,5 @@ --- -'@segment/analytics-next': patch +'@segment/analytics-next': minor --- Add ability to delay initialization From de3360deaf82221746207bf706b181e05ed1b2b3 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 1 Nov 2022 13:52:23 -0500 Subject: [PATCH 12/16] tweak test --- .../analytics-lazy-init.integration.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/browser/src/browser/__tests__/analytics-lazy-init.integration.test.ts b/packages/browser/src/browser/__tests__/analytics-lazy-init.integration.test.ts index bafed5cbc..526a7a972 100644 --- a/packages/browser/src/browser/__tests__/analytics-lazy-init.integration.test.ts +++ b/packages/browser/src/browser/__tests__/analytics-lazy-init.integration.test.ts @@ -29,20 +29,21 @@ describe('Lazy initialization', () => { await track expect(trackSpy).toBeCalledWith('foo') }) - it('load method return an analytics instance', async () => { + + it('.load method return an analytics instance', async () => { const analytics = new AnalyticsBrowser().load({ writeKey: 'foo' }) expect(analytics instanceof AnalyticsBrowser).toBeTruthy() - await analytics.track('foo') - expect(trackSpy).toBeCalledWith('foo') }) it('should ignore subsequent .load calls', async () => { const analytics = new AnalyticsBrowser() - await analytics.load({ writeKey: 'abc' }) - await analytics.load({ writeKey: 'abc' }) + await analytics.load({ writeKey: 'my-write-key' }) + await analytics.load({ writeKey: 'def' }) expect(fetched).toBeCalledTimes(1) expect(fetched).toBeCalledWith( - expect.stringContaining('https://cdn.segment.com/v1/projects/') + expect.stringContaining( + 'https://cdn.segment.com/v1/projects/my-write-key/settings' + ) ) }) }) From 44bc976640dd4e0f9fd7ee9f6cf878379c6fafbc Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 1 Nov 2022 15:11:20 -0500 Subject: [PATCH 13/16] fix spelling mistake --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b197377ba..2f7ff4676 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ document.body?.addEventListener('click', () => { ``` ## Lazy / Delayed Loading -You can load a buffered version of analytics that requires `.load` to be explicitly called before initatiating any network activity. This can be useful if you want to wait for a user to consent before fetching any tracking destinations or sending buffered events to segment. +You can load a buffered version of analytics that requires `.load` to be explicitly called before initiating any network activity. This can be useful if you want to wait for a user to consent before fetching any tracking destinations or sending buffered events to segment. - ⚠️ ️`.load` should only be called _once_. From 754e74c2d929d0041fc8feeb85eb936bb8d36cb3 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 1 Nov 2022 15:26:00 -0500 Subject: [PATCH 14/16] update docstring --- packages/browser/src/browser/index.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index f1335bd67..f26377954 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -310,6 +310,8 @@ async function loadAnalytics( /** * The public browser interface for Segment Analytics + * + * @example * ```ts * export const analytics = new AnalyticsBrowser() * analytics.load({ writeKey: 'foo' }) @@ -337,12 +339,18 @@ export class AnalyticsBrowser extends AnalyticsBuffered { } /** - * Starts loading an analytics instance including: - * * Fetching all destinations configured by the user (if applicable). + * Fully initialize an analytics instance, including: + * + * * Fetching settings from the segment CDN (by default). + * * Fetching all remote destinations configured by the user (if applicable). + * * Flushing buffered analytics events. * * Loading all middleware. - * * Flushing any analytics events that were captured before this method was called. + * + * Note:️ This method should only be called *once* in your application. + * + * @example * ```ts - * export const analytics = new AnalyticsBrowser() // nothing loaded yet + * export const analytics = new AnalyticsBrowser() * analytics.load({ writeKey: 'foo' }) * ``` */ @@ -354,6 +362,7 @@ export class AnalyticsBrowser extends AnalyticsBuffered { /** * Instantiates an object exposing Analytics methods. * + * @example * ```ts * const ajs = AnalyticsBrowser.load({ writeKey: '' }) * From 4e39761413be1aa1bd8c38d5cf8fc91590d40965 Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 1 Nov 2022 15:28:07 -0500 Subject: [PATCH 15/16] make type sig consistent --- packages/browser/src/browser/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index f26377954..e5509e8d0 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -354,7 +354,10 @@ export class AnalyticsBrowser extends AnalyticsBuffered { * analytics.load({ writeKey: 'foo' }) * ``` */ - load(settings: AnalyticsBrowserSettings, options: InitOptions = {}): this { + load( + settings: AnalyticsBrowserSettings, + options: InitOptions = {} + ): AnalyticsBrowser { this._resolveLoadStart(settings, options) return this } From b66c10f9c92aed46fc0e3054ae7fda836ba6475c Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Tue, 1 Nov 2022 15:32:13 -0500 Subject: [PATCH 16/16] sync README --- packages/browser/README.md | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/browser/README.md b/packages/browser/README.md index d8e209150..2f7ff4676 100644 --- a/packages/browser/README.md +++ b/packages/browser/README.md @@ -53,7 +53,30 @@ document.body?.addEventListener('click', () => { }) ``` -### using `React` (Simple / client-side only) +## Lazy / Delayed Loading +You can load a buffered version of analytics that requires `.load` to be explicitly called before initiating any network activity. This can be useful if you want to wait for a user to consent before fetching any tracking destinations or sending buffered events to segment. + +- ⚠️ ️`.load` should only be called _once_. + +```ts +export const analytics = new AnalyticsBrowser() + +analytics.identify("hello world") + +if (userConsentsToBeingTracked) { + analytics.load({ writeKey: '' }) // destinations loaded, enqueued events are flushed +} +``` +This strategy also comes in handy if you have some settings that are fetched asynchronously. +```ts +const analytics = new AnalyticsBrowser() +fetchWriteKey().then(writeKey => analytics.load({ writeKey })) + +analytics.identify("hello world") +``` + +## Usage in Common Frameworks +### using `React` (Simple) ```tsx import { AnalyticsBrowser } from '@segment/analytics-next' @@ -71,6 +94,8 @@ const App = () => ( ### using `React` (Advanced w/ React Context) ```tsx +import { AnalyticsBrowser } from '@segment/analytics-next' + const AnalyticsContext = React.createContext(undefined!); type Props = { @@ -102,7 +127,7 @@ export const useAnalytics = () => { const TrackButton = () => { const analytics = useAnalytics() return ( - ) @@ -126,7 +151,7 @@ More React Examples: 1. create composable file `segment.ts` with factory ref analytics: ```ts -import { Analytics, AnalyticsBrowser } from '@segment/analytics-next' +import { AnalyticsBrowser } from '@segment/analytics-next' export const analytics = AnalyticsBrowser.load({ writeKey: '', @@ -200,7 +225,10 @@ First, clone the repo and then startup our local dev environment: ```sh $ git clone git@github.com:segmentio/analytics-next.git $ cd analytics-next -$ yarn dev +$ nvm use # installs correct version of node defined in .nvmrc. +$ yarn && yarn build +$ yarn test +$ yarn dev # optional: runs analytics-next playground. ``` > If you get "Cannot find module '@segment/analytics-next' or its corresponding type declarations.ts(2307)" (in VSCode), you may have to "cmd+shift+p -> "TypeScript: Restart TS server"