diff --git a/.changeset/rare-pillows-warn.md b/.changeset/rare-pillows-warn.md new file mode 100644 index 000000000..cc002a53a --- /dev/null +++ b/.changeset/rare-pillows-warn.md @@ -0,0 +1,6 @@ +--- +'@segment/analytics-next': minor +--- + +Adds a new option `ignorePluginReadyError` which prevents `analytics.ready` failure when a plugin fails to be ready. +Also adds another option `onPluginReadyError` which allows developers to handle the plugin failures at ready stage. diff --git a/packages/browser/src/browser/__tests__/integration.test.ts b/packages/browser/src/browser/__tests__/integration.test.ts index 95bd97f30..da1cf9d54 100644 --- a/packages/browser/src/browser/__tests__/integration.test.ts +++ b/packages/browser/src/browser/__tests__/integration.test.ts @@ -207,6 +207,57 @@ describe('Initialization', () => { expect(ready).toHaveBeenCalled() }) + it('ready method is called even when plugin errors on ready, and calls `onPluginReadyError`', async () => { + const ready = jest.fn() + const onPluginReadyError = jest.fn() + + const lazyPlugin1: Plugin = { + name: 'Test 2', + type: 'destination', + version: '1.0', + + load: async (_ctx) => {}, + ready: async () => { + return new Promise((resolve) => setTimeout(resolve, 100)) + }, + isLoaded: () => true, + } + + const lazyPlugin2: Plugin = { + name: 'Test 2', + type: 'destination', + version: '1.0', + + load: async (_ctx) => {}, + ready: () => Promise.reject('failed readying'), + isLoaded: () => true, + } + + jest.spyOn(lazyPlugin1, 'load') + jest.spyOn(lazyPlugin2, 'load') + const [analytics] = await AnalyticsBrowser.load( + { + writeKey, + plugins: [lazyPlugin1, lazyPlugin2, xt], + }, + { + onPluginReadyError, + } + ) + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + const promise = analytics.ready(ready) + expect(lazyPlugin1.load).toHaveBeenCalled() + expect(lazyPlugin2.load).toHaveBeenCalled() + expect(ready).not.toHaveBeenCalled() + + await sleep(100) + expect(ready).toHaveBeenCalled() + expect(onPluginReadyError).toHaveBeenCalledTimes(1) + + return promise + }) + describe('cdn', () => { it('should get the correct CDN in plugins if the CDN overridden', async () => { const overriddenCDNUrl = 'http://cdn.segment.com' // http instead of https diff --git a/packages/browser/src/browser/settings.ts b/packages/browser/src/browser/settings.ts index 0c92bed9a..f10b373b1 100644 --- a/packages/browser/src/browser/settings.ts +++ b/packages/browser/src/browser/settings.ts @@ -224,6 +224,10 @@ export interface InitOptions { plan?: Plan retryQueue?: boolean obfuscate?: boolean + + /** Error handling function for when an integration fails */ + onPluginReadyError?: (error: Error) => Promise + /** * This callback allows you to update/mutate CDN Settings. * This is called directly after settings are fetched from the CDN. diff --git a/packages/browser/src/core/analytics/index.ts b/packages/browser/src/core/analytics/index.ts index 5a9034578..1b9aa5c6f 100644 --- a/packages/browser/src/core/analytics/index.ts +++ b/packages/browser/src/core/analytics/index.ts @@ -534,9 +534,18 @@ export class Analytics async ready( callback: Function = (res: Promise[]): Promise[] => res ): Promise { - return Promise.all( - this.queue.plugins.map((i) => (i.ready ? i.ready() : Promise.resolve())) - ).then((res) => { + return Promise.allSettled( + this.queue.plugins.map((i) => + i.ready + ? i.ready().catch(this.options.onPluginReadyError) + : Promise.resolve() + ) + ).then((results) => { + const res = results.map((result) => { + if (result.status === 'fulfilled') { + return result.value + } + }) callback(res) return res })