Skip to content

Commit

Permalink
fix(tracing): namespace attributes in build error events and add furt…
Browse files Browse the repository at this point in the history
…her data (#5381)

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
JGAntunes and kodiakhq[bot] authored Nov 13, 2023
1 parent c580c4d commit 795d935
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 4 deletions.
83 changes: 83 additions & 0 deletions packages/build/src/error/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Attributes } from '@opentelemetry/api'

// We override errorProps and title through getTitle and getErrorProps
export type BuildError = Omit<BasicErrorInfo, 'errorProps'> & {
title: string
Expand Down Expand Up @@ -75,15 +77,29 @@ export type BuildCommandLocation = {
buildCommandOrigin: string
}

export const isBuildCommandLocation = function (location?: ErrorLocation): location is BuildCommandLocation {
const buildLocation = location as BuildCommandLocation
return typeof buildLocation?.buildCommand === 'string' && typeof buildLocation?.buildCommandOrigin === 'string'
}

export type FunctionsBundlingLocation = {
functionName: string
functionType: string
}

export const isFunctionsBundlingLocation = function (location?: ErrorLocation): location is FunctionsBundlingLocation {
const bundlingLocation = location as FunctionsBundlingLocation
return typeof bundlingLocation?.functionName === 'string' && typeof bundlingLocation?.functionType === 'string'
}

export type CoreStepLocation = {
coreStepName: string
}

export const isCoreStepLocation = function (location?: ErrorLocation): location is CoreStepLocation {
return typeof (location as CoreStepLocation)?.coreStepName === 'string'
}

export type PluginLocation = {
event: string
packageName: string
Expand All @@ -92,18 +108,85 @@ export type PluginLocation = {
input?: string
}

export const isPluginLocation = function (location?: ErrorLocation): location is PluginLocation {
const pluginLocation = location as PluginLocation
return (
typeof pluginLocation?.event === 'string' &&
typeof pluginLocation?.packageName === 'string' &&
typeof pluginLocation?.loadedFrom === 'string'
)
}

export type APILocation = {
endpoint: string
parameters?: any
}

export const isAPILocation = function (location?: ErrorLocation): location is APILocation {
return typeof (location as APILocation)?.endpoint === 'string'
}

export type ErrorLocation =
| BuildCommandLocation
| FunctionsBundlingLocation
| CoreStepLocation
| PluginLocation
| APILocation

const buildErrorAttributePrefix = 'build.error'

const errorLocationToTracingAttributes = function (location: ErrorLocation): Attributes {
const locationAttributePrefix = `${buildErrorAttributePrefix}.location`
if (isBuildCommandLocation(location)) {
return {
[`${locationAttributePrefix}.command`]: location.buildCommand,
[`${locationAttributePrefix}.command_origin`]: location.buildCommandOrigin,
}
}
if (isPluginLocation(location)) {
return {
[`${locationAttributePrefix}.plugin.event`]: location.event,
[`${locationAttributePrefix}.plugin.package_name`]: location.packageName,
[`${locationAttributePrefix}.plugin.loaded_from`]: location.loadedFrom,
[`${locationAttributePrefix}.plugin.origin`]: location.origin,
}
}
if (isFunctionsBundlingLocation(location)) {
return {
[`${locationAttributePrefix}.function.type`]: location.functionType,
[`${locationAttributePrefix}.function.name`]: location.functionName,
}
}

if (isCoreStepLocation(location)) {
return {
[`${locationAttributePrefix}.core_step.name`]: location.coreStepName,
}
}

if (isAPILocation(location)) {
return {
[`${locationAttributePrefix}.api.endpoint`]: location.endpoint,
}
}
return {}
}

/**
* Given a BuildError, extract the relevant trace attributes to add to the on-going Span
*/
export const buildErrorToTracingAttributes = function (error: BuildError | BasicErrorInfo): Attributes {
const attributes = {}
// Check we're not adding undefined values
if (error?.severity) attributes[`${buildErrorAttributePrefix}.severity`] = error.severity
if (error?.type) attributes[`${buildErrorAttributePrefix}.type`] = error.type
if (error?.locationType) attributes[`${buildErrorAttributePrefix}.location.type`] = error.locationType
return {
...attributes,
...errorLocationToTracingAttributes(error.errorInfo?.location),
}
}

/**
* Retrieve error-type specific information
*/
Expand Down
7 changes: 4 additions & 3 deletions packages/build/src/tracing/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { NodeSDK } from '@opentelemetry/sdk-node'
import type { TracingOptions } from '../core/types.js'
import { isBuildError } from '../error/info.js'
import { parseErrorInfo } from '../error/parse/parse.js'
import { buildErrorToTracingAttributes } from '../error/types.js'
import { ROOT_PACKAGE_JSON } from '../utils/json.js'

let sdk: NodeSDK | undefined
Expand Down Expand Up @@ -94,9 +95,9 @@ export const addErrorToActiveSpan = function (error: Error) {
const span = trace.getActiveSpan()
if (!span) return
if (isBuildError(error)) {
const { severity, type } = parseErrorInfo(error)
if (severity == 'none') return
span.setAttributes({ severity, type })
const buildError = parseErrorInfo(error)
if (buildError.severity == 'none') return
span.setAttributes(buildErrorToTracingAttributes(buildError))
}

span.recordException(error)
Expand Down
104 changes: 104 additions & 0 deletions packages/build/tests/error/tests.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Fixture, normalizeOutput } from '@netlify/testing'
import test from 'ava'

import { buildErrorToTracingAttributes } from '../../lib/error/types.js'

test('exception', async (t) => {
const output = await new Fixture('./fixtures/exception').runWithBuild()
t.snapshot(normalizeOutput(output))
Expand Down Expand Up @@ -110,3 +112,105 @@ test('Redact API token on errors', async (t) => {
.runWithBuild()
t.snapshot(normalizeOutput(output))
})

const testMatrixAttributeTracing = [
{
description: 'build command error',
input: {
errorInfo: { location: { buildCommand: 'test-build', buildCommandOrigin: 'test-origin' } },
severity: 'error',
type: 'build-cmd',
locationType: 'build-cmd-location-type',
},
expects: {
'build.error.severity': 'error',
'build.error.type': 'build-cmd',
'build.error.location.type': 'build-cmd-location-type',
'build.error.location.command': 'test-build',
'build.error.location.command_origin': 'test-origin',
},
},
{
description: 'plugin error',
input: {
errorInfo: {
location: {
event: 'test-event',
packageName: 'test-package',
loadedFrom: 'test-loaded-from',
origin: 'test-origin',
},
},
severity: 'error',
type: 'plugin-error',
locationType: 'plugin-error-location-type',
},
expects: {
'build.error.severity': 'error',
'build.error.type': 'plugin-error',
'build.error.location.type': 'plugin-error-location-type',
'build.error.location.plugin.event': 'test-event',
'build.error.location.plugin.package_name': 'test-package',
'build.error.location.plugin.loaded_from': 'test-loaded-from',
'build.error.location.plugin.origin': 'test-origin',
},
},
{
description: 'functions bundling error',
input: {
errorInfo: { location: { functionType: 'function-type', functionName: 'function-name' } },
severity: 'error',
type: 'func-bundle',
locationType: 'func-bundle-location-type',
},
expects: {
'build.error.severity': 'error',
'build.error.type': 'func-bundle',
'build.error.location.type': 'func-bundle-location-type',
'build.error.location.function.type': 'function-type',
'build.error.location.function.name': 'function-name',
},
},
{
description: 'core step error',
input: {
errorInfo: { location: { coreStepName: 'some-name' } },
severity: 'error',
type: 'core-step',
locationType: 'core-step-location-type',
},
expects: {
'build.error.severity': 'error',
'build.error.type': 'core-step',
'build.error.location.type': 'core-step-location-type',
'build.error.location.core_step.name': 'some-name',
},
},
{
description: 'api error',
input: {
errorInfo: { location: { endpoint: 'some-endpoint' } },
severity: 'error',
type: 'api',
locationType: 'api-location-type',
},
expects: {
'build.error.severity': 'error',
'build.error.type': 'api',
'build.error.location.type': 'api-location-type',
'build.error.location.api.endpoint': 'some-endpoint',
},
},
{
description: 'nothing is added',
input: {},
expects: {},
},
]

testMatrixAttributeTracing.forEach(({ description, input, expects }) => {
test(`Tracing attributes - ${description}`, async (t) => {
const attributes = buildErrorToTracingAttributes(input)
t.deepEqual(attributes, expects)
})
})
6 changes: 5 additions & 1 deletion packages/build/tests/tracing/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,11 @@ test('addErrorToActiveSpan - when error severity info', async (t) => {

t.is(span.status.code, SpanStatusCode.ERROR)
// Severities are infered from the Error Type
t.deepEqual(span.attributes, { severity: 'info', type: 'failPlugin' })
t.deepEqual(span.attributes, {
'build.error.location.type': 'buildFail',
'build.error.severity': 'info',
'build.error.type': 'failPlugin',
})

const firstEvent = span.events[0]
t.deepEqual(firstEvent.name, 'exception')
Expand Down

0 comments on commit 795d935

Please sign in to comment.