Skip to content

Commit 18dc5b0

Browse files
Support destination filters for device mode action destinations (#597)
* Wrap actions in plugin to support middleware * Add unit test * add changeset * Use creation names * Refactor tsub middleware to be conditional * Fix tests around creationName * Fix more tests * Bump size limit * Add missing creation names * Cleanup from PR feedback, filter based on alternativeNames * removee unused import * Extract klona to its own module * log action names * re-lower size limit * move cloning logic to dest middleware, add unit test * increase size a tiny bit * Update packages/browser/src/browser/index.ts Co-authored-by: Christopher Radek <[email protected]> * Fix alternative name logic and add unit test * remove unused import Co-authored-by: Christopher Radek <[email protected]>
1 parent f179397 commit 18dc5b0

File tree

16 files changed

+405
-30
lines changed

16 files changed

+405
-30
lines changed

.changeset/silver-mugs-provide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@segment/analytics-next': minor
3+
---
4+
5+
Added destination filter support to action destinations

packages/browser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"size-limit": [
4444
{
4545
"path": "dist/umd/index.js",
46-
"limit": "26.02 KB"
46+
"limit": "27.1 KB"
4747
}
4848
],
4949
"dependencies": {

packages/browser/src/browser/index.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ function hasLegacyDestinations(settings: LegacySettings): boolean {
105105
)
106106
}
107107

108+
function hasTsubMiddleware(settings: LegacySettings): boolean {
109+
return (
110+
getProcessEnv().NODE_ENV !== 'test' &&
111+
(settings.middlewareSettings?.routingRules?.length ?? 0) > 0
112+
)
113+
}
114+
108115
/**
109116
* With AJS classic, we allow users to call setAnonymousId before the library initialization.
110117
* This is important because some of the destinations will use the anonymousId during the initialization,
@@ -146,11 +153,26 @@ async function registerPlugins(
146153
options: InitOptions,
147154
plugins: Plugin[]
148155
): Promise<Context> {
156+
const tsubMiddleware = hasTsubMiddleware(legacySettings)
157+
? await import(
158+
/* webpackChunkName: "tsub-middleware" */ '../plugins/routing-middleware'
159+
).then((mod) => {
160+
return mod.tsubMiddleware(
161+
legacySettings.middlewareSettings!.routingRules
162+
)
163+
})
164+
: undefined
165+
149166
const legacyDestinations = hasLegacyDestinations(legacySettings)
150167
? await import(
151168
/* webpackChunkName: "ajs-destination" */ '../plugins/ajs-destination'
152169
).then((mod) => {
153-
return mod.ajsDestinations(legacySettings, analytics.integrations, opts)
170+
return mod.ajsDestinations(
171+
legacySettings,
172+
analytics.integrations,
173+
opts,
174+
tsubMiddleware
175+
)
154176
})
155177
: []
156178

@@ -175,7 +197,8 @@ async function registerPlugins(
175197
legacySettings,
176198
analytics.integrations,
177199
mergedSettings,
178-
options.obfuscate
200+
options.obfuscate,
201+
tsubMiddleware
179202
).catch(() => [])
180203

181204
const toRegister = [

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface Plugin {
1515
name: string
1616
version: string
1717
type: 'before' | 'after' | 'destination' | 'enrichment' | 'utility'
18+
alternativeNames?: string[]
1819

1920
isLoaded: () => boolean
2021
load: (

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Context, ContextCancelation } from '../../context'
1111
import { Plugin } from '../../plugin'
1212
import { EventQueue } from '../event-queue'
1313
import { pTimeout } from '../../callback'
14+
import { ActionDestination } from '../../../plugins/remote-loader'
1415

1516
async function flushAll(eq: EventQueue): Promise<Context[]> {
1617
const flushSpy = jest.spyOn(eq, 'flush')
@@ -611,6 +612,40 @@ describe('Flushing', () => {
611612
expect(segmentio.track).toHaveBeenCalled()
612613
})
613614

615+
test('delivers to action destinations using alternative names', async () => {
616+
const eq = new EventQueue()
617+
const fullstory = new ActionDestination('fullstory', testPlugin)
618+
fullstory.alternativeNames.push('fullstory trackEvent')
619+
fullstory.type = 'destination'
620+
621+
jest.spyOn(fullstory, 'track')
622+
jest.spyOn(segmentio, 'track')
623+
624+
const evt = {
625+
type: 'track' as const,
626+
integrations: {
627+
All: false,
628+
'fullstory trackEvent': true,
629+
'Segment.io': {},
630+
},
631+
}
632+
633+
const ctx = new Context(evt)
634+
635+
await eq.register(Context.system(), fullstory, ajs)
636+
await eq.register(Context.system(), segmentio, ajs)
637+
638+
eq.dispatch(ctx)
639+
640+
expect(eq.queue.length).toBe(1)
641+
const flushed = await flushAll(eq)
642+
643+
expect(flushed).toEqual([ctx])
644+
645+
expect(fullstory.track).toHaveBeenCalled()
646+
expect(segmentio.track).toHaveBeenCalled()
647+
})
648+
614649
test('respect deny lists generated by other plugin', async () => {
615650
const eq = new EventQueue()
616651

packages/browser/src/core/queue/delivery.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ActionDestination } from '../../plugins/remote-loader'
12
import { Context, ContextCancelation } from '../context'
23
import { Plugin } from '../plugin'
34

@@ -13,9 +14,12 @@ async function tryOperation(
1314

1415
export function attempt(
1516
ctx: Context,
16-
plugin: Plugin
17+
plugin: Plugin | ActionDestination
1718
): Promise<Context | ContextCancelation | Error | undefined> {
18-
ctx.log('debug', 'plugin', { plugin: plugin.name })
19+
const name = 'action' in plugin ? plugin.action.name : plugin.name
20+
21+
ctx.log('debug', 'plugin', { plugin: name })
22+
1923
const start = new Date().getTime()
2024

2125
const hook = plugin[ctx.event.type]
@@ -26,7 +30,7 @@ export function attempt(
2630
const newCtx = tryOperation(() => hook.apply(plugin, [ctx]))
2731
.then((ctx) => {
2832
const done = new Date().getTime() - start
29-
ctx.stats.gauge('plugin_time', done, [`plugin:${plugin.name}`])
33+
ctx.stats.gauge('plugin_time', done, [`plugin:${name}`])
3034
return ctx
3135
})
3236
.catch((err) => {
@@ -39,19 +43,19 @@ export function attempt(
3943

4044
if (err instanceof ContextCancelation) {
4145
ctx.log('warn', err.type, {
42-
plugin: plugin.name,
46+
plugin: name,
4347
error: err,
4448
})
4549

4650
return err
4751
}
4852

4953
ctx.log('error', 'plugin Error', {
50-
plugin: plugin.name,
54+
plugin: name,
5155
error: err,
5256
})
5357

54-
ctx.stats.increment('plugin_error', 1, [`plugin:${plugin.name}`])
58+
ctx.stats.increment('plugin_error', 1, [`plugin:${name}`])
5559
return err as Error
5660
})
5761

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { PersistedPriorityQueue } from '../../lib/priority-queue/persisted'
55
import { isOnline } from '../connection'
66
import { Context, ContextCancelation } from '../context'
77
import { Emitter } from '@segment/analytics-core'
8-
import { Integrations } from '../events'
8+
import { Integrations, JSONObject } from '../events'
99
import { Plugin } from '../plugin'
1010
import { createTaskGroup, TaskGroup } from '../task/task-group'
1111
import { attempt, ensure } from './delivery'
@@ -232,9 +232,17 @@ export class EventQueue extends Emitter {
232232
return true
233233
}
234234

235+
let alternativeNameMatch: boolean | JSONObject | undefined = undefined
236+
p.alternativeNames?.forEach((name) => {
237+
if (denyList[name] !== undefined) {
238+
alternativeNameMatch = denyList[name]
239+
}
240+
})
241+
235242
// Explicit integration option takes precedence, `All: false` does not apply to Segment.io
236243
return (
237244
denyList[p.name] ??
245+
alternativeNameMatch ??
238246
(p.name === 'Segment.io' ? true : denyList.All) !== false
239247
)
240248
})

packages/browser/src/lib/klona.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { SegmentEvent } from '../core/events'
2+
3+
export const klona = (evt: SegmentEvent): SegmentEvent =>
4+
JSON.parse(JSON.stringify(evt))

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,10 @@ describe('loading ajsDestinations', () => {
253253
})
254254

255255
it('adds a tsub middleware for matching rules', () => {
256-
const destinations = ajsDestinations(cdnResponse)
256+
const middleware = tsubMiddleware(
257+
cdnResponse.middlewareSettings!.routingRules
258+
)
259+
const destinations = ajsDestinations(cdnResponse, {}, {}, middleware)
257260
const amplitude = destinations.find((d) => d.name === 'Amplitude')
258261
expect(amplitude?.middleware.length).toBe(1)
259262
})

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

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Integrations, JSONObject, SegmentEvent } from '@/core/events'
1+
import { Integrations, JSONObject } from '@/core/events'
22
import { Alias, Facade, Group, Identify, Page, Track } from '@segment/facade'
33
import { Analytics, InitOptions } from '../../core/analytics'
44
import { LegacySettings } from '../../browser'
@@ -17,13 +17,9 @@ import {
1717
applyDestinationMiddleware,
1818
DestinationMiddlewareFunction,
1919
} from '../middleware'
20-
import { tsubMiddleware } from '../routing-middleware'
2120
import { loadIntegration, resolveVersion, unloadIntegration } from './loader'
2221
import { LegacyIntegration } from './types'
2322

24-
const klona = (evt: SegmentEvent): SegmentEvent =>
25-
JSON.parse(JSON.stringify(evt))
26-
2723
export type ClassType<T> = new (...args: unknown[]) => T
2824

2925
async function flushQueue(
@@ -224,7 +220,7 @@ export class LegacyDestination implements Plugin {
224220

225221
const afterMiddleware = await applyDestinationMiddleware(
226222
this.name,
227-
klona(ctx.event),
223+
ctx.event,
228224
this.middleware
229225
)
230226

@@ -303,7 +299,8 @@ export class LegacyDestination implements Plugin {
303299
export function ajsDestinations(
304300
settings: LegacySettings,
305301
globalIntegrations: Integrations = {},
306-
options: InitOptions = {}
302+
options: InitOptions = {},
303+
routingMiddleware?: DestinationMiddlewareFunction
307304
): LegacyDestination[] {
308305
if (isServer()) {
309306
return []
@@ -315,7 +312,6 @@ export function ajsDestinations(
315312
}
316313

317314
const routingRules = settings.middlewareSettings?.routingRules ?? []
318-
const routingMiddleware = tsubMiddleware(routingRules)
319315

320316
// merged remote CDN settings with user provided options
321317
const integrationOptions = mergedOptions(settings, options ?? {}) as Record<
@@ -362,7 +358,7 @@ export function ajsDestinations(
362358
const routing = routingRules.filter(
363359
(rule) => rule.destinationName === name
364360
)
365-
if (routing.length > 0) {
361+
if (routing.length > 0 && routingMiddleware) {
366362
destination.addMiddleware(routingMiddleware)
367363
}
368364

0 commit comments

Comments
 (0)