Skip to content

Commit 78dcd0e

Browse files
authored
fix: timeline safe processing events for destinations (#604)
1 parent 55d7988 commit 78dcd0e

File tree

4 files changed

+144
-4
lines changed

4 files changed

+144
-4
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { SegmentClient } from '../analytics';
2+
import { Plugin } from '../plugin';
3+
import { Timeline } from '../timeline';
4+
import { EventType, PluginType, SegmentEvent, TrackEventType } from '../types';
5+
import { getMockLogger } from './__helpers__/mockLogger';
6+
import { MockSegmentStore } from './__helpers__/mockSegmentStore';
7+
8+
jest
9+
.spyOn(Date.prototype, 'toISOString')
10+
.mockReturnValue('2010-01-01T00:00:00.000Z');
11+
12+
describe('timeline', () => {
13+
const initialUserInfo = {
14+
traits: {
15+
name: 'Stacy',
16+
age: 30,
17+
},
18+
userId: 'current-user-id',
19+
anonymousId: 'very-anonymous',
20+
};
21+
22+
const store = new MockSegmentStore({
23+
userInfo: initialUserInfo,
24+
settings: {
25+
'1': true,
26+
'2': true,
27+
},
28+
});
29+
30+
const clientArgs = {
31+
config: {
32+
writeKey: 'mock-write-key',
33+
},
34+
logger: getMockLogger(),
35+
store: store,
36+
};
37+
38+
const client = new SegmentClient(clientArgs);
39+
40+
class MockPlugin extends Plugin {
41+
constructor(
42+
private readonly executeFunc: (
43+
event: SegmentEvent
44+
) => SegmentEvent | undefined,
45+
type: PluginType
46+
) {
47+
super();
48+
this.type = type;
49+
this.analytics = client;
50+
}
51+
52+
execute(event: SegmentEvent) {
53+
return this.executeFunc(event);
54+
}
55+
}
56+
57+
it('processes each destination independently', () => {
58+
const timeline = new Timeline();
59+
60+
const goodPlugin = jest.fn().mockImplementation((e) => e);
61+
const badPlugin = jest.fn().mockImplementation(() => undefined);
62+
timeline.add(new MockPlugin(badPlugin, PluginType.destination));
63+
timeline.add(new MockPlugin(goodPlugin, PluginType.destination));
64+
65+
const expectedEvent: TrackEventType = {
66+
type: EventType.TrackEvent,
67+
event: 'test',
68+
properties: {
69+
test: 'sample',
70+
},
71+
};
72+
73+
const result = timeline.process(expectedEvent);
74+
75+
expect(result).toEqual(expectedEvent);
76+
expect(goodPlugin).toHaveBeenCalled();
77+
expect(badPlugin).toHaveBeenCalled();
78+
});
79+
80+
it('handles errors from plugins execution', () => {
81+
const timeline = new Timeline();
82+
83+
const goodPlugin = jest.fn().mockImplementation((e) => e);
84+
const badPlugin = jest.fn().mockImplementation(() => {
85+
throw 'ERROR';
86+
});
87+
timeline.add(new MockPlugin(badPlugin, PluginType.before));
88+
timeline.add(new MockPlugin(goodPlugin, PluginType.before));
89+
90+
const expectedEvent: TrackEventType = {
91+
type: EventType.TrackEvent,
92+
event: 'test',
93+
properties: {
94+
test: 'sample',
95+
},
96+
};
97+
98+
const result = timeline.process(expectedEvent);
99+
100+
expect(result).toEqual(expectedEvent);
101+
expect(goodPlugin).toHaveBeenCalled();
102+
expect(badPlugin).toHaveBeenCalled();
103+
});
104+
105+
it('shortcircuits plugin execution if a plugin return undefined', () => {
106+
const timeline = new Timeline();
107+
108+
const goodPlugin = jest.fn().mockImplementation((e) => e);
109+
const badPlugin = jest.fn().mockImplementation(() => undefined);
110+
timeline.add(new MockPlugin(badPlugin, PluginType.before));
111+
timeline.add(new MockPlugin(goodPlugin, PluginType.before));
112+
timeline.add(new MockPlugin(goodPlugin, PluginType.destination));
113+
114+
const expectedEvent: TrackEventType = {
115+
type: EventType.TrackEvent,
116+
event: 'test',
117+
properties: {
118+
test: 'sample',
119+
},
120+
};
121+
122+
const result = timeline.process(expectedEvent);
123+
124+
expect(result).toEqual(undefined);
125+
expect(goodPlugin).not.toHaveBeenCalled();
126+
expect(badPlugin).toHaveBeenCalled();
127+
});
128+
});

packages/core/src/plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export class DestinationPlugin extends EventPlugin {
155155

156156
execute(event: SegmentEvent): SegmentEvent | undefined {
157157
if (!this.isEnabled(event)) {
158-
return undefined;
158+
return;
159159
}
160160

161161
// Apply before and enrichment plugins

packages/core/src/timeline.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { PluginType, SegmentEvent, UpdateType } from './types';
2-
import type { Plugin } from './plugin';
2+
import type { DestinationPlugin, Plugin } from './plugin';
33
import { getAllPlugins } from './util';
44

55
/*
@@ -97,7 +97,19 @@ export class Timeline {
9797
if (plugins) {
9898
plugins.forEach((plugin) => {
9999
if (result) {
100-
result = plugin.execute(result);
100+
try {
101+
const pluginResult = plugin.execute(result);
102+
// Each destination is independent from each other, so we don't roll over changes caused internally in each one of their processing
103+
if (type !== PluginType.destination) {
104+
result = pluginResult;
105+
}
106+
} catch (error) {
107+
console.warn(
108+
`Destination ${
109+
(plugin as DestinationPlugin).key
110+
} failed to execute: ${JSON.stringify(error)}`
111+
);
112+
}
101113
}
102114
});
103115
}

packages/plugins/plugin-braze/src/BrazePlugin.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import flush from './methods/flush';
1010

1111
export class BrazePlugin extends DestinationPlugin {
1212
type = PluginType.destination;
13-
key = 'Braze';
13+
key = 'Appboy';
1414

1515
identify(event: IdentifyEventType) {
1616
const currentUserInfo = this.analytics?.userInfo.get();

0 commit comments

Comments
 (0)