Skip to content

Commit

Permalink
action destinations from npm (#931)
Browse files Browse the repository at this point in the history
Co-authored-by: Neek Sandhu <[email protected]>
  • Loading branch information
silesky and zikaari authored Aug 31, 2023
1 parent 244451e commit 9123c0c
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 36 deletions.
5 changes: 5 additions & 0 deletions .changeset/spotty-geese-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@segment/analytics-next': minor
---

Add ability to use browser destination straight from NPM
1 change: 1 addition & 0 deletions packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 19 additions & 3 deletions packages/browser/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -178,9 +182,19 @@ async function registerPlugins(
analytics: Analytics,
opts: InitOptions,
options: InitOptions,
plugins: Plugin[],
pluginLikes: (Plugin | PluginFactory)[] = [],
legacyIntegrationSources: ClassicIntegrationSource[]
): Promise<Context> {
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'
Expand Down Expand Up @@ -229,7 +243,8 @@ async function registerPlugins(
analytics.integrations,
mergedSettings,
options.obfuscate,
tsubMiddleware
tsubMiddleware,
pluginSources
).catch(() => [])

const toRegister = [
Expand Down Expand Up @@ -308,6 +323,7 @@ async function loadAnalytics(
attachInspector(analytics)

const plugins = settings.plugins ?? []

const classicIntegrations = settings.classicIntegrations ?? []
Stats.initRemoteMetrics(legacySettings.metrics)

Expand Down
3 changes: 2 additions & 1 deletion packages/browser/src/core/analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -75,7 +76,7 @@ function createDefaultQueue(
export interface AnalyticsSettings {
writeKey: string
timeout?: number
plugins?: Plugin[]
plugins?: (Plugin | PluginFactory)[]
classicIntegrations?: ClassicIntegrationSource[]
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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(
{
Expand Down
73 changes: 44 additions & 29 deletions packages/browser/src/plugins/remote-loader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,10 @@ export class ActionDestination implements DestinationPlugin {
}
}

type PluginFactory = (
settings: JSONValue
) => Plugin | Plugin[] | Promise<Plugin | Plugin[]>
export type PluginFactory = {
(settings: JSONValue): Plugin | Plugin[] | Promise<Plugin | Plugin[]>
pluginName: string
}

function validate(pluginLike: unknown): pluginLike is Plugin[] {
if (!Array.isArray(pluginLike)) {
Expand Down Expand Up @@ -159,47 +160,61 @@ function isPluginDisabled(
return false
}

async function loadPluginFactory(
remotePlugin: RemotePlugin,
obfuscate?: boolean
): Promise<void | PluginFactory> {
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<string, JSONObject>,
obfuscate?: boolean,
routingMiddleware?: DestinationMiddlewareFunction
routingMiddleware?: DestinationMiddlewareFunction,
pluginSources?: PluginFactory[]
): Promise<Plugin[]> {
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],
Expand Down
Loading

0 comments on commit 9123c0c

Please sign in to comment.