Skip to content

Commit

Permalink
refactor and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
zikaari committed Nov 22, 2022
1 parent 0d6668c commit 969854b
Show file tree
Hide file tree
Showing 8 changed files with 462 additions and 59 deletions.
1 change: 1 addition & 0 deletions packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
},
"devDependencies": {
"@internal/config": "0.0.0",
"@segment/analytics.js-integration-amplitude": "^3.3.3",
"@segment/inspector-webext": "^2.0.3",
"@size-limit/preset-big-lib": "^7.0.8",
"@types/flat": "^5.0.1",
Expand Down
56 changes: 56 additions & 0 deletions packages/browser/src/browser/__tests__/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { PriorityQueue } from '../../lib/priority-queue'
import { getCDN, setGlobalCDNUrl } from '../../lib/parse-cdn'
import { clearAjsBrowserStorage } from '../../test-helpers/browser-storage'
import { LegacyIntegrationBuilder } from '../../plugins/ajs-destination/types'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let fetchCalls: Array<any>[] = []
Expand Down Expand Up @@ -945,6 +946,11 @@ describe('.Integrations', () => {
windowSpy.mockImplementation(
() => jsd.window as unknown as Window & typeof globalThis
)

const documentSpy = jest.spyOn(global, 'document', 'get')
documentSpy.mockImplementation(
() => jsd.window.document as unknown as Document
)
})

it('lists all legacy destinations', async () => {
Expand Down Expand Up @@ -1002,6 +1008,56 @@ describe('.Integrations', () => {
}
`)
})

it('uses directly provided classic integrations without fetching them from cdn', async () => {
const amplitude = // @ts-ignore
(await import('@segment/analytics.js-integration-amplitude')).default

const intializeSpy = jest.spyOn(amplitude.prototype, 'initialize')
const trackSpy = jest.spyOn(amplitude.prototype, 'track')

const [analytics] = await AnalyticsBrowser.load(
{
writeKey,
classicIntegrations: [amplitude as unknown as LegacyIntegrationBuilder],
},
{
integrations: {
Amplitude: {
apiKey: 'abc',
},
},
}
)

await analytics.ready()
expect(intializeSpy).toHaveBeenCalledTimes(1)

await analytics.track('test event')

expect(trackSpy).toHaveBeenCalledTimes(1)
})

it('ignores directly provided classic integrations if settings for them are unavailable', async () => {
const amplitude = // @ts-ignore
(await import('@segment/analytics.js-integration-amplitude')).default

const intializeSpy = jest.spyOn(amplitude.prototype, 'initialize')
const trackSpy = jest.spyOn(amplitude.prototype, 'track')

const [analytics] = await AnalyticsBrowser.load({
writeKey,
classicIntegrations: [amplitude as unknown as LegacyIntegrationBuilder],
})

await analytics.ready()

expect(intializeSpy).not.toHaveBeenCalled()

await analytics.track('test event')

expect(trackSpy).not.toHaveBeenCalled()
})
})

describe('Options', () => {
Expand Down
17 changes: 5 additions & 12 deletions packages/browser/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ async function registerPlugins(
opts: InitOptions,
options: InitOptions,
plugins: Plugin[],
legacyIntegrationSources?: LegacyIntegrationSource[]
legacyIntegrationSources: LegacyIntegrationSource[]
): Promise<Context> {
const tsubMiddleware = hasTsubMiddleware(legacySettings)
? await import(
Expand All @@ -167,7 +167,7 @@ async function registerPlugins(
: undefined

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

const plugins = settings.plugins ?? []
const classicIntegrations = settings.classicIntegrations ?? []
Context.initMetrics(legacySettings.metrics)

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

const legacyIntegrationSources = plugins.filter(
(pluginOrIntegration) => typeof pluginOrIntegration === 'function'
) as LegacyIntegrationSource[]

const simplePlugins = plugins.filter(
(pluginOrIntegration) => typeof pluginOrIntegration !== 'function'
) as Plugin[]

const ctx = await registerPlugins(
legacySettings,
analytics,
opts,
options,
simplePlugins,
legacyIntegrationSources
plugins,
classicIntegrations
)

const search = window.location.search ?? ''
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 @@ -60,7 +60,8 @@ function createDefaultQueue(retryQueue = false, disablePersistance = false) {
export interface AnalyticsSettings {
writeKey: string
timeout?: number
plugins?: (Plugin | LegacyIntegrationSource)[]
plugins?: Plugin[]
classicIntegrations?: LegacyIntegrationSource[]
}

export interface InitOptions {
Expand Down
74 changes: 36 additions & 38 deletions packages/browser/src/plugins/ajs-destination/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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'
import { LegacyIntegrationConfiguration, LegacySettings } from '../../browser'
import { isOffline, isOnline } from '../../core/connection'
import { Context, ContextCancelation } from '../../core/context'
import { isServer } from '../../core/environment'
Expand All @@ -20,10 +20,16 @@ import {
import {
buildIntegration,
loadIntegration,
resolveIntegrationNameFromSource,
resolveVersion,
unloadIntegration,
} from './loader'
import { LegacyIntegration, LegacyIntegrationSource } from './types'
import { isPlainObject } from '@segment/analytics-core'
import {
isDisabledIntegration as shouldSkipIntegration,
isInstallableIntegration,
} from './utils'

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

Expand Down Expand Up @@ -331,56 +337,49 @@ export function ajsDestinations(
}

const routingRules = settings.middlewareSettings?.routingRules ?? []

const remoteIntegrationsConfig = settings.integrations
const localIntegrationsConfig = options.integrations
// merged remote CDN settings with user provided options
const integrationOptions = mergedOptions(settings, options ?? {}) as Record<
string,
JSONObject
>

return Object.entries(settings.integrations)
.map(([name, integrationSettings]) => {
if (name.startsWith('Segment')) {
return
}

const allDisableAndNotDefined =
globalIntegrations.All === false &&
globalIntegrations[name] === undefined

if (globalIntegrations[name] === false || allDisableAndNotDefined) {
return
}

const { type, bundlingStatus, versionSettings } = integrationSettings
// We use `!== 'unbundled'` (versus `=== 'bundled'`) to be inclusive of
// destinations without a defined value for `bundlingStatus`
const deviceMode =
bundlingStatus !== 'unbundled' &&
(type === 'browser' ||
versionSettings?.componentTypes?.includes('browser'))

// checking for iterable is a quick fix we need in place to prevent
// errors showing Iterable as a failed destiantion. Ideally, we should
// fix the Iterable metadata instead, but that's a longer process.
if ((!deviceMode && name !== 'Segment.io') || name === 'Iterable') {
return
}
const adhocIntegrationSources = legacyIntegrationSources?.reduce(
(acc, integrationSource) => ({
...acc,
[resolveIntegrationNameFromSource(integrationSource)]: integrationSource,
}),
{} as Record<string, LegacyIntegrationSource>
)

const integrationSource = legacyIntegrationSources?.find(
(integrationSource) =>
('Integration' in integrationSource
? integrationSource.Integration
: integrationSource
).prototype.name === name
const installableIntegrations = new Set([
// Remotely configured installable integrations
...Object.entries(remoteIntegrationsConfig)
.filter(([name, integrationSettings]) =>
isInstallableIntegration(name, integrationSettings)
)
.map(([name]) => name),

// Directly provided integration sources are only installable if settings for them are available
...Object.keys(adhocIntegrationSources || {}).filter(
(name) =>
isPlainObject(remoteIntegrationsConfig[name]) ||
isPlainObject(localIntegrationsConfig?.[name])
),
])

return Array.from(installableIntegrations)
.filter((name) => !shouldSkipIntegration(name, globalIntegrations))
.map((name) => {
const integrationSettings = remoteIntegrationsConfig[name]
const version = resolveVersion(integrationSettings)
const destination = new LegacyDestination(
name,
version,
integrationOptions[name],
options,
integrationSource
adhocIntegrationSources?.[name]
)

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

return destination
})
.filter((xt) => xt !== undefined) as LegacyDestination[]
}
14 changes: 12 additions & 2 deletions packages/browser/src/plugins/ajs-destination/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ function obfuscatePathName(pathName: string, obfuscate = false): string | void {
return obfuscate ? btoa(pathName).replace(/=/g, '') : undefined
}

export function resolveIntegrationNameFromSource(
integrationSource: LegacyIntegrationSource
) {
return (
'Integration' in integrationSource
? integrationSource.Integration
: integrationSource
).prototype.name
}

function recordLoadMetrics(fullPath: string, ctx: Context, name: string): void {
try {
const [metric] =
Expand Down Expand Up @@ -113,8 +123,8 @@ export function resolveVersion(
settings: LegacyIntegrationConfiguration
): string {
return (
settings.versionSettings?.override ??
settings.versionSettings?.version ??
settings?.versionSettings?.override ??
settings?.versionSettings?.version ??
'latest'
)
}
34 changes: 34 additions & 0 deletions packages/browser/src/plugins/ajs-destination/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Integrations } from '@segment/analytics-core'
import { LegacyIntegrationConfiguration } from '../..'

export const isInstallableIntegration = (
name: string,
integrationSettings: LegacyIntegrationConfiguration
) => {
const { type, bundlingStatus, versionSettings } = integrationSettings
// We use `!== 'unbundled'` (versus `=== 'bundled'`) to be inclusive of
// destinations without a defined value for `bundlingStatus`
const deviceMode =
bundlingStatus !== 'unbundled' &&
(type === 'browser' || versionSettings?.componentTypes?.includes('browser'))

// checking for iterable is a quick fix we need in place to prevent
// errors showing Iterable as a failed destiantion. Ideally, we should
// fix the Iterable metadata instead, but that's a longer process.
return (deviceMode || name === 'Segment.io') && name !== 'Iterable'
}

export const isDisabledIntegration = (
integrationName: string,
globalIntegrations: Integrations
) => {
const allDisableAndNotDefined =
globalIntegrations.All === false &&
globalIntegrations[integrationName] === undefined

return (
integrationName.startsWith('Segment') ||
globalIntegrations[integrationName] === false ||
allDisableAndNotDefined
)
}
Loading

0 comments on commit 969854b

Please sign in to comment.