Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silent-destinations-metrics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@segment/analytics-next': patch
---

Emit analytics_js.integration.invoke.error metric for destination load and build failures that were previously silent
5 changes: 4 additions & 1 deletion packages/browser/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,10 @@ async function registerPlugins(
options,
tsubMiddleware,
pluginSources
).catch(() => [])
).catch((err) => {
console.error('Failed to load remote plugins', err)
return [] as Plugin[]
})

const basePlugins = [envEnrichment, ...legacyDestinations, ...remotePlugins]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { AMPLITUDE_WRITEKEY } from '../../../test-helpers/test-writekeys'
import { PersistedPriorityQueue } from '../../../lib/priority-queue/persisted'
import * as Factory from '../../../test-helpers/factories'
import { cdnSettingsMinimal } from '../../../test-helpers/fixtures'
import * as MetricHelpers from '../../../core/stats/metric-helpers'
import * as ScriptLoader from '../../../lib/load-script'

const cdnResponse: CDNSettings = {
...cdnSettingsMinimal,
Expand Down Expand Up @@ -838,3 +840,65 @@ describe('option overrides', () => {
)
})
})

describe('load error metrics', () => {
let metricSpy: jest.SpyInstance

beforeEach(() => {
jest.restoreAllMocks()
jest.resetAllMocks()
metricSpy = jest.spyOn(MetricHelpers, 'recordIntegrationMetric')
})

it('records integration invoke error metric when loadIntegration fails', async () => {
jest
.spyOn(ScriptLoader, 'loadScript')
.mockRejectedValue(new Error('Script load failed'))

const dest = new LegacyDestination(
'Broken Integration',
'latest',
'writeKey',
{},
{}
)

const ctx = Context.system()

await expect(dest.load(ctx, {} as Analytics)).rejects.toThrow(
'Script load failed'
)

expect(metricSpy).toHaveBeenCalledWith(ctx, {
integrationName: 'Broken Integration',
methodName: 'load',
type: 'classic',
didError: true,
})
})

it('records integration invoke error metric when buildIntegration fails', async () => {
// Provide an integrationSource that will cause buildIntegration to throw
const badSource = {} as any

const dest = new LegacyDestination(
'Bad Build Integration',
'latest',
'writeKey',
{},
{},
badSource
)

const ctx = Context.system()

await expect(dest.load(ctx, {} as Analytics)).rejects.toThrow()

expect(metricSpy).toHaveBeenCalledWith(ctx, {
integrationName: 'Bad Build Integration',
methodName: 'load',
type: 'classic',
didError: true,
})
})
})
38 changes: 24 additions & 14 deletions packages/browser/src/plugins/ajs-destination/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,20 +134,30 @@ export class LegacyDestination implements InternalPluginWithAddMiddleware {
return
}

const integrationSource =
this.integrationSource ??
(await loadIntegration(
ctx,
this.name,
this.version,
this.options.obfuscate
))

this.integration = buildIntegration(
integrationSource,
this.settings,
analyticsInstance
)
try {
const integrationSource =
this.integrationSource ??
(await loadIntegration(
ctx,
this.name,
this.version,
this.options.obfuscate
))

this.integration = buildIntegration(
integrationSource,
this.settings,
analyticsInstance
)
} catch (error) {
recordIntegrationMetric(ctx, {
integrationName: this.name,
methodName: 'load',
type: 'classic',
didError: true,
})
throw error
}

this.onReady = new Promise((resolve) => {
const onReadyFn = (): void => {
Expand Down
37 changes: 37 additions & 0 deletions packages/browser/src/plugins/remote-loader/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { InitOptions } from '../../../core/analytics'
import { Context } from '../../../core/context'
import { tsubMiddleware } from '../../routing-middleware'
import { cdnSettingsMinimal } from '../../../test-helpers/fixtures'
import * as MetricHelpers from '../../../core/stats/metric-helpers'

const pluginFactory = jest.fn()

Expand Down Expand Up @@ -966,4 +967,40 @@ describe('Remote Loader', () => {

expect(newCtx.event.name).toEqual('foobar')
})

it('records integration invoke error metric when plugin fails to load', async () => {
const metricSpy = jest.spyOn(MetricHelpers, 'recordIntegrationMetric')

// @ts-expect-error not gonna return a script tag sorry
jest.spyOn(loader, 'loadScript').mockImplementation(() => {
window['flaky'] = (): never => {
throw Error('aaay')
}
return Promise.resolve(true)
})

await remoteLoader(
{
...cdnSettingsMinimal,
remotePlugins: [
{
name: 'flaky plugin',
creationName: 'Flaky Plugin',
url: 'cdn/path/to/flaky.js',
libraryName: 'flaky',
settings: {},
},
],
},
{},
{}
)

expect(metricSpy).toHaveBeenCalledWith(expect.any(Context), {
integrationName: 'flaky plugin',
methodName: 'load',
type: 'action',
didError: true,
})
})
})
6 changes: 6 additions & 0 deletions packages/browser/src/plugins/remote-loader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,12 @@ export async function remoteLoader(
}
} catch (error) {
console.warn('Failed to load Remote Plugin', error)
recordIntegrationMetric(Context.system(), {
integrationName: remotePlugin.name,
methodName: 'load',
type: 'action',
didError: true,
})
Comment on lines 311 to +317
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new error metric uses integrationName: remotePlugin.creationName, but action destination metrics elsewhere in this module use integrationName: this.action.name (i.e., the plugin's name). This means load failures will be tagged under a different integration_name than successful load/invoke metrics for the same destination, making dashboards and error-rate calculations inconsistent. Consider using remotePlugin.name here for consistency, or update the ActionDestination metric calls to use the wrapper/destination name consistently (e.g., this.name / remotePlugin.creationName).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

}
}
)
Expand Down
⚔