-
Notifications
You must be signed in to change notification settings - Fork 135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Delay initialization (lazy loading) #637
Changes from 12 commits
22de419
0e51bc5
1266dfe
f24375e
4bea175
a67cfc2
2f931d6
9e20ac9
c4dbf05
0894ef2
0b5ce4d
de3360d
44bc976
754e74c
4e39761
b66c10f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@segment/analytics-next': minor | ||
--- | ||
|
||
Add ability to delay initialization |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
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 = () => { | ||
return jest | ||
.mocked(unfetch) | ||
.mockImplementation(() => createSuccess({ integrations: {} })) | ||
} | ||
|
||
describe('Lazy initialization', () => { | ||
let trackSpy: jest.SpiedFunction<Analytics['track']> | ||
let fetched: jest.MockedFn<typeof unfetch> | ||
beforeEach(() => { | ||
fetched = mockFetchSettingsSuccessResponse() | ||
trackSpy = jest.spyOn(Analytics.prototype, 'track') | ||
}) | ||
|
||
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() | ||
}) | ||
|
||
it('should ignore subsequent .load calls', async () => { | ||
const analytics = new AnalyticsBrowser() | ||
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/my-write-key/settings' | ||
) | ||
) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
|
@@ -18,7 +19,6 @@ import { | |
PreInitMethodCallBuffer, | ||
flushAnalyticsCallsInNewTask, | ||
flushAddSourceMiddleware, | ||
AnalyticsLoader, | ||
flushSetAnonymousID, | ||
flushOn, | ||
} from '../core/buffer' | ||
|
@@ -309,12 +309,46 @@ 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 constructor(loader: AnalyticsLoader) { | ||
super(loader) | ||
private _resolveLoadStart: ( | ||
settings: AnalyticsBrowserSettings, | ||
options: InitOptions | ||
) => void | ||
|
||
constructor() { | ||
const { promise: loadStart, resolve: resolveLoadStart } = | ||
createDeferred<Parameters<AnalyticsBrowser['load']>>() | ||
|
||
super((buffer) => | ||
loadStart.then(([settings, options]) => | ||
loadAnalytics(settings, options, buffer) | ||
) | ||
) | ||
|
||
this._resolveLoadStart = (settings, options) => | ||
resolveLoadStart([settings, options]) | ||
} | ||
|
||
/** | ||
* Starts loading an analytics instance including: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a note that this should only be called once? Want to make it clear that calling this with different settings won't essentially 'reload' analytics. |
||
* * 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 | ||
} | ||
|
||
/** | ||
|
@@ -331,9 +365,7 @@ export class AnalyticsBrowser extends AnalyticsBuffered { | |
settings: AnalyticsBrowserSettings, | ||
options: InitOptions = {} | ||
): AnalyticsBrowser { | ||
return new this((preInitBuffer) => | ||
loadAnalytics(settings, options, preInitBuffer) | ||
) | ||
return new AnalyticsBrowser().load(settings, options) | ||
} | ||
|
||
static standalone( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
/** | ||
* Return a promise that can be externally resolved | ||
*/ | ||
export const createDeferred = <T>() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a better name than |
||
let resolve!: (value: T | PromiseLike<T>) => void | ||
let reject!: (reason: any) => void | ||
const promise = new Promise<T>((_resolve, _reject) => { | ||
resolve = _resolve | ||
reject = _reject | ||
}) | ||
return { | ||
resolve, | ||
reject, | ||
promise, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for adding this section!