diff --git a/.changeset/spotty-geese-wave.md b/.changeset/spotty-geese-wave.md new file mode 100644 index 000000000..c73a59335 --- /dev/null +++ b/.changeset/spotty-geese-wave.md @@ -0,0 +1,5 @@ +--- +'@segment/analytics-next': minor +--- + +Add ability to use browser destination straight from NPM diff --git a/packages/browser/package.json b/packages/browser/package.json index 33a4c4c50..c857dbb2e 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -62,6 +62,7 @@ }, "devDependencies": { "@internal/config": "0.0.0", + "@segment/analytics-browser-actions-braze": "^1.3.0", "@segment/analytics.js-integration": "^3.3.3", "@segment/analytics.js-integration-amplitude": "^3.3.3", "@size-limit/preset-big-lib": "^7.0.8", diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index 8ee803b3a..095b8ae99 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -10,7 +10,11 @@ import { MetricsOptions } from '../core/stats/remote-metrics' import { mergedOptions } from '../lib/merged-options' import { createDeferred } from '../lib/create-deferred' import { pageEnrichment } from '../plugins/page-enrichment' -import { remoteLoader, RemotePlugin } from '../plugins/remote-loader' +import { + PluginFactory, + remoteLoader, + RemotePlugin, +} from '../plugins/remote-loader' import type { RoutingRule } from '../plugins/routing-middleware' import { segmentio, SegmentioSettings } from '../plugins/segmentio' import { validation } from '../plugins/validation' @@ -178,9 +182,19 @@ async function registerPlugins( analytics: Analytics, opts: InitOptions, options: InitOptions, - plugins: Plugin[], + pluginLikes: (Plugin | PluginFactory)[] = [], legacyIntegrationSources: ClassicIntegrationSource[] ): Promise { + const plugins = pluginLikes?.filter( + (pluginLike) => typeof pluginLike === 'object' + ) as Plugin[] + + const pluginSources = pluginLikes?.filter( + (pluginLike) => + typeof pluginLike === 'function' && + typeof pluginLike.pluginName === 'string' + ) as PluginFactory[] + const tsubMiddleware = hasTsubMiddleware(legacySettings) ? await import( /* webpackChunkName: "tsub-middleware" */ '../plugins/routing-middleware' @@ -229,7 +243,8 @@ async function registerPlugins( analytics.integrations, mergedSettings, options.obfuscate, - tsubMiddleware + tsubMiddleware, + pluginSources ).catch(() => []) const toRegister = [ @@ -308,6 +323,7 @@ async function loadAnalytics( attachInspector(analytics) const plugins = settings.plugins ?? [] + const classicIntegrations = settings.classicIntegrations ?? [] Stats.initRemoteMetrics(legacySettings.metrics) diff --git a/packages/browser/src/core/analytics/index.ts b/packages/browser/src/core/analytics/index.ts index ef88ed131..767e7338e 100644 --- a/packages/browser/src/core/analytics/index.ts +++ b/packages/browser/src/core/analytics/index.ts @@ -52,6 +52,7 @@ import { initializeStorages, isArrayOfStoreType, } from '../storage' +import { PluginFactory } from '../../plugins/remote-loader' const deprecationWarning = 'This is being deprecated and will be not be available in future releases of Analytics JS' @@ -75,7 +76,7 @@ function createDefaultQueue( export interface AnalyticsSettings { writeKey: string timeout?: number - plugins?: Plugin[] + plugins?: (Plugin | PluginFactory)[] classicIntegrations?: ClassicIntegrationSource[] } 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 237cebfe9..95d3c0abe 100644 --- a/packages/browser/src/plugins/remote-loader/__tests__/index.test.ts +++ b/packages/browser/src/plugins/remote-loader/__tests__/index.test.ts @@ -1,5 +1,7 @@ +import braze from '@segment/analytics-browser-actions-braze' + import * as loader from '../../../lib/load-script' -import { ActionDestination, remoteLoader } from '..' +import { ActionDestination, PluginFactory, remoteLoader } from '..' import { AnalyticsBrowser, LegacySettings } from '../../../browser' import { InitOptions } from '../../../core/analytics' import { Context } from '../../../core/context' @@ -142,6 +144,60 @@ describe('Remote Loader', () => { ) }) + it('should load from given plugin sources before loading from CDN', async () => { + const brazeSpy = jest.spyOn({ braze }, 'braze') + ;(brazeSpy as any).pluginName = braze.pluginName + + await remoteLoader( + { + integrations: {}, + remotePlugins: [ + { + name: 'Braze Web Mode (Actions)', + creationName: 'Braze Web Mode (Actions)', + libraryName: 'brazeDestination', + url: 'https://cdn.segment.com/next-integrations/actions/braze/a6f95f5869852b848386.js', + settings: { + api_key: 'test-api-key', + versionSettings: { + componentTypes: [], + }, + subscriptions: [ + { + id: '3thVuvYKBcEGKEZA185Tbs', + name: 'Track Calls', + enabled: true, + partnerAction: 'trackEvent', + subscribe: 'type = "track" and event != "Order Completed"', + mapping: { + eventName: { + '@path': '$.event', + }, + eventProperties: { + '@path': '$.properties', + }, + }, + }, + ], + }, + }, + ], + }, + {}, + {}, + false, + undefined, + [brazeSpy as unknown as PluginFactory] + ) + + expect(brazeSpy).toHaveBeenCalledTimes(1) + expect(brazeSpy).toHaveBeenCalledWith( + expect.objectContaining({ + api_key: 'test-api-key', + }) + ) + }) + it('should not load remote plugins when integrations object contains all: false', async () => { await remoteLoader( { diff --git a/packages/browser/src/plugins/remote-loader/index.ts b/packages/browser/src/plugins/remote-loader/index.ts index f3d77eee4..73d797c02 100644 --- a/packages/browser/src/plugins/remote-loader/index.ts +++ b/packages/browser/src/plugins/remote-loader/index.ts @@ -110,9 +110,10 @@ export class ActionDestination implements DestinationPlugin { } } -type PluginFactory = ( - settings: JSONValue -) => Plugin | Plugin[] | Promise +export type PluginFactory = { + (settings: JSONValue): Plugin | Plugin[] | Promise + pluginName: string +} function validate(pluginLike: unknown): pluginLike is Plugin[] { if (!Array.isArray(pluginLike)) { @@ -159,47 +160,61 @@ function isPluginDisabled( return false } +async function loadPluginFactory( + remotePlugin: RemotePlugin, + obfuscate?: boolean +): Promise { + const defaultCdn = new RegExp('https://cdn.segment.(com|build)') + const cdn = getCDN() + + if (obfuscate) { + const urlSplit = remotePlugin.url.split('/') + const name = urlSplit[urlSplit.length - 2] + const obfuscatedURL = remotePlugin.url.replace( + name, + btoa(name).replace(/=/g, '') + ) + try { + await loadScript(obfuscatedURL.replace(defaultCdn, cdn)) + } catch (error) { + // Due to syncing concerns it is possible that the obfuscated action destination (or requested version) might not exist. + // We should use the unobfuscated version as a fallback. + await loadScript(remotePlugin.url.replace(defaultCdn, cdn)) + } + } else { + await loadScript(remotePlugin.url.replace(defaultCdn, cdn)) + } + + // @ts-expect-error + if (typeof window[remotePlugin.libraryName] === 'function') { + // @ts-expect-error + return window[remotePlugin.libraryName] as PluginFactory + } +} + export async function remoteLoader( settings: LegacySettings, userIntegrations: Integrations, mergedIntegrations: Record, obfuscate?: boolean, - routingMiddleware?: DestinationMiddlewareFunction + routingMiddleware?: DestinationMiddlewareFunction, + pluginSources?: PluginFactory[] ): Promise { const allPlugins: Plugin[] = [] - const cdn = getCDN() const routingRules = settings.middlewareSettings?.routingRules ?? [] const pluginPromises = (settings.remotePlugins ?? []).map( async (remotePlugin) => { if (isPluginDisabled(userIntegrations, remotePlugin)) return - try { - const defaultCdn = new RegExp('https://cdn.segment.(com|build)') - if (obfuscate) { - const urlSplit = remotePlugin.url.split('/') - const name = urlSplit[urlSplit.length - 2] - const obfuscatedURL = remotePlugin.url.replace( - name, - btoa(name).replace(/=/g, '') - ) - try { - await loadScript(obfuscatedURL.replace(defaultCdn, cdn)) - } catch (error) { - // Due to syncing concerns it is possible that the obfuscated action destination (or requested version) might not exist. - // We should use the unobfuscated version as a fallback. - await loadScript(remotePlugin.url.replace(defaultCdn, cdn)) - } - } else { - await loadScript(remotePlugin.url.replace(defaultCdn, cdn)) - } - const libraryName = remotePlugin.libraryName + try { + const pluginFactory = + pluginSources?.find( + ({ pluginName }) => pluginName === remotePlugin.name + ) || (await loadPluginFactory(remotePlugin, obfuscate)) - // @ts-expect-error - if (typeof window[libraryName] === 'function') { - // @ts-expect-error - const pluginFactory = window[libraryName] as PluginFactory + if (pluginFactory) { const plugin = await pluginFactory({ ...remotePlugin.settings, ...mergedIntegrations[remotePlugin.name], diff --git a/yarn.lock b/yarn.lock index 38cf4f0e5..0abe05c8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1897,6 +1897,20 @@ __metadata: languageName: node linkType: hard +"@braze/web-sdk-v3@npm:@braze/web-sdk@^3.5.1": + version: 3.5.1 + resolution: "@braze/web-sdk@npm:3.5.1" + checksum: 2e99f0dcc7cde62cc6cc40ddff17b008cdf0b56f61883fa9cbf3183a73a200cb166c6563cd5b4aa8ebc1dd2cfd5784c4b28375cf2b58973d2a2e4edbdc5a7d76 + languageName: node + linkType: hard + +"@braze/web-sdk@npm:@braze/web-sdk@^4.1.0": + version: 4.8.3 + resolution: "@braze/web-sdk@npm:4.8.3" + checksum: cdd1218c4654c62f15fa78e64e87269a0251f3e802c84fa5099ba167a80617dec217f8982c1193fd82687f7e1255ea02d90cdfc5ba601f32b2a3dc5619d3deac + languageName: node + linkType: hard + "@builder.io/partytown@npm:^0.7.4": version: 0.7.4 resolution: "@builder.io/partytown@npm:0.7.4" @@ -3196,6 +3210,59 @@ __metadata: languageName: node linkType: hard +"@segment/action-emitters@npm:^1.1.2": + version: 1.2.2 + resolution: "@segment/action-emitters@npm:1.2.2" + dependencies: + "@types/node": ^18.11.15 + checksum: 4db0e39c6558d5112f26b3b2c7ab88f34ee99dfa7e6cbcd6a409aa561e61bf61d909e150a4db51b00131b1fdfd415de89cb591f1ba09d888b53759980e8c66c0 + languageName: node + linkType: hard + +"@segment/actions-core@npm:^3.76.0": + version: 3.77.0 + resolution: "@segment/actions-core@npm:3.77.0" + dependencies: + "@lukeed/uuid": ^2.0.0 + "@segment/action-emitters": ^1.1.2 + "@segment/ajv-human-errors": ^2.11.0 + "@segment/destination-subscriptions": ^3.28.0 + "@types/node": ^18.11.15 + abort-controller: ^3.0.0 + aggregate-error: ^3.1.0 + ajv: ^8.6.3 + ajv-formats: ^2.1.1 + btoa-lite: ^1.0.0 + cross-fetch: ^3.1.4 + dayjs: ^1.10.7 + ts-custom-error: ^3.2.0 + checksum: 9b3bd3a3fb3c93013cd99d60881418e1af5d9735e60e1e3abed56952cbfb9432c90b499ad1b805be335c44b3373dc0cba2abc89a7a664092aeb0c6045e779478 + languageName: node + linkType: hard + +"@segment/ajv-human-errors@npm:^2.11.0": + version: 2.11.0 + resolution: "@segment/ajv-human-errors@npm:2.11.0" + peerDependencies: + ajv: ^8.0.0 + checksum: ab13c5b2d95ffacf5dc6a2b7efaf49c16e850519887d6ffdd4b3c3c266c59acf9c609df742c0705f22def902b961a694efc8dba354cac6a87c0efcb392666a1f + languageName: node + linkType: hard + +"@segment/analytics-browser-actions-braze@npm:^1.3.0": + version: 1.6.0 + resolution: "@segment/analytics-browser-actions-braze@npm:1.6.0" + dependencies: + "@braze/web-sdk": "npm:@braze/web-sdk@^4.1.0" + "@braze/web-sdk-v3": "npm:@braze/web-sdk@^3.5.1" + "@segment/actions-core": ^3.76.0 + "@segment/browser-destination-runtime": ^1.6.0 + peerDependencies: + "@segment/analytics-next": "*" + checksum: 7352cfd3c74275e0ec349dd252025c7233f781f79948b0b4c40cd7f36cff58b490b67098687de0da9ca0d4385d7540aba99519d12e79d7315352338f97984448 + languageName: node + linkType: hard + "@segment/analytics-consent-tools@0.2.0, @segment/analytics-consent-tools@workspace:^, @segment/analytics-consent-tools@workspace:packages/consent/consent-tools": version: 0.0.0-use.local resolution: "@segment/analytics-consent-tools@workspace:packages/consent/consent-tools" @@ -3242,6 +3309,7 @@ __metadata: dependencies: "@internal/config": 0.0.0 "@lukeed/uuid": ^2.0.0 + "@segment/analytics-browser-actions-braze": ^1.3.0 "@segment/analytics-core": 1.3.0 "@segment/analytics.js-integration": ^3.3.3 "@segment/analytics.js-integration-amplitude": ^3.3.3 @@ -3359,6 +3427,26 @@ __metadata: languageName: node linkType: hard +"@segment/browser-destination-runtime@npm:^1.6.0": + version: 1.6.0 + resolution: "@segment/browser-destination-runtime@npm:1.6.0" + dependencies: + "@segment/actions-core": ^3.76.0 + peerDependencies: + "@segment/analytics-next": "*" + checksum: 9ac97093ea07fc75a19c4be92a808e41413757193f19bb3f0b35716e165baaa9738f68c99dce953dc58143bb67c70ac935f5233aa9dc26cb1ee7792bddddc094 + languageName: node + linkType: hard + +"@segment/destination-subscriptions@npm:^3.28.0": + version: 3.28.0 + resolution: "@segment/destination-subscriptions@npm:3.28.0" + dependencies: + "@segment/fql-ts": ^1.10.1 + checksum: 2e4d2e56a91f2646090c7ab3df3ea966f954c69483a6c6ee49de044ffa7837b114730ef8d8b48c79da55b8c060abe9e76e1975e3d36a60abc9daba4dc80c3e33 + languageName: node + linkType: hard + "@segment/facade@npm:^3.4.9": version: 3.4.9 resolution: "@segment/facade@npm:3.4.9" @@ -3378,6 +3466,15 @@ __metadata: languageName: node linkType: hard +"@segment/fql-ts@npm:^1.10.1": + version: 1.10.1 + resolution: "@segment/fql-ts@npm:1.10.1" + bin: + fql-ts: dist/cjs/index.js + checksum: 6f0c03bc5ffcdd48b4f6d0dbfbc9332dee19dc8affd47dfd45d0866e5fd0b61890aec70c50506be368ba1d6dc49b55e164a611d039bca4867dfb69211155cfca + languageName: node + linkType: hard + "@segment/isodate-traverse@npm:^1.0.0, @segment/isodate-traverse@npm:^1.1.1": version: 1.1.1 resolution: "@segment/isodate-traverse@npm:1.1.1" @@ -5139,6 +5236,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^18.11.15": + version: 18.17.5 + resolution: "@types/node@npm:18.17.5" + checksum: b8c658a99234b99425243c324b641ed7b9ceb6bee6b06421fdc9bb7c58f9a5552e353225cc549e6982462ac384abe1985022ed76e2e4728797f59b21f659ca2b + languageName: node + linkType: hard + "@types/node@npm:^20.1.0, @types/node@npm:^20.1.1": version: 20.3.1 resolution: "@types/node@npm:20.3.1" @@ -6163,6 +6267,15 @@ __metadata: languageName: node linkType: hard +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: ^5.0.0 + checksum: 170bdba9b47b7e65906a28c8ce4f38a7a369d78e2271706f020849c1bfe0ee2067d4261df8bbb66eb84f79208fd5b710df759d64191db58cfba7ce8ef9c54b75 + languageName: node + linkType: hard + "accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" @@ -6305,7 +6418,7 @@ __metadata: languageName: node linkType: hard -"aggregate-error@npm:^3.0.0": +"aggregate-error@npm:^3.0.0, aggregate-error@npm:^3.1.0": version: 3.1.0 resolution: "aggregate-error@npm:3.1.0" dependencies: @@ -6373,7 +6486,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.9.0": +"ajv@npm:^8.0.0, ajv@npm:^8.6.3, ajv@npm:^8.9.0": version: 8.12.0 resolution: "ajv@npm:8.12.0" dependencies: @@ -7350,6 +7463,13 @@ __metadata: languageName: node linkType: hard +"btoa-lite@npm:^1.0.0": + version: 1.0.0 + resolution: "btoa-lite@npm:1.0.0" + checksum: c2d61993b801f8e35a96f20692a45459c753d9baa29d86d1343e714f8d6bbe7069f1a20a5ae868488f3fb137d5bd0c560f6fbbc90b5a71050919d2d2c97c0475 + languageName: node + linkType: hard + "buffer-alloc-unsafe@npm:^1.1.0": version: 1.1.0 resolution: "buffer-alloc-unsafe@npm:1.1.0" @@ -8443,6 +8563,15 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^3.1.4": + version: 3.1.8 + resolution: "cross-fetch@npm:3.1.8" + dependencies: + node-fetch: ^2.6.12 + checksum: 78f993fa099eaaa041122ab037fe9503ecbbcb9daef234d1d2e0b9230a983f64d645d088c464e21a247b825a08dc444a6e7064adfa93536d3a9454b4745b3632 + languageName: node + linkType: hard + "cross-spawn@npm:^4.0.2": version: 4.0.2 resolution: "cross-spawn@npm:4.0.2" @@ -8624,6 +8753,13 @@ __metadata: languageName: node linkType: hard +"dayjs@npm:^1.10.7": + version: 1.11.9 + resolution: "dayjs@npm:1.11.9" + checksum: a4844d83dc87f921348bb9b1b93af851c51e6f71fa259604809cfe1b49d1230e6b0212dab44d1cb01994c096ad3a77ea1cf18fa55154da6efcc9d3610526ac38 + languageName: node + linkType: hard + "debug@npm:2.6.9, debug@npm:^2.2.0, debug@npm:^2.6.8, debug@npm:^2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -10185,6 +10321,13 @@ __metadata: languageName: node linkType: hard +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 1ffe3bb22a6d51bdeb6bf6f7cf97d2ff4a74b017ad12284cc9e6a279e727dc30a5de6bb613e5596ff4dc3e517841339ad09a7eec44266eccb1aa201a30448166 + languageName: node + linkType: hard + "eventemitter3@npm:^4.0.0": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" @@ -15020,6 +15163,20 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^2.6.12": + version: 2.6.12 + resolution: "node-fetch@npm:2.6.12" + dependencies: + whatwg-url: ^5.0.0 + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 3bc1655203d47ee8e313c0d96664b9673a3d4dd8002740318e9d27d14ef306693a4b2ef8d6525775056fd912a19e23f3ac0d7111ad8925877b7567b29a625592 + languageName: node + linkType: hard + "node-forge@npm:^1": version: 1.3.1 resolution: "node-forge@npm:1.3.1" @@ -18780,6 +18937,13 @@ __metadata: languageName: node linkType: hard +"ts-custom-error@npm:^3.2.0": + version: 3.3.1 + resolution: "ts-custom-error@npm:3.3.1" + checksum: 50a1e825fced68d70049bd8d282379a635e43aa023a370fa8e736b12a6edba7f18a2d731fa194ac35303a8b625be56e121bdb31d8a0318250d1a8b277059fce3 + languageName: node + linkType: hard + "ts-jest@npm:^28.0.4": version: 28.0.4 resolution: "ts-jest@npm:28.0.4"