Skip to content

Commit e884b61

Browse files
authored
Make SDK resilient to blocking of non-destination type actions (#1080)
1 parent 77e3cf9 commit e884b61

File tree

8 files changed

+98
-31
lines changed

8 files changed

+98
-31
lines changed

.changeset/brave-jokes-train.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@segment/analytics-core': minor
3+
---
4+
5+
Do not throw errors in .register() method.
6+

.changeset/fast-moons-knock.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@segment/analytics-next': minor
3+
'@segment/analytics-core': minor
4+
---
5+
6+
Addresses an issue where, if one of the non-destination actions fails to load/is blocked, the entire SDK fails to load. This is most notable in GA4, where, if GA was blocked, Segment initialization would fail.

packages/browser/src/browser/__tests__/integration.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,63 @@ describe('timeout', () => {
10481048
expect(analytics.settings.timeout).toEqual(50)
10491049
})
10501050
})
1051+
describe('register', () => {
1052+
it('will not invoke any plugins that have initialization errors', async () => {
1053+
const analytics = AnalyticsBrowser.load({
1054+
writeKey,
1055+
})
1056+
1057+
const errorPlugin = new ActionDestination('Error Plugin', {
1058+
...googleAnalytics,
1059+
load: () => Promise.reject('foo'),
1060+
})
1061+
const errorPluginSpy = jest.spyOn(errorPlugin, 'track')
1062+
1063+
const goodPlugin = new ActionDestination('Good Plugin', {
1064+
...googleAnalytics,
1065+
load: () => Promise.resolve('foo'),
1066+
})
1067+
const goodPluginSpy = jest.spyOn(goodPlugin, 'track')
1068+
1069+
await analytics.register(goodPlugin, errorPlugin)
1070+
await errorPlugin.ready()
1071+
await goodPlugin.ready()
1072+
1073+
await analytics.track('foo')
1074+
1075+
expect(errorPluginSpy).not.toHaveBeenCalled()
1076+
expect(goodPluginSpy).toHaveBeenCalled()
1077+
})
1078+
1079+
it('will emit initialization errors', async () => {
1080+
const analytics = AnalyticsBrowser.load({
1081+
writeKey,
1082+
})
1083+
1084+
const errorPlugin = new ActionDestination('Error Plugin', {
1085+
...googleAnalytics,
1086+
load: () => Promise.reject('foo'),
1087+
})
1088+
1089+
const goodPlugin = new ActionDestination('Good Plugin', {
1090+
...googleAnalytics,
1091+
load: () => Promise.resolve('foo'),
1092+
})
1093+
1094+
await analytics
1095+
const errorPluginName = new Promise<string>((resolve) => {
1096+
analytics.instance?.queue.on('initialization_failure', (plugin) =>
1097+
resolve(plugin.name)
1098+
)
1099+
})
1100+
1101+
await analytics.register(goodPlugin, errorPlugin)
1102+
await errorPlugin.ready()
1103+
await goodPlugin.ready()
1104+
1105+
expect(await errorPluginName).toBe('Error Plugin')
1106+
})
1107+
})
10511108

10521109
describe('deregister', () => {
10531110
beforeEach(async () => {

packages/core/src/plugins/index.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import type { CoreAnalytics } from '../analytics'
22
import type { CoreContext } from '../context'
33

4-
interface CorePluginConfig {
5-
options: any
6-
priority: 'critical' | 'non-critical' // whether AJS should expect this plugin to be loaded before starting event delivery
7-
}
8-
94
export type PluginType =
105
| 'before'
116
| 'after'
@@ -26,11 +21,7 @@ export interface CorePlugin<
2621
version: string
2722
type: PluginType
2823
isLoaded: () => boolean
29-
load: (
30-
ctx: Ctx,
31-
instance: Analytics,
32-
config?: CorePluginConfig
33-
) => Promise<unknown>
24+
load: (ctx: Ctx, instance: Analytics) => Promise<unknown>
3425

3526
unload?: (ctx: Ctx, instance: Analytics) => Promise<unknown> | unknown
3627
ready?: () => Promise<unknown>

packages/core/src/queue/__tests__/event-queue.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,7 @@ describe('Flushing', () => {
611611

612612
describe('register', () => {
613613
it('only filters out failed destinations after loading', async () => {
614+
jest.spyOn(console, 'warn').mockImplementation(noop)
614615
const eq = new TestEventQueue()
615616
const goodDestinationPlugin = {
616617
...testPlugin,

packages/core/src/queue/__tests__/extension-flushing.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe('Registration', () => {
5353
expect(load).toHaveBeenCalledWith(ctx, ajs)
5454
})
5555

56-
test('fails if plugin cant be loaded', async () => {
56+
test('does not reject if a plugin cant be loaded', async () => {
5757
const eq = new EventQueue()
5858

5959
const plugin: Plugin = {
@@ -65,9 +65,7 @@ describe('Registration', () => {
6565
}
6666

6767
const ctx = TestCtx.system()
68-
await expect(
69-
eq.register(ctx, plugin, ajs)
70-
).rejects.toThrowErrorMatchingInlineSnapshot(`"👻"`)
68+
await expect(eq.register(ctx, plugin, ajs)).resolves.toBe(undefined)
7169
})
7270

7371
test('allows for destinations to fail registration', async () => {

packages/core/src/queue/event-queue.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,34 @@ export abstract class CoreEventQueue<
5050
plugin: Plugin,
5151
instance: CoreAnalytics
5252
): Promise<void> {
53-
if (plugin.type === 'destination' && plugin.name !== 'Segment.io') {
54-
plugin.load(ctx, instance).catch((err) => {
55-
this.failedInitializations.push(plugin.name)
56-
this.emit('initialization_failure', plugin)
57-
console.warn(plugin.name, err)
58-
59-
ctx.log('warn', 'Failed to load destination', {
60-
plugin: plugin.name,
61-
error: err,
62-
})
63-
64-
// Filter out the failed plugin by excluding it from the list
65-
this.plugins = this.plugins.filter((p) => p !== plugin)
53+
this.plugins.push(plugin)
54+
55+
const handleLoadError = (err: any) => {
56+
this.failedInitializations.push(plugin.name)
57+
this.emit('initialization_failure', plugin)
58+
console.warn(plugin.name, err)
59+
60+
ctx.log('warn', 'Failed to load destination', {
61+
plugin: plugin.name,
62+
error: err,
6663
})
67-
} else {
68-
await plugin.load(ctx, instance)
64+
65+
// Filter out the failed plugin by excluding it from the list
66+
this.plugins = this.plugins.filter((p) => p !== plugin)
6967
}
7068

71-
this.plugins.push(plugin)
69+
if (plugin.type === 'destination' && plugin.name !== 'Segment.io') {
70+
plugin.load(ctx, instance).catch(handleLoadError)
71+
} else {
72+
// for non-destinations plugins, we do need to wait for them to load
73+
// reminder: action destinations can require plugins that are not of type "destination".
74+
// For example, GA4 loads a type 'before' plugins and addition to a type 'destination' plugin
75+
try {
76+
await plugin.load(ctx, instance)
77+
} catch (err) {
78+
handleLoadError(err)
79+
}
80+
}
7281
}
7382

7483
async deregister(

packages/node/src/__tests__/integration.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,6 @@ describe('version', () => {
406406
describe('ready', () => {
407407
it('should only resolve when plugin registration is done', async () => {
408408
const analytics = createTestAnalytics()
409-
expect(analytics['_queue'].plugins.length).toBe(0)
410409
const result = await analytics.ready
411410
expect(result).toBeUndefined()
412411
expect(analytics['_queue'].plugins.length).toBeGreaterThan(0)

0 commit comments

Comments
 (0)