Skip to content

Commit 724cc53

Browse files
committed
ref(opentelemetry-node): Extract maybeCaptureExceptionForTimedEvent
Also add tests for this. This can then be reused e.g. by node-experimental in a later step.
1 parent 0532541 commit 724cc53

File tree

3 files changed

+206
-39
lines changed

3 files changed

+206
-39
lines changed

packages/opentelemetry-node/src/spanprocessor.ts

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { Context } from '@opentelemetry/api';
22
import { SpanKind, trace } from '@opentelemetry/api';
33
import type { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base';
4-
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
54
import { addGlobalEventProcessor, addTracingExtensions, getCurrentHub, Transaction } from '@sentry/core';
65
import type { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types';
7-
import { isString, logger } from '@sentry/utils';
6+
import { logger } from '@sentry/utils';
87

98
import { SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, SENTRY_TRACE_PARENT_CONTEXT_KEY } from './constants';
9+
import { maybeCaptureExceptionForTimedEvent } from './utils/captureExceptionForTimedEvent';
1010
import { isSentryRequestSpan } from './utils/isSentryRequest';
1111
import { mapOtelStatus } from './utils/mapOtelStatus';
1212
import { parseSpanDescription } from './utils/parseOtelSpanDescription';
@@ -109,44 +109,9 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
109109
return;
110110
}
111111

112+
const hub = getCurrentHub();
112113
otelSpan.events.forEach(event => {
113-
if (event.name !== 'exception') {
114-
return;
115-
}
116-
117-
const attributes = event.attributes;
118-
if (!attributes) {
119-
return;
120-
}
121-
122-
const message = attributes[SemanticAttributes.EXCEPTION_MESSAGE];
123-
const syntheticError = new Error(message as string | undefined);
124-
125-
const stack = attributes[SemanticAttributes.EXCEPTION_STACKTRACE];
126-
if (isString(stack)) {
127-
syntheticError.stack = stack;
128-
}
129-
130-
const type = attributes[SemanticAttributes.EXCEPTION_TYPE];
131-
if (isString(type)) {
132-
syntheticError.name = type;
133-
}
134-
135-
getCurrentHub().captureException(syntheticError, {
136-
captureContext: {
137-
contexts: {
138-
otel: {
139-
attributes: otelSpan.attributes,
140-
resource: otelSpan.resource.attributes,
141-
},
142-
trace: {
143-
trace_id: otelSpan.spanContext().traceId,
144-
span_id: otelSpan.spanContext().spanId,
145-
parent_span_id: otelSpan.parentSpanId,
146-
},
147-
},
148-
},
149-
});
114+
maybeCaptureExceptionForTimedEvent(hub, event, otelSpan);
150115
});
151116

152117
if (sentrySpan instanceof Transaction) {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { Span as OtelSpan, TimedEvent } from '@opentelemetry/sdk-trace-base';
2+
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
3+
import type { Hub } from '@sentry/types';
4+
import { isString } from '@sentry/utils';
5+
6+
/**
7+
* Maybe capture a Sentry exception for an OTEL timed event.
8+
* This will check if the event is exception-like and in that case capture it as an exception.
9+
*/
10+
export function maybeCaptureExceptionForTimedEvent(hub: Hub, event: TimedEvent, otelSpan?: OtelSpan): void {
11+
if (event.name !== 'exception') {
12+
return;
13+
}
14+
15+
const attributes = event.attributes;
16+
if (!attributes) {
17+
return;
18+
}
19+
20+
const message = attributes[SemanticAttributes.EXCEPTION_MESSAGE];
21+
22+
if (typeof message !== 'string') {
23+
return;
24+
}
25+
26+
const syntheticError = new Error(message);
27+
28+
const stack = attributes[SemanticAttributes.EXCEPTION_STACKTRACE];
29+
if (isString(stack)) {
30+
syntheticError.stack = stack;
31+
}
32+
33+
const type = attributes[SemanticAttributes.EXCEPTION_TYPE];
34+
if (isString(type)) {
35+
syntheticError.name = type;
36+
}
37+
38+
hub.captureException(syntheticError, {
39+
captureContext: otelSpan
40+
? {
41+
contexts: {
42+
otel: {
43+
attributes: otelSpan.attributes,
44+
resource: otelSpan.resource.attributes,
45+
},
46+
trace: {
47+
trace_id: otelSpan.spanContext().traceId,
48+
span_id: otelSpan.spanContext().spanId,
49+
parent_span_id: otelSpan.parentSpanId,
50+
},
51+
},
52+
}
53+
: undefined,
54+
});
55+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import type { Span as OtelSpan, TimedEvent } from '@opentelemetry/sdk-trace-base';
2+
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
3+
import type { Hub } from '@sentry/types';
4+
5+
import { maybeCaptureExceptionForTimedEvent } from '../../src/utils/captureExceptionForTimedEvent';
6+
7+
describe('maybeCaptureExceptionForTimedEvent', () => {
8+
it('ignores non-exception events', async () => {
9+
const event: TimedEvent = {
10+
time: [12345, 0],
11+
name: 'test event',
12+
};
13+
14+
const captureException = jest.fn();
15+
const hub = {
16+
captureException,
17+
} as unknown as Hub;
18+
19+
maybeCaptureExceptionForTimedEvent(hub, event);
20+
21+
expect(captureException).not.toHaveBeenCalled();
22+
});
23+
24+
it('ignores exception events without EXCEPTION_MESSAGE', async () => {
25+
const event: TimedEvent = {
26+
time: [12345, 0],
27+
name: 'exception',
28+
};
29+
30+
const captureException = jest.fn();
31+
const hub = {
32+
captureException,
33+
} as unknown as Hub;
34+
35+
maybeCaptureExceptionForTimedEvent(hub, event);
36+
37+
expect(captureException).not.toHaveBeenCalled();
38+
});
39+
40+
it('captures exception from event with EXCEPTION_MESSAGE', async () => {
41+
const event: TimedEvent = {
42+
time: [12345, 0],
43+
name: 'exception',
44+
attributes: {
45+
[SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message',
46+
},
47+
};
48+
49+
const captureException = jest.fn();
50+
const hub = {
51+
captureException,
52+
} as unknown as Hub;
53+
54+
maybeCaptureExceptionForTimedEvent(hub, event);
55+
56+
expect(captureException).toHaveBeenCalledTimes(1);
57+
expect(captureException).toHaveBeenCalledWith(expect.objectContaining({ message: 'test-message' }), {
58+
captureContext: undefined,
59+
});
60+
expect(captureException).toHaveBeenCalledWith(expect.any(Error), {
61+
captureContext: undefined,
62+
});
63+
});
64+
65+
it('captures stack and type, if available', async () => {
66+
const event: TimedEvent = {
67+
time: [12345, 0],
68+
name: 'exception',
69+
attributes: {
70+
[SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message',
71+
[SemanticAttributes.EXCEPTION_STACKTRACE]: 'test-stack',
72+
[SemanticAttributes.EXCEPTION_TYPE]: 'test-type',
73+
},
74+
};
75+
76+
const captureException = jest.fn();
77+
const hub = {
78+
captureException,
79+
} as unknown as Hub;
80+
81+
maybeCaptureExceptionForTimedEvent(hub, event);
82+
83+
expect(captureException).toHaveBeenCalledTimes(1);
84+
expect(captureException).toHaveBeenCalledWith(
85+
expect.objectContaining({ message: 'test-message', name: 'test-type', stack: 'test-stack' }),
86+
{
87+
captureContext: undefined,
88+
},
89+
);
90+
expect(captureException).toHaveBeenCalledWith(expect.any(Error), {
91+
captureContext: undefined,
92+
});
93+
});
94+
95+
it('captures span context, if available', async () => {
96+
const event: TimedEvent = {
97+
time: [12345, 0],
98+
name: 'exception',
99+
attributes: {
100+
[SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message',
101+
},
102+
};
103+
104+
const span = {
105+
parentSpanId: 'test-parent-span-id',
106+
attributes: {
107+
'test-attr1': 'test-value1',
108+
},
109+
resource: {
110+
attributes: {
111+
'test-attr2': 'test-value2',
112+
},
113+
},
114+
spanContext: () => {
115+
return { spanId: 'test-span-id', traceId: 'test-trace-id' };
116+
},
117+
} as unknown as OtelSpan;
118+
119+
const captureException = jest.fn();
120+
const hub = {
121+
captureException,
122+
} as unknown as Hub;
123+
124+
maybeCaptureExceptionForTimedEvent(hub, event, span);
125+
126+
expect(captureException).toHaveBeenCalledTimes(1);
127+
expect(captureException).toHaveBeenCalledWith(expect.objectContaining({ message: 'test-message' }), {
128+
captureContext: {
129+
contexts: {
130+
otel: {
131+
attributes: {
132+
'test-attr1': 'test-value1',
133+
},
134+
resource: {
135+
'test-attr2': 'test-value2',
136+
},
137+
},
138+
trace: {
139+
trace_id: 'test-trace-id',
140+
span_id: 'test-span-id',
141+
parent_span_id: 'test-parent-span-id',
142+
},
143+
},
144+
},
145+
});
146+
});
147+
});

0 commit comments

Comments
 (0)