Skip to content

Commit

Permalink
Support destination filters for device mode action destinations (#597)
Browse files Browse the repository at this point in the history
* 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 <14189820+chrisradek@users.noreply.github.com>

* Fix alternative name logic and add unit test

* remove unused import

Co-authored-by: Christopher Radek <14189820+chrisradek@users.noreply.github.com>
  • Loading branch information
danieljackins and chrisradek authored Oct 12, 2022
1 parent f179397 commit 18dc5b0
Show file tree
Hide file tree
Showing 16 changed files with 405 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/silver-mugs-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@segment/analytics-next': minor
---

Added destination filter support to action destinations
2 changes: 1 addition & 1 deletion packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"size-limit": [
{
"path": "dist/umd/index.js",
"limit": "26.02 KB"
"limit": "27.1 KB"
}
],
"dependencies": {
Expand Down
27 changes: 25 additions & 2 deletions packages/browser/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ function hasLegacyDestinations(settings: LegacySettings): boolean {
)
}

function hasTsubMiddleware(settings: LegacySettings): boolean {
return (
getProcessEnv().NODE_ENV !== 'test' &&
(settings.middlewareSettings?.routingRules?.length ?? 0) > 0
)
}

/**
* With AJS classic, we allow users to call setAnonymousId before the library initialization.
* This is important because some of the destinations will use the anonymousId during the initialization,
Expand Down Expand Up @@ -146,11 +153,26 @@ async function registerPlugins(
options: InitOptions,
plugins: Plugin[]
): Promise<Context> {
const tsubMiddleware = hasTsubMiddleware(legacySettings)
? await import(
/* webpackChunkName: "tsub-middleware" */ '../plugins/routing-middleware'
).then((mod) => {
return mod.tsubMiddleware(
legacySettings.middlewareSettings!.routingRules
)
})
: undefined

const legacyDestinations = hasLegacyDestinations(legacySettings)
? await import(
/* webpackChunkName: "ajs-destination" */ '../plugins/ajs-destination'
).then((mod) => {
return mod.ajsDestinations(legacySettings, analytics.integrations, opts)
return mod.ajsDestinations(
legacySettings,
analytics.integrations,
opts,
tsubMiddleware
)
})
: []

Expand All @@ -175,7 +197,8 @@ async function registerPlugins(
legacySettings,
analytics.integrations,
mergedSettings,
options.obfuscate
options.obfuscate,
tsubMiddleware
).catch(() => [])

const toRegister = [
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/core/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface Plugin {
name: string
version: string
type: 'before' | 'after' | 'destination' | 'enrichment' | 'utility'
alternativeNames?: string[]

isLoaded: () => boolean
load: (
Expand Down
35 changes: 35 additions & 0 deletions packages/browser/src/core/queue/__tests__/event-queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Context, ContextCancelation } from '../../context'
import { Plugin } from '../../plugin'
import { EventQueue } from '../event-queue'
import { pTimeout } from '../../callback'
import { ActionDestination } from '../../../plugins/remote-loader'

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

test('delivers to action destinations using alternative names', async () => {
const eq = new EventQueue()
const fullstory = new ActionDestination('fullstory', testPlugin)
fullstory.alternativeNames.push('fullstory trackEvent')
fullstory.type = 'destination'

jest.spyOn(fullstory, 'track')
jest.spyOn(segmentio, 'track')

const evt = {
type: 'track' as const,
integrations: {
All: false,
'fullstory trackEvent': true,
'Segment.io': {},
},
}

const ctx = new Context(evt)

await eq.register(Context.system(), fullstory, ajs)
await eq.register(Context.system(), segmentio, ajs)

eq.dispatch(ctx)

expect(eq.queue.length).toBe(1)
const flushed = await flushAll(eq)

expect(flushed).toEqual([ctx])

expect(fullstory.track).toHaveBeenCalled()
expect(segmentio.track).toHaveBeenCalled()
})

test('respect deny lists generated by other plugin', async () => {
const eq = new EventQueue()

Expand Down
16 changes: 10 additions & 6 deletions packages/browser/src/core/queue/delivery.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ActionDestination } from '../../plugins/remote-loader'
import { Context, ContextCancelation } from '../context'
import { Plugin } from '../plugin'

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

export function attempt(
ctx: Context,
plugin: Plugin
plugin: Plugin | ActionDestination
): Promise<Context | ContextCancelation | Error | undefined> {
ctx.log('debug', 'plugin', { plugin: plugin.name })
const name = 'action' in plugin ? plugin.action.name : plugin.name

ctx.log('debug', 'plugin', { plugin: name })

const start = new Date().getTime()

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

if (err instanceof ContextCancelation) {
ctx.log('warn', err.type, {
plugin: plugin.name,
plugin: name,
error: err,
})

return err
}

ctx.log('error', 'plugin Error', {
plugin: plugin.name,
plugin: name,
error: err,
})

ctx.stats.increment('plugin_error', 1, [`plugin:${plugin.name}`])
ctx.stats.increment('plugin_error', 1, [`plugin:${name}`])
return err as Error
})

Expand Down
10 changes: 9 additions & 1 deletion packages/browser/src/core/queue/event-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { PersistedPriorityQueue } from '../../lib/priority-queue/persisted'
import { isOnline } from '../connection'
import { Context, ContextCancelation } from '../context'
import { Emitter } from '@segment/analytics-core'
import { Integrations } from '../events'
import { Integrations, JSONObject } from '../events'
import { Plugin } from '../plugin'
import { createTaskGroup, TaskGroup } from '../task/task-group'
import { attempt, ensure } from './delivery'
Expand Down Expand Up @@ -232,9 +232,17 @@ export class EventQueue extends Emitter {
return true
}

let alternativeNameMatch: boolean | JSONObject | undefined = undefined
p.alternativeNames?.forEach((name) => {
if (denyList[name] !== undefined) {
alternativeNameMatch = denyList[name]
}
})

// Explicit integration option takes precedence, `All: false` does not apply to Segment.io
return (
denyList[p.name] ??
alternativeNameMatch ??
(p.name === 'Segment.io' ? true : denyList.All) !== false
)
})
Expand Down
4 changes: 4 additions & 0 deletions packages/browser/src/lib/klona.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SegmentEvent } from '../core/events'

export const klona = (evt: SegmentEvent): SegmentEvent =>
JSON.parse(JSON.stringify(evt))
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,10 @@ describe('loading ajsDestinations', () => {
})

it('adds a tsub middleware for matching rules', () => {
const destinations = ajsDestinations(cdnResponse)
const middleware = tsubMiddleware(
cdnResponse.middlewareSettings!.routingRules
)
const destinations = ajsDestinations(cdnResponse, {}, {}, middleware)
const amplitude = destinations.find((d) => d.name === 'Amplitude')
expect(amplitude?.middleware.length).toBe(1)
})
Expand Down
14 changes: 5 additions & 9 deletions packages/browser/src/plugins/ajs-destination/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Integrations, JSONObject, SegmentEvent } from '@/core/events'
import { Integrations, JSONObject } from '@/core/events'
import { Alias, Facade, Group, Identify, Page, Track } from '@segment/facade'
import { Analytics, InitOptions } from '../../core/analytics'
import { LegacySettings } from '../../browser'
Expand All @@ -17,13 +17,9 @@ import {
applyDestinationMiddleware,
DestinationMiddlewareFunction,
} from '../middleware'
import { tsubMiddleware } from '../routing-middleware'
import { loadIntegration, resolveVersion, unloadIntegration } from './loader'
import { LegacyIntegration } from './types'

const klona = (evt: SegmentEvent): SegmentEvent =>
JSON.parse(JSON.stringify(evt))

export type ClassType<T> = new (...args: unknown[]) => T

async function flushQueue(
Expand Down Expand Up @@ -224,7 +220,7 @@ export class LegacyDestination implements Plugin {

const afterMiddleware = await applyDestinationMiddleware(
this.name,
klona(ctx.event),
ctx.event,
this.middleware
)

Expand Down Expand Up @@ -303,7 +299,8 @@ export class LegacyDestination implements Plugin {
export function ajsDestinations(
settings: LegacySettings,
globalIntegrations: Integrations = {},
options: InitOptions = {}
options: InitOptions = {},
routingMiddleware?: DestinationMiddlewareFunction
): LegacyDestination[] {
if (isServer()) {
return []
Expand All @@ -315,7 +312,6 @@ export function ajsDestinations(
}

const routingRules = settings.middlewareSettings?.routingRules ?? []
const routingMiddleware = tsubMiddleware(routingRules)

// merged remote CDN settings with user provided options
const integrationOptions = mergedOptions(settings, options ?? {}) as Record<
Expand Down Expand Up @@ -362,7 +358,7 @@ export function ajsDestinations(
const routing = routingRules.filter(
(rule) => rule.destinationName === name
)
if (routing.length > 0) {
if (routing.length > 0 && routingMiddleware) {
destination.addMiddleware(routingMiddleware)
}

Expand Down
38 changes: 37 additions & 1 deletion packages/browser/src/plugins/middleware/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { MiddlewareFunction, sourceMiddlewarePlugin } from '..'
import {
DestinationMiddlewareFunction,
MiddlewareFunction,
sourceMiddlewarePlugin,
} from '..'
import { Analytics } from '../../../core/analytics'
import { Context } from '../../../core/context'
import { Plugin } from '../../../core/plugin'
import { asPromise } from '../../../lib/as-promise'
import { LegacyDestination } from '../../ajs-destination'

describe(sourceMiddlewarePlugin, () => {
const simpleMiddleware: MiddlewareFunction = ({ payload, next }) => {
Expand Down Expand Up @@ -98,6 +103,37 @@ describe(sourceMiddlewarePlugin, () => {
})
})

describe('Destination Middleware', () => {
it('doesnt modify original context', async () => {
const changeProperties: DestinationMiddlewareFunction = ({
payload,
next,
}) => {
if (!payload.obj.properties) {
payload.obj.properties = {}
}
payload.obj.properties.hello = 'from the other side'
next(payload)
}

const dest = new LegacyDestination('Google Analytics', 'latest', {}, {})

const ctx = new Context({
type: 'track',
event: 'Foo',
properties: {
hello: 'from this side',
},
})

dest.addMiddleware(changeProperties)

await dest.track(ctx)

expect(ctx.event.properties!.hello).toEqual('from this side')
})
})

describe('Common use cases', () => {
it('can be used to cancel an event altogether', async () => {
const blowUp: MiddlewareFunction = () => {
Expand Down
8 changes: 5 additions & 3 deletions packages/browser/src/plugins/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SegmentEvent } from '../../core/events'
import { Plugin } from '../../core/plugin'
import { asPromise } from '../../lib/as-promise'
import { SegmentFacade, toFacade } from '../../lib/to-facade'
import { klona } from '../../lib/klona'

export interface MiddlewareParams {
payload: SegmentFacade
Expand All @@ -27,6 +28,7 @@ export async function applyDestinationMiddleware(
evt: SegmentEvent,
middleware: DestinationMiddlewareFunction[]
): Promise<SegmentEvent | null> {
let modifiedEvent = klona(evt)
async function applyMiddleware(
event: SegmentEvent,
fn: DestinationMiddlewareFunction
Expand Down Expand Up @@ -67,14 +69,14 @@ export async function applyDestinationMiddleware(
}

for (const md of middleware) {
const result = await applyMiddleware(evt, md)
const result = await applyMiddleware(modifiedEvent, md)
if (result === null) {
return null
}
evt = result
modifiedEvent = result
}

return evt
return modifiedEvent
}

export function sourceMiddlewarePlugin(
Expand Down
Loading

0 comments on commit 18dc5b0

Please sign in to comment.