Skip to content

Commit b335096

Browse files
authored
Delay initialization (lazy loading) (#637)
1 parent 9ba3889 commit b335096

File tree

8 files changed

+190
-13
lines changed

8 files changed

+190
-13
lines changed

.changeset/friendly-mails-heal.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@segment/analytics-next': minor
3+
---
4+
5+
Add ability to delay initialization

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,29 @@ document.body?.addEventListener('click', () => {
5353
})
5454
```
5555

56+
## Lazy / Delayed Loading
57+
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.
58+
59+
- ⚠️ ️`.load` should only be called _once_.
60+
61+
```ts
62+
export const analytics = new AnalyticsBrowser()
63+
64+
analytics.identify("hello world")
65+
66+
if (userConsentsToBeingTracked) {
67+
analytics.load({ writeKey: '<YOUR_WRITE_KEY>' }) // destinations loaded, enqueued events are flushed
68+
}
69+
```
70+
This strategy also comes in handy if you have some settings that are fetched asynchronously.
71+
```ts
72+
const analytics = new AnalyticsBrowser()
73+
fetchWriteKey().then(writeKey => analytics.load({ writeKey }))
74+
75+
analytics.identify("hello world")
76+
```
77+
78+
## Usage in Common Frameworks
5679
### using `React` (Simple)
5780

5881
```tsx

packages/browser/README.md

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,30 @@ document.body?.addEventListener('click', () => {
5353
})
5454
```
5555

56-
### using `React` (Simple / client-side only)
56+
## Lazy / Delayed Loading
57+
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.
58+
59+
- ⚠️ ️`.load` should only be called _once_.
60+
61+
```ts
62+
export const analytics = new AnalyticsBrowser()
63+
64+
analytics.identify("hello world")
65+
66+
if (userConsentsToBeingTracked) {
67+
analytics.load({ writeKey: '<YOUR_WRITE_KEY>' }) // destinations loaded, enqueued events are flushed
68+
}
69+
```
70+
This strategy also comes in handy if you have some settings that are fetched asynchronously.
71+
```ts
72+
const analytics = new AnalyticsBrowser()
73+
fetchWriteKey().then(writeKey => analytics.load({ writeKey }))
74+
75+
analytics.identify("hello world")
76+
```
77+
78+
## Usage in Common Frameworks
79+
### using `React` (Simple)
5780

5881
```tsx
5982
import { AnalyticsBrowser } from '@segment/analytics-next'
@@ -71,6 +94,8 @@ const App = () => (
7194
### using `React` (Advanced w/ React Context)
7295

7396
```tsx
97+
import { AnalyticsBrowser } from '@segment/analytics-next'
98+
7499
const AnalyticsContext = React.createContext<AnalyticsBrowser>(undefined!);
75100

76101
type Props = {
@@ -102,7 +127,7 @@ export const useAnalytics = () => {
102127
const TrackButton = () => {
103128
const analytics = useAnalytics()
104129
return (
105-
<button onClick={() => analytics.track('hello world').then(console.log)}>
130+
<button onClick={() => analytics.track('hello world')}>
106131
Track!
107132
</button>
108133
)
@@ -126,7 +151,7 @@ More React Examples:
126151
1. create composable file `segment.ts` with factory ref analytics:
127152
128153
```ts
129-
import { Analytics, AnalyticsBrowser } from '@segment/analytics-next'
154+
import { AnalyticsBrowser } from '@segment/analytics-next'
130155

131156
export const analytics = AnalyticsBrowser.load({
132157
writeKey: '<YOUR_WRITE_KEY>',
@@ -200,7 +225,10 @@ First, clone the repo and then startup our local dev environment:
200225
```sh
201226
$ git clone git@github.com:segmentio/analytics-next.git
202227
$ cd analytics-next
203-
$ yarn dev
228+
$ nvm use # installs correct version of node defined in .nvmrc.
229+
$ yarn && yarn build
230+
$ yarn test
231+
$ yarn dev # optional: runs analytics-next playground.
204232
```
205233

206234
> 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"

packages/browser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"size-limit": [
4444
{
4545
"path": "dist/umd/index.js",
46-
"limit": "27.1 KB"
46+
"limit": "27.2 KB"
4747
}
4848
],
4949
"dependencies": {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { sleep } from '@segment/analytics-core'
2+
import unfetch from 'unfetch'
3+
import { AnalyticsBrowser } from '..'
4+
import { Analytics } from '../../core/analytics'
5+
import { createSuccess } from '../../test-helpers/factories'
6+
7+
jest.mock('unfetch')
8+
9+
const mockFetchSettingsSuccessResponse = () => {
10+
return jest
11+
.mocked(unfetch)
12+
.mockImplementation(() => createSuccess({ integrations: {} }))
13+
}
14+
15+
describe('Lazy initialization', () => {
16+
let trackSpy: jest.SpiedFunction<Analytics['track']>
17+
let fetched: jest.MockedFn<typeof unfetch>
18+
beforeEach(() => {
19+
fetched = mockFetchSettingsSuccessResponse()
20+
trackSpy = jest.spyOn(Analytics.prototype, 'track')
21+
})
22+
23+
it('Should be able to delay initialization ', async () => {
24+
const analytics = new AnalyticsBrowser()
25+
const track = analytics.track('foo')
26+
await sleep(100)
27+
expect(trackSpy).not.toBeCalled()
28+
analytics.load({ writeKey: 'abc' })
29+
await track
30+
expect(trackSpy).toBeCalledWith('foo')
31+
})
32+
33+
it('.load method return an analytics instance', async () => {
34+
const analytics = new AnalyticsBrowser().load({ writeKey: 'foo' })
35+
expect(analytics instanceof AnalyticsBrowser).toBeTruthy()
36+
})
37+
38+
it('should ignore subsequent .load calls', async () => {
39+
const analytics = new AnalyticsBrowser()
40+
await analytics.load({ writeKey: 'my-write-key' })
41+
await analytics.load({ writeKey: 'def' })
42+
expect(fetched).toBeCalledTimes(1)
43+
expect(fetched).toBeCalledWith(
44+
expect.stringContaining(
45+
'https://cdn.segment.com/v1/projects/my-write-key/settings'
46+
)
47+
)
48+
})
49+
})

packages/browser/src/browser/__tests__/typedef-tests/analytics-browser.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,16 @@ export default {
110110
}
111111
void AnalyticsBrowser.load({ writeKey: 'foo' }).track('foo', {} as User)
112112
},
113+
'Lazy instantiation should be supported': () => {
114+
const analytics = new AnalyticsBrowser()
115+
assertNotAny(analytics)
116+
assertIs<AnalyticsBrowser>(analytics)
117+
analytics.load({ writeKey: 'foo' })
118+
void analytics.track('foo')
119+
},
120+
'.load should return this': () => {
121+
const analytics = new AnalyticsBrowser().load({ writeKey: 'foo' })
122+
assertNotAny(analytics)
123+
assertIs<AnalyticsBrowser>(analytics)
124+
},
113125
}

packages/browser/src/browser/index.ts

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Plan } from '../core/events'
88
import { Plugin } from '../core/plugin'
99
import { MetricsOptions } from '../core/stats/remote-metrics'
1010
import { mergedOptions } from '../lib/merged-options'
11+
import { createDeferred } from '../lib/create-deferred'
1112
import { pageEnrichment } from '../plugins/page-enrichment'
1213
import { remoteLoader, RemotePlugin } from '../plugins/remote-loader'
1314
import type { RoutingRule } from '../plugins/routing-middleware'
@@ -18,7 +19,6 @@ import {
1819
PreInitMethodCallBuffer,
1920
flushAnalyticsCallsInNewTask,
2021
flushAddSourceMiddleware,
21-
AnalyticsLoader,
2222
flushSetAnonymousID,
2323
flushOn,
2424
} from '../core/buffer'
@@ -309,17 +309,63 @@ async function loadAnalytics(
309309
}
310310

311311
/**
312-
* The public browser interface for this package.
313-
* Use AnalyticsBrowser.load to create an instance.
312+
* The public browser interface for Segment Analytics
313+
*
314+
* @example
315+
* ```ts
316+
* export const analytics = new AnalyticsBrowser()
317+
* analytics.load({ writeKey: 'foo' })
318+
* ```
319+
* @link https://github.com/segmentio/analytics-next/#readme
314320
*/
315321
export class AnalyticsBrowser extends AnalyticsBuffered {
316-
private constructor(loader: AnalyticsLoader) {
317-
super(loader)
322+
private _resolveLoadStart: (
323+
settings: AnalyticsBrowserSettings,
324+
options: InitOptions
325+
) => void
326+
327+
constructor() {
328+
const { promise: loadStart, resolve: resolveLoadStart } =
329+
createDeferred<Parameters<AnalyticsBrowser['load']>>()
330+
331+
super((buffer) =>
332+
loadStart.then(([settings, options]) =>
333+
loadAnalytics(settings, options, buffer)
334+
)
335+
)
336+
337+
this._resolveLoadStart = (settings, options) =>
338+
resolveLoadStart([settings, options])
339+
}
340+
341+
/**
342+
* Fully initialize an analytics instance, including:
343+
*
344+
* * Fetching settings from the segment CDN (by default).
345+
* * Fetching all remote destinations configured by the user (if applicable).
346+
* * Flushing buffered analytics events.
347+
* * Loading all middleware.
348+
*
349+
* Note:️ This method should only be called *once* in your application.
350+
*
351+
* @example
352+
* ```ts
353+
* export const analytics = new AnalyticsBrowser()
354+
* analytics.load({ writeKey: 'foo' })
355+
* ```
356+
*/
357+
load(
358+
settings: AnalyticsBrowserSettings,
359+
options: InitOptions = {}
360+
): AnalyticsBrowser {
361+
this._resolveLoadStart(settings, options)
362+
return this
318363
}
319364

320365
/**
321366
* Instantiates an object exposing Analytics methods.
322367
*
368+
* @example
323369
* ```ts
324370
* const ajs = AnalyticsBrowser.load({ writeKey: '<YOUR_WRITE_KEY>' })
325371
*
@@ -331,9 +377,7 @@ export class AnalyticsBrowser extends AnalyticsBuffered {
331377
settings: AnalyticsBrowserSettings,
332378
options: InitOptions = {}
333379
): AnalyticsBrowser {
334-
return new this((preInitBuffer) =>
335-
loadAnalytics(settings, options, preInitBuffer)
336-
)
380+
return new AnalyticsBrowser().load(settings, options)
337381
}
338382

339383
static standalone(
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Return a promise that can be externally resolved
3+
*/
4+
export const createDeferred = <T>() => {
5+
let resolve!: (value: T | PromiseLike<T>) => void
6+
let reject!: (reason: any) => void
7+
const promise = new Promise<T>((_resolve, _reject) => {
8+
resolve = _resolve
9+
reject = _reject
10+
})
11+
return {
12+
resolve,
13+
reject,
14+
promise,
15+
}
16+
}

0 commit comments

Comments
 (0)