diff --git a/.changeset/silver-mugs-provide.md b/.changeset/silver-mugs-provide.md new file mode 100644 index 000000000..cf906cf0c --- /dev/null +++ b/.changeset/silver-mugs-provide.md @@ -0,0 +1,5 @@ +--- +'@segment/analytics-next': minor +--- + +Added destination filter support to action destinations diff --git a/packages/browser/package.json b/packages/browser/package.json index 98ddd3bf0..b67244a12 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -43,7 +43,7 @@ "size-limit": [ { "path": "dist/umd/index.js", - "limit": "26.02 KB" + "limit": "27.1 KB" } ], "dependencies": { diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index cce51a445..7a91ddebf 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -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, @@ -146,11 +153,26 @@ async function registerPlugins( options: InitOptions, plugins: Plugin[] ): Promise { + 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 + ) }) : [] @@ -175,7 +197,8 @@ async function registerPlugins( legacySettings, analytics.integrations, mergedSettings, - options.obfuscate + options.obfuscate, + tsubMiddleware ).catch(() => []) const toRegister = [ diff --git a/packages/browser/src/core/plugin/index.ts b/packages/browser/src/core/plugin/index.ts index 6faaf419d..578ef256b 100644 --- a/packages/browser/src/core/plugin/index.ts +++ b/packages/browser/src/core/plugin/index.ts @@ -15,6 +15,7 @@ export interface Plugin { name: string version: string type: 'before' | 'after' | 'destination' | 'enrichment' | 'utility' + alternativeNames?: string[] isLoaded: () => boolean load: ( diff --git a/packages/browser/src/core/queue/__tests__/event-queue.test.ts b/packages/browser/src/core/queue/__tests__/event-queue.test.ts index 20c2a800b..8e024a190 100644 --- a/packages/browser/src/core/queue/__tests__/event-queue.test.ts +++ b/packages/browser/src/core/queue/__tests__/event-queue.test.ts @@ -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 { const flushSpy = jest.spyOn(eq, 'flush') @@ -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() diff --git a/packages/browser/src/core/queue/delivery.ts b/packages/browser/src/core/queue/delivery.ts index 11d3234e7..140bd79b2 100644 --- a/packages/browser/src/core/queue/delivery.ts +++ b/packages/browser/src/core/queue/delivery.ts @@ -1,3 +1,4 @@ +import { ActionDestination } from '../../plugins/remote-loader' import { Context, ContextCancelation } from '../context' import { Plugin } from '../plugin' @@ -13,9 +14,12 @@ async function tryOperation( export function attempt( ctx: Context, - plugin: Plugin + plugin: Plugin | ActionDestination ): Promise { - 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] @@ -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) => { @@ -39,7 +43,7 @@ export function attempt( if (err instanceof ContextCancelation) { ctx.log('warn', err.type, { - plugin: plugin.name, + plugin: name, error: err, }) @@ -47,11 +51,11 @@ export function attempt( } 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 }) diff --git a/packages/browser/src/core/queue/event-queue.ts b/packages/browser/src/core/queue/event-queue.ts index 3adeaadd8..b5e16d6c4 100644 --- a/packages/browser/src/core/queue/event-queue.ts +++ b/packages/browser/src/core/queue/event-queue.ts @@ -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' @@ -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 ) }) diff --git a/packages/browser/src/lib/klona.ts b/packages/browser/src/lib/klona.ts new file mode 100644 index 000000000..4a325be7b --- /dev/null +++ b/packages/browser/src/lib/klona.ts @@ -0,0 +1,4 @@ +import { SegmentEvent } from '../core/events' + +export const klona = (evt: SegmentEvent): SegmentEvent => + JSON.parse(JSON.stringify(evt)) diff --git a/packages/browser/src/plugins/ajs-destination/__tests__/index.test.ts b/packages/browser/src/plugins/ajs-destination/__tests__/index.test.ts index ad1510cb0..d4dfcf9a1 100644 --- a/packages/browser/src/plugins/ajs-destination/__tests__/index.test.ts +++ b/packages/browser/src/plugins/ajs-destination/__tests__/index.test.ts @@ -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) }) diff --git a/packages/browser/src/plugins/ajs-destination/index.ts b/packages/browser/src/plugins/ajs-destination/index.ts index c59b6e143..441f30408 100644 --- a/packages/browser/src/plugins/ajs-destination/index.ts +++ b/packages/browser/src/plugins/ajs-destination/index.ts @@ -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' @@ -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 = new (...args: unknown[]) => T async function flushQueue( @@ -224,7 +220,7 @@ export class LegacyDestination implements Plugin { const afterMiddleware = await applyDestinationMiddleware( this.name, - klona(ctx.event), + ctx.event, this.middleware ) @@ -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 [] @@ -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< @@ -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) } diff --git a/packages/browser/src/plugins/middleware/__tests__/index.test.ts b/packages/browser/src/plugins/middleware/__tests__/index.test.ts index 72fd7b903..0ceaacddb 100644 --- a/packages/browser/src/plugins/middleware/__tests__/index.test.ts +++ b/packages/browser/src/plugins/middleware/__tests__/index.test.ts @@ -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 }) => { @@ -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 = () => { diff --git a/packages/browser/src/plugins/middleware/index.ts b/packages/browser/src/plugins/middleware/index.ts index 3e5529727..3a89953c1 100644 --- a/packages/browser/src/plugins/middleware/index.ts +++ b/packages/browser/src/plugins/middleware/index.ts @@ -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 @@ -27,6 +28,7 @@ export async function applyDestinationMiddleware( evt: SegmentEvent, middleware: DestinationMiddlewareFunction[] ): Promise { + let modifiedEvent = klona(evt) async function applyMiddleware( event: SegmentEvent, fn: DestinationMiddlewareFunction @@ -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( diff --git a/packages/browser/src/plugins/remote-loader/__tests__/index.test.ts b/packages/browser/src/plugins/remote-loader/__tests__/index.test.ts index c423d1dea..8b147b3f1 100644 --- a/packages/browser/src/plugins/remote-loader/__tests__/index.test.ts +++ b/packages/browser/src/plugins/remote-loader/__tests__/index.test.ts @@ -1,7 +1,9 @@ import * as loader from '../../../lib/load-script' import { remoteLoader } from '..' -import { AnalyticsBrowser } from '../../../browser' +import { AnalyticsBrowser, LegacySettings } from '../../../browser' import { InitOptions } from '../../../core/analytics' +import { Context } from '../../../core/context' +import { tsubMiddleware } from '../../routing-middleware' const pluginFactory = jest.fn() @@ -26,6 +28,7 @@ describe('Remote Loader', () => { remotePlugins: [ { name: 'remote plugin', + creationName: 'remote plugin', url: 'cdn/path/to/file.js', libraryName: 'testPlugin', settings: {}, @@ -46,6 +49,7 @@ describe('Remote Loader', () => { remotePlugins: [ { name: 'remote plugin', + creationName: 'remote plugin', url: 'cdn/path/to/file.js', libraryName: 'testPlugin', settings: {}, @@ -71,6 +75,7 @@ describe('Remote Loader', () => { remotePlugins: [ { name: 'remote plugin', + creationName: 'remote plugin', url: 'https://cdn.segment.com/actions/file.js', libraryName: 'testPlugin', settings: {}, @@ -91,6 +96,7 @@ describe('Remote Loader', () => { remotePlugins: [ { name: 'remote plugin', + creationName: 'remote plugin', url: 'cdn/path/to/file.js', libraryName: 'testPlugin', settings: { @@ -118,6 +124,7 @@ describe('Remote Loader', () => { remotePlugins: [ { name: 'remote plugin', + creationName: 'remote plugin', url: 'cdn/path/to/file.js', libraryName: 'testPlugin', settings: { @@ -140,6 +147,7 @@ describe('Remote Loader', () => { remotePlugins: [ { name: 'remote plugin', + creationName: 'remote plugin', url: 'cdn/path/to/file.js', libraryName: 'testPlugin', settings: { @@ -167,6 +175,7 @@ describe('Remote Loader', () => { remotePlugins: [ { name: 'remote plugin', + creationName: 'remote plugin', url: 'cdn/path/to/file.js', libraryName: 'testPlugin', settings: { @@ -189,6 +198,7 @@ describe('Remote Loader', () => { remotePlugins: [ { name: 'remote plugin', + creationName: 'remote plugin', url: 'cdn/path/to/file.js', libraryName: 'this wont resolve', settings: {}, @@ -245,12 +255,14 @@ describe('Remote Loader', () => { remotePlugins: [ { name: 'multiple plugins', + creationName: 'multiple plugins', url: 'multiple-plugins.js', libraryName: 'multiple-plugins', settings: { foo: true }, }, { name: 'single plugin', + creationName: 'single plugin', url: 'single-plugin.js', libraryName: 'single-plugin', settings: { bar: false }, @@ -262,7 +274,52 @@ describe('Remote Loader', () => { ) expect(plugins).toHaveLength(3) - expect(plugins).toEqual(expect.arrayContaining([one, two, three])) + expect(plugins).toEqual( + expect.arrayContaining([ + { + action: one, + name: 'multiple plugins', + version: '1.0.0', + type: 'before', + alternativeNames: ['one'], + middleware: [], + track: expect.any(Function), + alias: expect.any(Function), + group: expect.any(Function), + identify: expect.any(Function), + page: expect.any(Function), + screen: expect.any(Function), + }, + { + action: two, + name: 'multiple plugins', + version: '1.0.0', + type: 'before', + alternativeNames: ['two'], + middleware: [], + track: expect.any(Function), + alias: expect.any(Function), + group: expect.any(Function), + identify: expect.any(Function), + page: expect.any(Function), + screen: expect.any(Function), + }, + { + action: three, + name: 'single plugin', + version: '1.0.0', + type: 'enrichment', + alternativeNames: ['three'], + middleware: [], + track: expect.any(Function), + alias: expect.any(Function), + group: expect.any(Function), + identify: expect.any(Function), + page: expect.any(Function), + screen: expect.any(Function), + }, + ]) + ) expect(multiPluginFactory).toHaveBeenCalledWith({ foo: true }) expect(singlePluginFactory).toHaveBeenCalledWith({ bar: false }) }) @@ -287,12 +344,14 @@ describe('Remote Loader', () => { remotePlugins: [ { name: 'flaky plugin', + creationName: 'flaky plugin', url: 'cdn/path/to/flaky.js', libraryName: 'flaky', settings: {}, }, { name: 'async flaky plugin', + creationName: 'async flaky plugin', url: 'cdn/path/to/asyncFlaky.js', libraryName: 'asyncFlaky', settings: {}, @@ -339,12 +398,14 @@ describe('Remote Loader', () => { remotePlugins: [ { name: 'valid plugin', + creationName: 'valid plugin', url: 'valid', libraryName: 'valid', settings: { foo: true }, }, { name: 'invalid plugin', + creationName: 'invalid plugin', url: 'invalid', libraryName: 'invalid', settings: { bar: false }, @@ -356,12 +417,29 @@ describe('Remote Loader', () => { ) expect(plugins).toHaveLength(1) - expect(plugins).toEqual(expect.arrayContaining([validPlugin])) + expect(plugins).toEqual( + expect.arrayContaining([ + { + action: validPlugin, + name: 'valid plugin', + version: '1.0.0', + type: 'enrichment', + alternativeNames: ['valid'], + middleware: [], + track: expect.any(Function), + alias: expect.any(Function), + group: expect.any(Function), + identify: expect.any(Function), + page: expect.any(Function), + screen: expect.any(Function), + }, + ]) + ) expect(console.warn).toHaveBeenCalledTimes(1) }) it('accepts settings overrides from merged integrations', async () => { - const cdnSettings = { + const cdnSettings: LegacySettings = { integrations: { remotePlugin: { name: 'Charlie Brown', @@ -371,6 +449,7 @@ describe('Remote Loader', () => { remotePlugins: [ { name: 'remotePlugin', + creationName: 'remotePlugin', libraryName: 'testPlugin', url: 'cdn/path/to/file.js', settings: { @@ -416,6 +495,7 @@ describe('Remote Loader', () => { remotePlugins: [ { name: 'remotePlugin', + creationName: 'remotePlugin', libraryName: 'testPlugin', url: 'cdn/path/to/file.js', settings: { @@ -452,4 +532,69 @@ describe('Remote Loader', () => { }) ) }) + + it('applies remote routing rules based on creation name', async () => { + const validPlugin = { + name: 'valid', + version: '1.0.0', + type: 'destination', + load: () => {}, + isLoaded: () => true, + track: (ctx: Context) => ctx, + } + + const cdnSettings: LegacySettings = { + integrations: {}, + middlewareSettings: { + routingRules: [ + { + matchers: [ + { + ir: '["=","event",{"value":"Item Impression"}]', + type: 'fql', + }, + ], + scope: 'destinations', + target_type: 'workspace::project::destination::config', + transformers: [[{ type: 'drop' }]], + destinationName: 'oldValidName', + }, + ], + }, + remotePlugins: [ + { + name: 'valid', + creationName: 'oldValidName', + url: 'valid', + libraryName: 'valid', + settings: { foo: true }, + }, + ], + } + + // @ts-expect-error not gonna return a script tag sorry + jest.spyOn(loader, 'loadScript').mockImplementation((url: string) => { + if (url === 'valid') { + window['valid'] = jest.fn().mockImplementation(() => validPlugin) + } + + return Promise.resolve(true) + }) + + const middleware = tsubMiddleware( + cdnSettings.middlewareSettings!.routingRules + ) + + const plugins = await remoteLoader(cdnSettings, {}, {}, false, middleware) + const plugin = plugins[0] + await expect(() => + plugin.track!(new Context({ type: 'track', event: 'Item Impression' })) + ).rejects.toMatchInlineSnapshot(` + ContextCancelation { + "reason": "dropped by destination middleware", + "retry": false, + "type": "plugin Error", + } + `) + }) }) diff --git a/packages/browser/src/plugins/remote-loader/__tests__/integration.test.ts b/packages/browser/src/plugins/remote-loader/__tests__/integration.test.ts index 9a4e9fa9b..49b6ec1a6 100644 --- a/packages/browser/src/plugins/remote-loader/__tests__/integration.test.ts +++ b/packages/browser/src/plugins/remote-loader/__tests__/integration.test.ts @@ -43,6 +43,7 @@ describe.skip('Remote Plugin Integration', () => { // but I'd like to have a full integration test if possible { name: 'amplitude', + creationName: 'amplitude', url: 'https://ajs-next-integrations.s3-us-west-2.amazonaws.com/fab-5/amplitude-plugins.js', libraryName: 'amplitude-pluginsDestination', settings: { diff --git a/packages/browser/src/plugins/remote-loader/index.ts b/packages/browser/src/plugins/remote-loader/index.ts index cb274f8c7..0e87251b2 100644 --- a/packages/browser/src/plugins/remote-loader/index.ts +++ b/packages/browser/src/plugins/remote-loader/index.ts @@ -5,10 +5,18 @@ import { Plugin } from '../../core/plugin' import { asPromise } from '../../lib/as-promise' import { loadScript } from '../../lib/load-script' import { getCDN } from '../../lib/parse-cdn' +import { + applyDestinationMiddleware, + DestinationMiddlewareFunction, +} from '../middleware' +import { Context, ContextCancelation } from '../../core/context' +import { Analytics } from '../../core/analytics' export interface RemotePlugin { /** The name of the remote plugin */ name: string + /** The creation name of the remote plugin */ + creationName: string /** The url of the javascript file to load */ url: string /** The UMD/global name the plugin uses. Plugins are expected to exist here with the `PluginFactory` method signature */ @@ -17,6 +25,85 @@ export interface RemotePlugin { settings: JSONObject } +export class ActionDestination implements Plugin { + name: string // destination name + version = '1.0.0' + type: Plugin['type'] + + alternativeNames: string[] = [] + + middleware: DestinationMiddlewareFunction[] = [] + + action: Plugin + + constructor(name: string, action: Plugin) { + this.action = action + this.name = name + this.type = action.type + this.alternativeNames.push(action.name) + } + + addMiddleware(...fn: DestinationMiddlewareFunction[]): void { + this.middleware.push(...fn) + } + + private async transform(ctx: Context): Promise { + const modifiedEvent = await applyDestinationMiddleware( + this.name, + ctx.event, + this.middleware + ) + + if (modifiedEvent === null) { + ctx.cancel( + new ContextCancelation({ + retry: false, + reason: 'dropped by destination middleware', + }) + ) + } + + return new Context(modifiedEvent!) + } + + private _createMethod( + methodName: 'track' | 'page' | 'identify' | 'alias' | 'group' | 'screen' + ) { + return async (ctx: Context): Promise => { + if (!this.action[methodName]) return ctx + + const transformedContext = await this.transform(ctx) + await this.action[methodName]!(transformedContext) + + return ctx + } + } + + alias = this._createMethod('alias') + group = this._createMethod('group') + identify = this._createMethod('identify') + page = this._createMethod('page') + screen = this._createMethod('screen') + track = this._createMethod('track') + + /* --- PASSTHROUGH METHODS --- */ + isLoaded(): boolean { + return this.action.isLoaded() + } + + ready(): Promise { + return this.action.ready ? this.action.ready() : Promise.resolve() + } + + load(ctx: Context, analytics: Analytics): Promise { + return this.action.load(ctx, analytics) + } + + unload(ctx: Context, analytics: Analytics): Promise | unknown { + return this.action.unload?.(ctx, analytics) + } +} + type PluginFactory = ( settings: JSONValue ) => Plugin | Plugin[] | Promise @@ -46,11 +133,14 @@ export async function remoteLoader( settings: LegacySettings, userIntegrations: Integrations, mergedIntegrations: Record, - obfuscate?: boolean + obfuscate?: boolean, + routingMiddleware?: DestinationMiddlewareFunction ): Promise { const allPlugins: Plugin[] = [] const cdn = getCDN() + const routingRules = settings.middlewareSettings?.routingRules ?? [] + const pluginPromises = (settings.remotePlugins ?? []).map( async (remotePlugin) => { if ( @@ -100,7 +190,27 @@ export async function remoteLoader( validate(plugins) - allPlugins.push(...plugins) + const routing = routingRules.filter( + (rule) => rule.destinationName === remotePlugin.creationName + ) + + plugins.forEach((plugin) => { + const wrapper = new ActionDestination( + remotePlugin.creationName, + plugin + ) + + /** Make sure we only apply destination filters to actions of the "destination" type to avoid causing issues for hybrid destinations */ + if ( + routing.length && + routingMiddleware && + plugin.type === 'destination' + ) { + wrapper.addMiddleware(routingMiddleware) + } + + allPlugins.push(wrapper) + }) } } catch (error) { console.warn('Failed to load Remote Plugin', error) diff --git a/packages/browser/src/plugins/schema-filter/__tests__/index.test.ts b/packages/browser/src/plugins/schema-filter/__tests__/index.test.ts index f9e5c73cf..b6e31a9af 100644 --- a/packages/browser/src/plugins/schema-filter/__tests__/index.test.ts +++ b/packages/browser/src/plugins/schema-filter/__tests__/index.test.ts @@ -15,6 +15,7 @@ const settings: LegacySettings = { remotePlugins: [ { name: 'Braze Web Mode (Actions)', + creationName: 'Braze Web Mode (Actions)', libraryName: 'brazeDestination', url: 'https://cdn.segment.com/next-integrations/actions/braze/9850d2cc8308a89db62a.js', settings: { @@ -34,6 +35,7 @@ const settings: LegacySettings = { { // note that Fullstory name contains 'Actions' name: 'Fullstory (Actions)', + creationName: 'Fullstory (Actions)', libraryName: 'fullstoryDestination', url: 'https://cdn.segment.com/next-integrations/actions/fullstory/35ea1d304f85f3306f48.js', settings: {