diff --git a/.changeset/small-phones-allow.md b/.changeset/small-phones-allow.md new file mode 100644 index 000000000..60b44c495 --- /dev/null +++ b/.changeset/small-phones-allow.md @@ -0,0 +1,10 @@ +--- +'@segment/analytics-next': minor +--- + +Allow `*` in integration name field to apply middleware to all destinations plugins. +```ts +addDestinationMiddleware('*', ({ ... }) => { + ... +}) +``` diff --git a/packages/browser/package.json b/packages/browser/package.json index e31add952..a007d8cd7 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -44,7 +44,7 @@ "size-limit": [ { "path": "dist/umd/index.js", - "limit": "29.6 KB" + "limit": "29.7 KB" } ], "dependencies": { diff --git a/packages/browser/src/browser/__tests__/integration.test.ts b/packages/browser/src/browser/__tests__/integration.test.ts index 02ec2aee9..bd9eeb12e 100644 --- a/packages/browser/src/browser/__tests__/integration.test.ts +++ b/packages/browser/src/browser/__tests__/integration.test.ts @@ -878,6 +878,116 @@ describe('addDestinationMiddleware', () => { }) }) + it('drops events if next is never called', async () => { + const testPlugin: Plugin = { + name: 'test', + type: 'destination', + version: '0.1.0', + load: () => Promise.resolve(), + track: jest.fn(), + isLoaded: () => true, + } + + const [analytics] = await AnalyticsBrowser.load({ + writeKey, + }) + + const fullstory = new ActionDestination('fullstory', testPlugin) + + await analytics.register(fullstory) + await fullstory.ready() + analytics.addDestinationMiddleware('fullstory', () => { + // do nothing + }) + + await analytics.track('foo') + + expect(testPlugin.track).not.toHaveBeenCalled() + }) + + it('drops events if next is called with null', async () => { + const testPlugin: Plugin = { + name: 'test', + type: 'destination', + version: '0.1.0', + load: () => Promise.resolve(), + track: jest.fn(), + isLoaded: () => true, + } + + const [analytics] = await AnalyticsBrowser.load({ + writeKey, + }) + + const fullstory = new ActionDestination('fullstory', testPlugin) + + await analytics.register(fullstory) + await fullstory.ready() + analytics.addDestinationMiddleware('fullstory', ({ next }) => { + next(null) + }) + + await analytics.track('foo') + + expect(testPlugin.track).not.toHaveBeenCalled() + }) + + it('applies to all destinations if * glob is passed as name argument', async () => { + const [analytics] = await AnalyticsBrowser.load({ + writeKey, + }) + + const p1 = new ActionDestination('p1', { ...googleAnalytics }) + const p2 = new ActionDestination('p2', { ...amplitude }) + + await analytics.register(p1, p2) + await p1.ready() + await p2.ready() + + const middleware = jest.fn() + + analytics.addDestinationMiddleware('*', middleware) + await analytics.track('foo') + + expect(middleware).toHaveBeenCalledTimes(2) + expect(middleware).toHaveBeenCalledWith( + expect.objectContaining({ integration: 'p1' }) + ) + expect(middleware).toHaveBeenCalledWith( + expect.objectContaining({ integration: 'p2' }) + ) + }) + + it('middleware is only applied to type: destination plugins', async () => { + const [analytics] = await AnalyticsBrowser.load({ + writeKey, + }) + + const utilityPlugin = new ActionDestination('p1', { + ...xt, + type: 'utility', + }) + + const destinationPlugin = new ActionDestination('p2', { + ...xt, + type: 'destination', + }) + + await analytics.register(utilityPlugin, destinationPlugin) + await utilityPlugin.ready() + await destinationPlugin.ready() + + const middleware = jest.fn() + + analytics.addDestinationMiddleware('*', middleware) + await analytics.track('foo') + + expect(middleware).toHaveBeenCalledTimes(1) + expect(middleware).toHaveBeenCalledWith( + expect.objectContaining({ integration: 'p2' }) + ) + }) + it('supports registering action destination middlewares', async () => { const testPlugin: Plugin = { name: 'test', diff --git a/packages/browser/src/core/analytics/__tests__/test-plugins.ts b/packages/browser/src/core/analytics/__tests__/test-plugins.ts index bbc189993..a140a171f 100644 --- a/packages/browser/src/core/analytics/__tests__/test-plugins.ts +++ b/packages/browser/src/core/analytics/__tests__/test-plugins.ts @@ -1,5 +1,4 @@ import { Context, ContextCancelation, Plugin } from '../../../index' -import type { DestinationPlugin } from '../../plugin' export interface BasePluginOptions { shouldThrow?: boolean @@ -65,30 +64,26 @@ class BasePlugin implements Partial { } } -export class TestBeforePlugin extends BasePlugin implements Plugin { +export class TestBeforePlugin extends BasePlugin { public name = 'Test Before Error' public type = 'before' as const } -export class TestEnrichmentPlugin extends BasePlugin implements Plugin { +export class TestEnrichmentPlugin extends BasePlugin { public name = 'Test Enrichment Error' public type = 'enrichment' as const } -export class TestDestinationPlugin - extends BasePlugin - implements DestinationPlugin -{ +export class TestDestinationPlugin extends BasePlugin { public name = 'Test Destination Error' public type = 'destination' as const - addMiddleware() {} public ready() { return Promise.resolve(true) } } -export class TestAfterPlugin extends BasePlugin implements Plugin { +export class TestAfterPlugin extends BasePlugin { public name = 'Test After Error' public type = 'after' as const } diff --git a/packages/browser/src/core/analytics/index.ts b/packages/browser/src/core/analytics/index.ts index b396a8423..a7675c131 100644 --- a/packages/browser/src/core/analytics/index.ts +++ b/packages/browser/src/core/analytics/index.ts @@ -23,12 +23,11 @@ import { EventProperties, SegmentEvent, } from '../events' -import type { Plugin } from '../plugin' +import { isDestinationPluginWithAddMiddleware, Plugin } from '../plugin' import { EventQueue } from '../queue/event-queue' import { Group, ID, User, UserOptions } from '../user' import autoBind from '../../lib/bind-all' import { PersistedPriorityQueue } from '../../lib/priority-queue/persisted' -import type { LegacyDestination } from '../../plugins/ajs-destination' import type { LegacyIntegration, ClassicIntegrationSource, @@ -520,13 +519,17 @@ export class Analytics integrationName: string, ...middlewares: DestinationMiddlewareFunction[] ): Promise { - const legacyDestinations = this.queue.plugins.filter( - (xt) => xt.name.toLowerCase() === integrationName.toLowerCase() - ) as LegacyDestination[] + this.queue.plugins + .filter(isDestinationPluginWithAddMiddleware) + .forEach((p) => { + if ( + integrationName === '*' || + p.name.toLowerCase() === integrationName.toLowerCase() + ) { + p.addMiddleware(...middlewares) + } + }) - legacyDestinations.forEach((destination) => { - destination.addMiddleware(...middlewares) - }) return Promise.resolve(this) } diff --git a/packages/browser/src/core/plugin/index.ts b/packages/browser/src/core/plugin/index.ts index 5cd35d601..5120121fa 100644 --- a/packages/browser/src/core/plugin/index.ts +++ b/packages/browser/src/core/plugin/index.ts @@ -5,8 +5,18 @@ import type { Context } from '../context' export interface Plugin extends CorePlugin {} -export interface DestinationPlugin extends Plugin { +export interface InternalPluginWithAddMiddleware extends Plugin { addMiddleware: (...fns: DestinationMiddlewareFunction[]) => void } -export type AnyBrowserPlugin = Plugin | DestinationPlugin +export interface InternalDestinationPluginWithAddMiddleware + extends InternalPluginWithAddMiddleware { + type: 'destination' +} + +export const isDestinationPluginWithAddMiddleware = ( + plugin: Plugin +): plugin is InternalDestinationPluginWithAddMiddleware => { + // FYI: segment's plugin does not currently have an 'addMiddleware' method + return 'addMiddleware' in plugin && plugin.type === 'destination' +} diff --git a/packages/browser/src/core/queue/event-queue.ts b/packages/browser/src/core/queue/event-queue.ts index 299508fac..29ab2a411 100644 --- a/packages/browser/src/core/queue/event-queue.ts +++ b/packages/browser/src/core/queue/event-queue.ts @@ -1,11 +1,11 @@ import { PriorityQueue } from '../../lib/priority-queue' import { PersistedPriorityQueue } from '../../lib/priority-queue/persisted' import { Context } from '../context' -import { AnyBrowserPlugin } from '../plugin' +import { Plugin } from '../plugin' import { CoreEventQueue } from '@segment/analytics-core' import { isOffline } from '../connection' -export class EventQueue extends CoreEventQueue { +export class EventQueue extends CoreEventQueue { constructor(name: string) constructor(priorityQueue: PriorityQueue) constructor(nameOrQueue: string | PriorityQueue) { diff --git a/packages/browser/src/plugins/ajs-destination/index.ts b/packages/browser/src/plugins/ajs-destination/index.ts index b51a8f86e..d2354e1de 100644 --- a/packages/browser/src/plugins/ajs-destination/index.ts +++ b/packages/browser/src/plugins/ajs-destination/index.ts @@ -5,7 +5,7 @@ import { LegacySettings } from '../../browser' import { isOffline, isOnline } from '../../core/connection' import { Context, ContextCancelation } from '../../core/context' import { isServer } from '../../core/environment' -import { DestinationPlugin, Plugin } from '../../core/plugin' +import { InternalPluginWithAddMiddleware, Plugin } from '../../core/plugin' import { attempt } from '@segment/analytics-core' import { isPlanEventEnabled } from '../../lib/is-plan-event-enabled' import { mergedOptions } from '../../lib/merged-options' @@ -65,12 +65,12 @@ async function flushQueue( return queue } -export class LegacyDestination implements DestinationPlugin { +export class LegacyDestination implements InternalPluginWithAddMiddleware { name: string version: string settings: JSONObject options: InitOptions = {} - type: Plugin['type'] = 'destination' + readonly type = 'destination' middleware: DestinationMiddlewareFunction[] = [] private _ready: boolean | undefined @@ -226,7 +226,6 @@ export class LegacyDestination implements DestinationPlugin { type: 'Dropped by plan', }) ) - return ctx } else { ctx.updateEvent('integrations', { ...ctx.event.integrations, @@ -242,7 +241,6 @@ export class LegacyDestination implements DestinationPlugin { type: 'Dropped by plan', }) ) - return ctx } } diff --git a/packages/browser/src/plugins/remote-loader/index.ts b/packages/browser/src/plugins/remote-loader/index.ts index b0979fa19..cbf9a7565 100644 --- a/packages/browser/src/plugins/remote-loader/index.ts +++ b/packages/browser/src/plugins/remote-loader/index.ts @@ -1,7 +1,7 @@ import type { Integrations } from '../../core/events/interfaces' import { LegacySettings } from '../../browser' import { JSONObject, JSONValue } from '../../core/events' -import { DestinationPlugin, Plugin } from '../../core/plugin' +import { Plugin, InternalPluginWithAddMiddleware } from '../../core/plugin' import { loadScript } from '../../lib/load-script' import { getCDN } from '../../lib/parse-cdn' import { @@ -26,9 +26,13 @@ export interface RemotePlugin { settings: JSONObject } -export class ActionDestination implements DestinationPlugin { +export class ActionDestination implements InternalPluginWithAddMiddleware { name: string // destination name version = '1.0.0' + /** + * The lifecycle name of the wrapped plugin. + * This does not need to be 'destination', and can be 'enrichment', etc. + */ type: Plugin['type'] alternativeNames: string[] = [] @@ -47,6 +51,7 @@ export class ActionDestination implements DestinationPlugin { } addMiddleware(...fn: DestinationMiddlewareFunction[]): void { + /** Make sure we only apply destination filters to actions of the "destination" type to avoid causing issues for hybrid destinations */ if (this.type === 'destination') { this.middleware.push(...fn) } @@ -289,12 +294,7 @@ export async function remoteLoader( 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' - ) { + if (routing.length && routingMiddleware) { wrapper.addMiddleware(routingMiddleware) } diff --git a/packages/browser/src/test-helpers/fixtures/create-fetch-method.ts b/packages/browser/src/test-helpers/fixtures/create-fetch-method.ts index 85cd7e41e..46f1a4fa3 100644 --- a/packages/browser/src/test-helpers/fixtures/create-fetch-method.ts +++ b/packages/browser/src/test-helpers/fixtures/create-fetch-method.ts @@ -7,16 +7,22 @@ export const createMockFetchImplementation = ( ) => { return (...[url, req]: Parameters) => { const reqUrl = url.toString() - if (!req || (req.method === 'get' && reqUrl.includes('cdn.segment.com'))) { + const reqMethod = req?.method?.toLowerCase() + if (!req || (reqMethod === 'get' && reqUrl.includes('cdn.segment.com'))) { // GET https://cdn.segment.com/v1/projects/{writeKey} return createSuccess({ ...cdnSettingsMinimal, ...cdnSettings }) } - if (req?.method === 'post' && reqUrl.includes('api.segment.io')) { + if (reqMethod === 'post' && reqUrl.includes('api.segment.io')) { // POST https://api.segment.io/v1/{event.type} return createSuccess({ success: true }, { status: 201 }) } + if (reqMethod === 'post' && reqUrl.endsWith('/m')) { + // POST https://api.segment.io/m + return createSuccess({ success: true }) + } + throw new Error( `no match found for request (url:${url}, req:${JSON.stringify(req)})` )