Skip to content

Commit fd09fbc

Browse files
authored
Support adding middleware to every device mode destination (#1053)
1 parent 3218d9e commit fd09fbc

File tree

10 files changed

+169
-37
lines changed

10 files changed

+169
-37
lines changed

.changeset/small-phones-allow.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@segment/analytics-next': minor
3+
---
4+
5+
Allow `*` in integration name field to apply middleware to all destinations plugins.
6+
```ts
7+
addDestinationMiddleware('*', ({ ... }) => {
8+
...
9+
})
10+
```

packages/browser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"size-limit": [
4545
{
4646
"path": "dist/umd/index.js",
47-
"limit": "29.6 KB"
47+
"limit": "29.7 KB"
4848
}
4949
],
5050
"dependencies": {

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

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,116 @@ describe('addDestinationMiddleware', () => {
878878
})
879879
})
880880

881+
it('drops events if next is never called', async () => {
882+
const testPlugin: Plugin = {
883+
name: 'test',
884+
type: 'destination',
885+
version: '0.1.0',
886+
load: () => Promise.resolve(),
887+
track: jest.fn(),
888+
isLoaded: () => true,
889+
}
890+
891+
const [analytics] = await AnalyticsBrowser.load({
892+
writeKey,
893+
})
894+
895+
const fullstory = new ActionDestination('fullstory', testPlugin)
896+
897+
await analytics.register(fullstory)
898+
await fullstory.ready()
899+
analytics.addDestinationMiddleware('fullstory', () => {
900+
// do nothing
901+
})
902+
903+
await analytics.track('foo')
904+
905+
expect(testPlugin.track).not.toHaveBeenCalled()
906+
})
907+
908+
it('drops events if next is called with null', async () => {
909+
const testPlugin: Plugin = {
910+
name: 'test',
911+
type: 'destination',
912+
version: '0.1.0',
913+
load: () => Promise.resolve(),
914+
track: jest.fn(),
915+
isLoaded: () => true,
916+
}
917+
918+
const [analytics] = await AnalyticsBrowser.load({
919+
writeKey,
920+
})
921+
922+
const fullstory = new ActionDestination('fullstory', testPlugin)
923+
924+
await analytics.register(fullstory)
925+
await fullstory.ready()
926+
analytics.addDestinationMiddleware('fullstory', ({ next }) => {
927+
next(null)
928+
})
929+
930+
await analytics.track('foo')
931+
932+
expect(testPlugin.track).not.toHaveBeenCalled()
933+
})
934+
935+
it('applies to all destinations if * glob is passed as name argument', async () => {
936+
const [analytics] = await AnalyticsBrowser.load({
937+
writeKey,
938+
})
939+
940+
const p1 = new ActionDestination('p1', { ...googleAnalytics })
941+
const p2 = new ActionDestination('p2', { ...amplitude })
942+
943+
await analytics.register(p1, p2)
944+
await p1.ready()
945+
await p2.ready()
946+
947+
const middleware = jest.fn()
948+
949+
analytics.addDestinationMiddleware('*', middleware)
950+
await analytics.track('foo')
951+
952+
expect(middleware).toHaveBeenCalledTimes(2)
953+
expect(middleware).toHaveBeenCalledWith(
954+
expect.objectContaining({ integration: 'p1' })
955+
)
956+
expect(middleware).toHaveBeenCalledWith(
957+
expect.objectContaining({ integration: 'p2' })
958+
)
959+
})
960+
961+
it('middleware is only applied to type: destination plugins', async () => {
962+
const [analytics] = await AnalyticsBrowser.load({
963+
writeKey,
964+
})
965+
966+
const utilityPlugin = new ActionDestination('p1', {
967+
...xt,
968+
type: 'utility',
969+
})
970+
971+
const destinationPlugin = new ActionDestination('p2', {
972+
...xt,
973+
type: 'destination',
974+
})
975+
976+
await analytics.register(utilityPlugin, destinationPlugin)
977+
await utilityPlugin.ready()
978+
await destinationPlugin.ready()
979+
980+
const middleware = jest.fn()
981+
982+
analytics.addDestinationMiddleware('*', middleware)
983+
await analytics.track('foo')
984+
985+
expect(middleware).toHaveBeenCalledTimes(1)
986+
expect(middleware).toHaveBeenCalledWith(
987+
expect.objectContaining({ integration: 'p2' })
988+
)
989+
})
990+
881991
it('supports registering action destination middlewares', async () => {
882992
const testPlugin: Plugin = {
883993
name: 'test',

packages/browser/src/core/analytics/__tests__/test-plugins.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Context, ContextCancelation, Plugin } from '../../../index'
2-
import type { DestinationPlugin } from '../../plugin'
32

43
export interface BasePluginOptions {
54
shouldThrow?: boolean
@@ -65,30 +64,26 @@ class BasePlugin implements Partial<Plugin> {
6564
}
6665
}
6766

68-
export class TestBeforePlugin extends BasePlugin implements Plugin {
67+
export class TestBeforePlugin extends BasePlugin {
6968
public name = 'Test Before Error'
7069
public type = 'before' as const
7170
}
7271

73-
export class TestEnrichmentPlugin extends BasePlugin implements Plugin {
72+
export class TestEnrichmentPlugin extends BasePlugin {
7473
public name = 'Test Enrichment Error'
7574
public type = 'enrichment' as const
7675
}
7776

78-
export class TestDestinationPlugin
79-
extends BasePlugin
80-
implements DestinationPlugin
81-
{
77+
export class TestDestinationPlugin extends BasePlugin {
8278
public name = 'Test Destination Error'
8379
public type = 'destination' as const
84-
addMiddleware() {}
8580

8681
public ready() {
8782
return Promise.resolve(true)
8883
}
8984
}
9085

91-
export class TestAfterPlugin extends BasePlugin implements Plugin {
86+
export class TestAfterPlugin extends BasePlugin {
9287
public name = 'Test After Error'
9388
public type = 'after' as const
9489
}

packages/browser/src/core/analytics/index.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,11 @@ import {
2323
EventProperties,
2424
SegmentEvent,
2525
} from '../events'
26-
import type { Plugin } from '../plugin'
26+
import { isDestinationPluginWithAddMiddleware, Plugin } from '../plugin'
2727
import { EventQueue } from '../queue/event-queue'
2828
import { Group, ID, User, UserOptions } from '../user'
2929
import autoBind from '../../lib/bind-all'
3030
import { PersistedPriorityQueue } from '../../lib/priority-queue/persisted'
31-
import type { LegacyDestination } from '../../plugins/ajs-destination'
3231
import type {
3332
LegacyIntegration,
3433
ClassicIntegrationSource,
@@ -520,13 +519,17 @@ export class Analytics
520519
integrationName: string,
521520
...middlewares: DestinationMiddlewareFunction[]
522521
): Promise<Analytics> {
523-
const legacyDestinations = this.queue.plugins.filter(
524-
(xt) => xt.name.toLowerCase() === integrationName.toLowerCase()
525-
) as LegacyDestination[]
522+
this.queue.plugins
523+
.filter(isDestinationPluginWithAddMiddleware)
524+
.forEach((p) => {
525+
if (
526+
integrationName === '*' ||
527+
p.name.toLowerCase() === integrationName.toLowerCase()
528+
) {
529+
p.addMiddleware(...middlewares)
530+
}
531+
})
526532

527-
legacyDestinations.forEach((destination) => {
528-
destination.addMiddleware(...middlewares)
529-
})
530533
return Promise.resolve(this)
531534
}
532535

packages/browser/src/core/plugin/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,18 @@ import type { Context } from '../context'
55

66
export interface Plugin extends CorePlugin<Context, Analytics> {}
77

8-
export interface DestinationPlugin extends Plugin {
8+
export interface InternalPluginWithAddMiddleware extends Plugin {
99
addMiddleware: (...fns: DestinationMiddlewareFunction[]) => void
1010
}
1111

12-
export type AnyBrowserPlugin = Plugin | DestinationPlugin
12+
export interface InternalDestinationPluginWithAddMiddleware
13+
extends InternalPluginWithAddMiddleware {
14+
type: 'destination'
15+
}
16+
17+
export const isDestinationPluginWithAddMiddleware = (
18+
plugin: Plugin
19+
): plugin is InternalDestinationPluginWithAddMiddleware => {
20+
// FYI: segment's plugin does not currently have an 'addMiddleware' method
21+
return 'addMiddleware' in plugin && plugin.type === 'destination'
22+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { PriorityQueue } from '../../lib/priority-queue'
22
import { PersistedPriorityQueue } from '../../lib/priority-queue/persisted'
33
import { Context } from '../context'
4-
import { AnyBrowserPlugin } from '../plugin'
4+
import { Plugin } from '../plugin'
55
import { CoreEventQueue } from '@segment/analytics-core'
66
import { isOffline } from '../connection'
77

8-
export class EventQueue extends CoreEventQueue<Context, AnyBrowserPlugin> {
8+
export class EventQueue extends CoreEventQueue<Context, Plugin> {
99
constructor(name: string)
1010
constructor(priorityQueue: PriorityQueue<Context>)
1111
constructor(nameOrQueue: string | PriorityQueue<Context>) {

packages/browser/src/plugins/ajs-destination/index.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { LegacySettings } from '../../browser'
55
import { isOffline, isOnline } from '../../core/connection'
66
import { Context, ContextCancelation } from '../../core/context'
77
import { isServer } from '../../core/environment'
8-
import { DestinationPlugin, Plugin } from '../../core/plugin'
8+
import { InternalPluginWithAddMiddleware, Plugin } from '../../core/plugin'
99
import { attempt } from '@segment/analytics-core'
1010
import { isPlanEventEnabled } from '../../lib/is-plan-event-enabled'
1111
import { mergedOptions } from '../../lib/merged-options'
@@ -65,12 +65,12 @@ async function flushQueue(
6565
return queue
6666
}
6767

68-
export class LegacyDestination implements DestinationPlugin {
68+
export class LegacyDestination implements InternalPluginWithAddMiddleware {
6969
name: string
7070
version: string
7171
settings: JSONObject
7272
options: InitOptions = {}
73-
type: Plugin['type'] = 'destination'
73+
readonly type = 'destination'
7474
middleware: DestinationMiddlewareFunction[] = []
7575

7676
private _ready: boolean | undefined
@@ -226,7 +226,6 @@ export class LegacyDestination implements DestinationPlugin {
226226
type: 'Dropped by plan',
227227
})
228228
)
229-
return ctx
230229
} else {
231230
ctx.updateEvent('integrations', {
232231
...ctx.event.integrations,
@@ -242,7 +241,6 @@ export class LegacyDestination implements DestinationPlugin {
242241
type: 'Dropped by plan',
243242
})
244243
)
245-
return ctx
246244
}
247245
}
248246

packages/browser/src/plugins/remote-loader/index.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Integrations } from '../../core/events/interfaces'
22
import { LegacySettings } from '../../browser'
33
import { JSONObject, JSONValue } from '../../core/events'
4-
import { DestinationPlugin, Plugin } from '../../core/plugin'
4+
import { Plugin, InternalPluginWithAddMiddleware } from '../../core/plugin'
55
import { loadScript } from '../../lib/load-script'
66
import { getCDN } from '../../lib/parse-cdn'
77
import {
@@ -26,9 +26,13 @@ export interface RemotePlugin {
2626
settings: JSONObject
2727
}
2828

29-
export class ActionDestination implements DestinationPlugin {
29+
export class ActionDestination implements InternalPluginWithAddMiddleware {
3030
name: string // destination name
3131
version = '1.0.0'
32+
/**
33+
* The lifecycle name of the wrapped plugin.
34+
* This does not need to be 'destination', and can be 'enrichment', etc.
35+
*/
3236
type: Plugin['type']
3337

3438
alternativeNames: string[] = []
@@ -47,6 +51,7 @@ export class ActionDestination implements DestinationPlugin {
4751
}
4852

4953
addMiddleware(...fn: DestinationMiddlewareFunction[]): void {
54+
/** Make sure we only apply destination filters to actions of the "destination" type to avoid causing issues for hybrid destinations */
5055
if (this.type === 'destination') {
5156
this.middleware.push(...fn)
5257
}
@@ -289,12 +294,7 @@ export async function remoteLoader(
289294
plugin
290295
)
291296

292-
/** Make sure we only apply destination filters to actions of the "destination" type to avoid causing issues for hybrid destinations */
293-
if (
294-
routing.length &&
295-
routingMiddleware &&
296-
plugin.type === 'destination'
297-
) {
297+
if (routing.length && routingMiddleware) {
298298
wrapper.addMiddleware(routingMiddleware)
299299
}
300300

packages/browser/src/test-helpers/fixtures/create-fetch-method.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@ export const createMockFetchImplementation = (
77
) => {
88
return (...[url, req]: Parameters<typeof fetch>) => {
99
const reqUrl = url.toString()
10-
if (!req || (req.method === 'get' && reqUrl.includes('cdn.segment.com'))) {
10+
const reqMethod = req?.method?.toLowerCase()
11+
if (!req || (reqMethod === 'get' && reqUrl.includes('cdn.segment.com'))) {
1112
// GET https://cdn.segment.com/v1/projects/{writeKey}
1213
return createSuccess({ ...cdnSettingsMinimal, ...cdnSettings })
1314
}
1415

15-
if (req?.method === 'post' && reqUrl.includes('api.segment.io')) {
16+
if (reqMethod === 'post' && reqUrl.includes('api.segment.io')) {
1617
// POST https://api.segment.io/v1/{event.type}
1718
return createSuccess({ success: true }, { status: 201 })
1819
}
1920

21+
if (reqMethod === 'post' && reqUrl.endsWith('/m')) {
22+
// POST https://api.segment.io/m
23+
return createSuccess({ success: true })
24+
}
25+
2026
throw new Error(
2127
`no match found for request (url:${url}, req:${JSON.stringify(req)})`
2228
)

0 commit comments

Comments
 (0)