Skip to content
This repository was archived by the owner on Jun 27, 2023. It is now read-only.

Commit 8b1df55

Browse files
authored
feat: added destination metadata and internal timeline processing for destinatio plugins (segmentio#477)
1 parent 48add81 commit 8b1df55

File tree

5 files changed

+157
-47
lines changed

5 files changed

+157
-47
lines changed

packages/core/src/plugin.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class Plugin {
4040
}
4141

4242
export class EventPlugin extends Plugin {
43-
execute(event: SegmentEvent) {
43+
execute(event: SegmentEvent): SegmentEvent | undefined {
4444
if (event === undefined) {
4545
return event;
4646
}
@@ -140,9 +140,33 @@ export class DestinationPlugin extends EventPlugin {
140140
this.timeline.remove(plugin);
141141
}
142142

143-
// find(pluginType: PluginType) {
144-
// // return this.timeline.find(pluginType);
145-
// }
143+
execute(event: SegmentEvent): SegmentEvent | undefined {
144+
// Apply before and enrichment plugins
145+
const beforeResult = this.timeline.applyPlugins({
146+
type: PluginType.before,
147+
event,
148+
});
149+
150+
if (beforeResult === undefined) {
151+
return;
152+
}
153+
154+
const enrichmentResult = this.timeline.applyPlugins({
155+
type: PluginType.enrichment,
156+
event: beforeResult,
157+
});
158+
159+
// Now send the event to the destination by executing the normal flow of an EventPlugin
160+
super.execute(enrichmentResult);
161+
162+
// apply .after plugins
163+
let afterResult = this.timeline.applyPlugins({
164+
type: PluginType.after,
165+
event: enrichmentResult,
166+
});
167+
168+
return afterResult;
169+
}
146170
}
147171

148172
export class UtilityPlugin extends EventPlugin {}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { DestinationPlugin, UtilityPlugin } from '../plugin';
2+
import { PluginType, SegmentEvent } from '../types';
3+
import { SEGMENT_DESTINATION_KEY } from './SegmentDestination';
4+
5+
export class DestinationMetadataEnrichment extends UtilityPlugin {
6+
type = PluginType.enrichment;
7+
8+
execute(event: SegmentEvent): SegmentEvent {
9+
const pluginSettings = this.analytics?.settings.get();
10+
const plugins = this.analytics?.getPlugins(PluginType.destination);
11+
12+
if (pluginSettings === undefined) {
13+
return event;
14+
}
15+
16+
// Disable all destinations that have a device mode plugin
17+
const destinations =
18+
plugins?.map((plugin) => (plugin as DestinationPlugin).key) ?? [];
19+
const bundled: string[] = [];
20+
21+
for (const key of destinations) {
22+
if (key === SEGMENT_DESTINATION_KEY) {
23+
continue;
24+
}
25+
26+
if (key in pluginSettings) {
27+
bundled.push(key);
28+
}
29+
}
30+
31+
const unbundled: string[] = [];
32+
const segmentInfo =
33+
(pluginSettings[SEGMENT_DESTINATION_KEY] as Record<string, any>) ?? {};
34+
const unbundledIntegrations: string[] =
35+
segmentInfo.unbundledIntegrations ?? [];
36+
37+
for (const integration of unbundledIntegrations) {
38+
if (!(integration in bundled)) {
39+
unbundled.push(integration);
40+
}
41+
}
42+
43+
// User/event defined integrations override the cloud/device mode merge
44+
const enrichedEvent: SegmentEvent = {
45+
...event,
46+
_metadata: {
47+
bundled,
48+
unbundled,
49+
bundledIds: [],
50+
},
51+
};
52+
return enrichedEvent;
53+
}
54+
}

packages/core/src/plugins/SegmentDestination.ts

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,31 @@
11
import { DestinationPlugin } from '../plugin';
2-
import {
3-
PluginType,
4-
SegmentAPIIntegrations,
5-
SegmentAPISettings,
6-
SegmentEvent,
7-
UpdateType,
8-
} from '../types';
2+
import { PluginType, SegmentEvent } from '../types';
93
import { chunk } from '../util';
104
import { sendEvents } from '../api';
5+
import type { SegmentClient } from '../analytics';
6+
import { DestinationMetadataEnrichment } from './DestinationMetadataEnrichment';
117

128
const MAX_EVENTS_PER_BATCH = 100;
9+
export const SEGMENT_DESTINATION_KEY = 'Segment.io';
1310

1411
export class SegmentDestination extends DestinationPlugin {
1512
type = PluginType.destination;
1613

17-
key = 'Segment.io';
14+
key = SEGMENT_DESTINATION_KEY;
1815

19-
update(_: SegmentAPISettings, __: UpdateType) {
20-
// this is where analytics-swift initalizes the HTTP client
21-
// no need to do this for React Native where we just use the fetch polyfill directly
22-
// see flush() below
23-
}
16+
configure(analytics: SegmentClient): void {
17+
super.configure(analytics);
2418

25-
execute(event: SegmentEvent): SegmentEvent {
26-
const pluginSettings = this.analytics?.settings.get();
27-
const plugins = this.analytics?.getPlugins(PluginType.destination);
19+
// Enrich events with the Destination metadata
20+
this.add(new DestinationMetadataEnrichment());
21+
}
2822

29-
// Disable all destinations that have a device mode plugin
30-
const deviceModePlugins =
31-
plugins?.map((plugin) => (plugin as DestinationPlugin).key) ?? [];
32-
const disabledCloudIntegrations: SegmentAPIIntegrations = {};
33-
if (pluginSettings !== undefined) {
34-
for (const key of deviceModePlugins) {
35-
if (key in pluginSettings) {
36-
disabledCloudIntegrations[key] = false;
37-
}
38-
}
23+
execute(event: SegmentEvent): SegmentEvent | undefined {
24+
const enrichedEvent = super.execute(event);
25+
if (enrichedEvent !== undefined) {
26+
this.analytics?.queueEvent(enrichedEvent);
3927
}
40-
41-
// User/event defined integrations override the cloud/device mode merge
42-
const mergedEvent = {
43-
...event,
44-
integrations: {
45-
...disabledCloudIntegrations,
46-
...event?.integrations,
47-
},
48-
};
49-
this.analytics?.queueEvent(mergedEvent);
50-
return mergedEvent;
28+
return enrichedEvent;
5129
}
5230

5331
async flush() {

packages/core/src/plugins/__tests__/SegmentDestination.test.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { EventType, SegmentEvent, TrackEventType } from '../../types';
2-
import { SegmentDestination } from '../SegmentDestination';
2+
import {
3+
SegmentDestination,
4+
SEGMENT_DESTINATION_KEY,
5+
} from '../SegmentDestination';
36
import { SegmentClient } from '../../analytics';
47
import { MockSegmentStore } from '../../__tests__/__helpers__/mockSegmentStore';
58
import { getMockLogger } from '../../__tests__/__helpers__/mockLogger';
@@ -43,7 +46,7 @@ describe('SegmentDestination', () => {
4346

4447
it('disables device mode plugins to prevent dups', () => {
4548
const plugin = new SegmentDestination();
46-
plugin.analytics = new SegmentClient({
49+
const analytics = new SegmentClient({
4750
...clientArgs,
4851
store: new MockSegmentStore({
4952
settings: {
@@ -53,8 +56,9 @@ describe('SegmentDestination', () => {
5356
},
5457
}),
5558
});
59+
plugin.configure(analytics);
5660

57-
plugin.analytics.getPlugins = jest.fn().mockReturnValue([
61+
plugin.analytics!.getPlugins = jest.fn().mockReturnValue([
5862
{
5963
key: 'firebase',
6064
type: 'destination',
@@ -72,14 +76,57 @@ describe('SegmentDestination', () => {
7276
integrations: {},
7377
};
7478

75-
const expectedIntegrations = {
76-
firebase: false,
79+
const result = plugin.execute(event);
80+
expect(result).toEqual({
81+
...event,
82+
_metadata: {
83+
bundled: ['firebase'],
84+
unbundled: [],
85+
bundledIds: [],
86+
},
87+
});
88+
});
89+
90+
it('marks unbundled plugins where the cloud mode is disabled', () => {
91+
const plugin = new SegmentDestination();
92+
const analytics = new SegmentClient({
93+
...clientArgs,
94+
store: new MockSegmentStore({
95+
settings: {
96+
[SEGMENT_DESTINATION_KEY]: {
97+
unbundledIntegrations: ['firebase'],
98+
},
99+
},
100+
}),
101+
});
102+
plugin.configure(analytics);
103+
104+
plugin.analytics!.getPlugins = jest.fn().mockReturnValue([
105+
{
106+
key: 'firebase',
107+
type: 'destination',
108+
},
109+
]);
110+
111+
const event: TrackEventType = {
112+
anonymousId: '3534a492-e975-4efa-a18b-3c70c562fec2',
113+
event: 'Awesome event',
114+
type: EventType.TrackEvent,
115+
properties: {},
116+
timestamp: '2000-01-01T00:00:00.000Z',
117+
messageId: '1d1744bf-5beb-41ac-ad7a-943eac33babc',
118+
context: { app: { name: 'TestApp' } },
119+
integrations: {},
77120
};
78121

79122
const result = plugin.execute(event);
80123
expect(result).toEqual({
81124
...event,
82-
integrations: expectedIntegrations,
125+
_metadata: {
126+
bundled: [],
127+
unbundled: ['firebase'],
128+
bundledIds: [],
129+
},
83130
});
84131
});
85132

packages/core/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ interface BaseEventType {
2727

2828
context?: PartialContext;
2929
integrations?: SegmentAPIIntegrations;
30+
_metadata?: DestinationMetadata;
3031
}
3132

3233
export interface TrackEventType extends BaseEventType {
@@ -256,6 +257,12 @@ export type SegmentAPISettings = {
256257
integrations: SegmentAPIIntegrations;
257258
};
258259

260+
export type DestinationMetadata = {
261+
bundled: string[];
262+
unbundled: string[];
263+
bundledIds: string[];
264+
};
265+
259266
export enum PluginType {
260267
// Executed before event processing begins.
261268
'before' = 'before',

0 commit comments

Comments
 (0)