Skip to content

Commit 969854b

Browse files
committed
refactor and add tests
1 parent 0d6668c commit 969854b

File tree

8 files changed

+462
-59
lines changed

8 files changed

+462
-59
lines changed

packages/browser/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
},
6262
"devDependencies": {
6363
"@internal/config": "0.0.0",
64+
"@segment/analytics.js-integration-amplitude": "^3.3.3",
6465
"@segment/inspector-webext": "^2.0.3",
6566
"@size-limit/preset-big-lib": "^7.0.8",
6667
"@types/flat": "^5.0.1",

packages/browser/src/browser/__tests__/integration.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { PriorityQueue } from '../../lib/priority-queue'
1919
import { getCDN, setGlobalCDNUrl } from '../../lib/parse-cdn'
2020
import { clearAjsBrowserStorage } from '../../test-helpers/browser-storage'
21+
import { LegacyIntegrationBuilder } from '../../plugins/ajs-destination/types'
2122

2223
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2324
let fetchCalls: Array<any>[] = []
@@ -945,6 +946,11 @@ describe('.Integrations', () => {
945946
windowSpy.mockImplementation(
946947
() => jsd.window as unknown as Window & typeof globalThis
947948
)
949+
950+
const documentSpy = jest.spyOn(global, 'document', 'get')
951+
documentSpy.mockImplementation(
952+
() => jsd.window.document as unknown as Document
953+
)
948954
})
949955

950956
it('lists all legacy destinations', async () => {
@@ -1002,6 +1008,56 @@ describe('.Integrations', () => {
10021008
}
10031009
`)
10041010
})
1011+
1012+
it('uses directly provided classic integrations without fetching them from cdn', async () => {
1013+
const amplitude = // @ts-ignore
1014+
(await import('@segment/analytics.js-integration-amplitude')).default
1015+
1016+
const intializeSpy = jest.spyOn(amplitude.prototype, 'initialize')
1017+
const trackSpy = jest.spyOn(amplitude.prototype, 'track')
1018+
1019+
const [analytics] = await AnalyticsBrowser.load(
1020+
{
1021+
writeKey,
1022+
classicIntegrations: [amplitude as unknown as LegacyIntegrationBuilder],
1023+
},
1024+
{
1025+
integrations: {
1026+
Amplitude: {
1027+
apiKey: 'abc',
1028+
},
1029+
},
1030+
}
1031+
)
1032+
1033+
await analytics.ready()
1034+
expect(intializeSpy).toHaveBeenCalledTimes(1)
1035+
1036+
await analytics.track('test event')
1037+
1038+
expect(trackSpy).toHaveBeenCalledTimes(1)
1039+
})
1040+
1041+
it('ignores directly provided classic integrations if settings for them are unavailable', async () => {
1042+
const amplitude = // @ts-ignore
1043+
(await import('@segment/analytics.js-integration-amplitude')).default
1044+
1045+
const intializeSpy = jest.spyOn(amplitude.prototype, 'initialize')
1046+
const trackSpy = jest.spyOn(amplitude.prototype, 'track')
1047+
1048+
const [analytics] = await AnalyticsBrowser.load({
1049+
writeKey,
1050+
classicIntegrations: [amplitude as unknown as LegacyIntegrationBuilder],
1051+
})
1052+
1053+
await analytics.ready()
1054+
1055+
expect(intializeSpy).not.toHaveBeenCalled()
1056+
1057+
await analytics.track('test event')
1058+
1059+
expect(trackSpy).not.toHaveBeenCalled()
1060+
})
10051061
})
10061062

10071063
describe('Options', () => {

packages/browser/src/browser/index.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ async function registerPlugins(
154154
opts: InitOptions,
155155
options: InitOptions,
156156
plugins: Plugin[],
157-
legacyIntegrationSources?: LegacyIntegrationSource[]
157+
legacyIntegrationSources: LegacyIntegrationSource[]
158158
): Promise<Context> {
159159
const tsubMiddleware = hasTsubMiddleware(legacySettings)
160160
? await import(
@@ -167,7 +167,7 @@ async function registerPlugins(
167167
: undefined
168168

169169
const legacyDestinations =
170-
hasLegacyDestinations(legacySettings) || legacyIntegrationSources
170+
hasLegacyDestinations(legacySettings) || legacyIntegrationSources.length > 0
171171
? await import(
172172
/* webpackChunkName: "ajs-destination" */ '../plugins/ajs-destination'
173173
).then((mod) => {
@@ -278,26 +278,19 @@ async function loadAnalytics(
278278
inspectorHost.attach?.(analytics as any)
279279

280280
const plugins = settings.plugins ?? []
281+
const classicIntegrations = settings.classicIntegrations ?? []
281282
Context.initMetrics(legacySettings.metrics)
282283

283284
// needs to be flushed before plugins are registered
284285
flushPreBuffer(analytics, preInitBuffer)
285286

286-
const legacyIntegrationSources = plugins.filter(
287-
(pluginOrIntegration) => typeof pluginOrIntegration === 'function'
288-
) as LegacyIntegrationSource[]
289-
290-
const simplePlugins = plugins.filter(
291-
(pluginOrIntegration) => typeof pluginOrIntegration !== 'function'
292-
) as Plugin[]
293-
294287
const ctx = await registerPlugins(
295288
legacySettings,
296289
analytics,
297290
opts,
298291
options,
299-
simplePlugins,
300-
legacyIntegrationSources
292+
plugins,
293+
classicIntegrations
301294
)
302295

303296
const search = window.location.search ?? ''

packages/browser/src/core/analytics/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ function createDefaultQueue(retryQueue = false, disablePersistance = false) {
6060
export interface AnalyticsSettings {
6161
writeKey: string
6262
timeout?: number
63-
plugins?: (Plugin | LegacyIntegrationSource)[]
63+
plugins?: Plugin[]
64+
classicIntegrations?: LegacyIntegrationSource[]
6465
}
6566

6667
export interface InitOptions {

packages/browser/src/plugins/ajs-destination/index.ts

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Integrations, JSONObject } from '@/core/events'
22
import { Alias, Facade, Group, Identify, Page, Track } from '@segment/facade'
33
import { Analytics, InitOptions } from '../../core/analytics'
4-
import { LegacySettings } from '../../browser'
4+
import { LegacyIntegrationConfiguration, LegacySettings } from '../../browser'
55
import { isOffline, isOnline } from '../../core/connection'
66
import { Context, ContextCancelation } from '../../core/context'
77
import { isServer } from '../../core/environment'
@@ -20,10 +20,16 @@ import {
2020
import {
2121
buildIntegration,
2222
loadIntegration,
23+
resolveIntegrationNameFromSource,
2324
resolveVersion,
2425
unloadIntegration,
2526
} from './loader'
2627
import { LegacyIntegration, LegacyIntegrationSource } from './types'
28+
import { isPlainObject } from '@segment/analytics-core'
29+
import {
30+
isDisabledIntegration as shouldSkipIntegration,
31+
isInstallableIntegration,
32+
} from './utils'
2733

2834
export type ClassType<T> = new (...args: unknown[]) => T
2935

@@ -331,56 +337,49 @@ export function ajsDestinations(
331337
}
332338

333339
const routingRules = settings.middlewareSettings?.routingRules ?? []
334-
340+
const remoteIntegrationsConfig = settings.integrations
341+
const localIntegrationsConfig = options.integrations
335342
// merged remote CDN settings with user provided options
336343
const integrationOptions = mergedOptions(settings, options ?? {}) as Record<
337344
string,
338345
JSONObject
339346
>
340347

341-
return Object.entries(settings.integrations)
342-
.map(([name, integrationSettings]) => {
343-
if (name.startsWith('Segment')) {
344-
return
345-
}
346-
347-
const allDisableAndNotDefined =
348-
globalIntegrations.All === false &&
349-
globalIntegrations[name] === undefined
350-
351-
if (globalIntegrations[name] === false || allDisableAndNotDefined) {
352-
return
353-
}
354-
355-
const { type, bundlingStatus, versionSettings } = integrationSettings
356-
// We use `!== 'unbundled'` (versus `=== 'bundled'`) to be inclusive of
357-
// destinations without a defined value for `bundlingStatus`
358-
const deviceMode =
359-
bundlingStatus !== 'unbundled' &&
360-
(type === 'browser' ||
361-
versionSettings?.componentTypes?.includes('browser'))
362-
363-
// checking for iterable is a quick fix we need in place to prevent
364-
// errors showing Iterable as a failed destiantion. Ideally, we should
365-
// fix the Iterable metadata instead, but that's a longer process.
366-
if ((!deviceMode && name !== 'Segment.io') || name === 'Iterable') {
367-
return
368-
}
348+
const adhocIntegrationSources = legacyIntegrationSources?.reduce(
349+
(acc, integrationSource) => ({
350+
...acc,
351+
[resolveIntegrationNameFromSource(integrationSource)]: integrationSource,
352+
}),
353+
{} as Record<string, LegacyIntegrationSource>
354+
)
369355

370-
const integrationSource = legacyIntegrationSources?.find(
371-
(integrationSource) =>
372-
('Integration' in integrationSource
373-
? integrationSource.Integration
374-
: integrationSource
375-
).prototype.name === name
356+
const installableIntegrations = new Set([
357+
// Remotely configured installable integrations
358+
...Object.entries(remoteIntegrationsConfig)
359+
.filter(([name, integrationSettings]) =>
360+
isInstallableIntegration(name, integrationSettings)
376361
)
362+
.map(([name]) => name),
363+
364+
// Directly provided integration sources are only installable if settings for them are available
365+
...Object.keys(adhocIntegrationSources || {}).filter(
366+
(name) =>
367+
isPlainObject(remoteIntegrationsConfig[name]) ||
368+
isPlainObject(localIntegrationsConfig?.[name])
369+
),
370+
])
371+
372+
return Array.from(installableIntegrations)
373+
.filter((name) => !shouldSkipIntegration(name, globalIntegrations))
374+
.map((name) => {
375+
const integrationSettings = remoteIntegrationsConfig[name]
377376
const version = resolveVersion(integrationSettings)
378377
const destination = new LegacyDestination(
379378
name,
380379
version,
381380
integrationOptions[name],
382381
options,
383-
integrationSource
382+
adhocIntegrationSources?.[name]
384383
)
385384

386385
const routing = routingRules.filter(
@@ -392,5 +391,4 @@ export function ajsDestinations(
392391

393392
return destination
394393
})
395-
.filter((xt) => xt !== undefined) as LegacyDestination[]
396394
}

packages/browser/src/plugins/ajs-destination/loader.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ function obfuscatePathName(pathName: string, obfuscate = false): string | void {
1818
return obfuscate ? btoa(pathName).replace(/=/g, '') : undefined
1919
}
2020

21+
export function resolveIntegrationNameFromSource(
22+
integrationSource: LegacyIntegrationSource
23+
) {
24+
return (
25+
'Integration' in integrationSource
26+
? integrationSource.Integration
27+
: integrationSource
28+
).prototype.name
29+
}
30+
2131
function recordLoadMetrics(fullPath: string, ctx: Context, name: string): void {
2232
try {
2333
const [metric] =
@@ -113,8 +123,8 @@ export function resolveVersion(
113123
settings: LegacyIntegrationConfiguration
114124
): string {
115125
return (
116-
settings.versionSettings?.override ??
117-
settings.versionSettings?.version ??
126+
settings?.versionSettings?.override ??
127+
settings?.versionSettings?.version ??
118128
'latest'
119129
)
120130
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Integrations } from '@segment/analytics-core'
2+
import { LegacyIntegrationConfiguration } from '../..'
3+
4+
export const isInstallableIntegration = (
5+
name: string,
6+
integrationSettings: LegacyIntegrationConfiguration
7+
) => {
8+
const { type, bundlingStatus, versionSettings } = integrationSettings
9+
// We use `!== 'unbundled'` (versus `=== 'bundled'`) to be inclusive of
10+
// destinations without a defined value for `bundlingStatus`
11+
const deviceMode =
12+
bundlingStatus !== 'unbundled' &&
13+
(type === 'browser' || versionSettings?.componentTypes?.includes('browser'))
14+
15+
// checking for iterable is a quick fix we need in place to prevent
16+
// errors showing Iterable as a failed destiantion. Ideally, we should
17+
// fix the Iterable metadata instead, but that's a longer process.
18+
return (deviceMode || name === 'Segment.io') && name !== 'Iterable'
19+
}
20+
21+
export const isDisabledIntegration = (
22+
integrationName: string,
23+
globalIntegrations: Integrations
24+
) => {
25+
const allDisableAndNotDefined =
26+
globalIntegrations.All === false &&
27+
globalIntegrations[integrationName] === undefined
28+
29+
return (
30+
integrationName.startsWith('Segment') ||
31+
globalIntegrations[integrationName] === false ||
32+
allDisableAndNotDefined
33+
)
34+
}

0 commit comments

Comments
 (0)