Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment thread
danieljackins marked this conversation as resolved.
)
}

/**
* 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
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
5 changes: 5 additions & 0 deletions packages/browser/src/core/queue/event-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,14 @@ export class EventQueue extends Emitter {
return true
}

const alternativeNameMatch = p.alternativeNames?.filter((name) =>
Object.keys(denyList).includes(name)
)

// Explicit integration option takes precedence, `All: false` does not apply to Segment.io
return (
denyList[p.name] ??
alternativeNameMatch ??
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't alternativeNameMismatch an array of strings here? I think that'd be evaluated as a truthy value so we'd never hit the next conditional, or am I missing something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch - fixed the logic and added a new test

(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