Skip to content

Commit cf51cd6

Browse files
committed
Preserve raw error status messages under span streaming
1 parent 45ed4f3 commit cf51cd6

2 files changed

Lines changed: 29 additions & 3 deletions

File tree

packages/opentelemetry/src/applyOtelSpanData.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { SpanKind } from '@opentelemetry/api';
22
import { HTTP_RESPONSE_STATUS_CODE, HTTP_STATUS_CODE } from '@sentry/conventions/attributes';
33
import {
44
addNonEnumerableProperty,
5+
getClient,
6+
hasSpanStreamingEnabled,
57
SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME,
68
SEMANTIC_ATTRIBUTE_SENTRY_OP,
79
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
@@ -81,7 +83,8 @@ export function applyOtelSpanData(span: Span, options: { finalizeStatus?: boolea
8183

8284
if (options.finalizeStatus) {
8385
applyOtelCompatibilityAttributes(span, attributes);
84-
applyOtelSpanStatus(span, attributes, spanJSON.status);
86+
const client = getClient();
87+
applyOtelSpanStatus(span, attributes, spanJSON.status, !!client && hasSpanStreamingEnabled(client));
8588
}
8689

8790
// Only re-infer the name for spans branded for OTel source inference (those the provider created
@@ -103,13 +106,23 @@ export function applyOtelSpanKind(span: Span, kind: SpanKind | undefined): void
103106
addNonEnumerableProperty(span as SentrySpanWithOtelKind, 'kind', kind ?? SpanKind.INTERNAL);
104107
}
105108

106-
function applyOtelSpanStatus(span: Span, attributes: SpanAttributes, status: string | undefined): void {
109+
function applyOtelSpanStatus(
110+
span: Span,
111+
attributes: SpanAttributes,
112+
status: string | undefined,
113+
spanStreamingEnabled: boolean,
114+
): void {
107115
if (status === undefined) {
108116
span.setStatus(inferStatusFromAttributes(attributes) || { code: SPAN_STATUS_OK });
109117
return;
110118
}
111119

112-
if (status !== 'ok' && !isStatusErrorMessageValid(status)) {
120+
// Normalize a non-canonical error message to `internal_error` for the (non-streamed) transaction
121+
// `status` field, matching the OTel SDK exporter's `mapStatus`. Skip this under span streaming: the
122+
// streamed serializer preserves the raw message as `sentry.status.message` by reading the live span
123+
// status, and the OTel SDK path keeps it too because `mapStatus` maps at export without mutating the
124+
// span. Overwriting it here would replace that message with `internal_error`.
125+
if (!spanStreamingEnabled && status !== 'ok' && !isStatusErrorMessageValid(status)) {
113126
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
114127
}
115128
}

packages/opentelemetry/test/tracerProvider.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,19 @@ describe('SentryTracerProvider', () => {
192192
expect(spanToJSON(span as Span).data?.['sentry.source']).toBe('custom');
193193
});
194194

195+
it('preserves a non-canonical error status message under span streaming', () => {
196+
// Under streaming the streamed serializer surfaces the raw message as `sentry.status.message`, so
197+
// finalizing must not normalize it to `internal_error` the way it does for the non-streamed
198+
// transaction status field. Without streaming, `finalizes span statuses` covers the `internal_error` case.
199+
initTestClient({ tracesSampleRate: 1, traceLifecycle: 'stream' });
200+
const span = trace.getTracer('test').startSpan('db-error');
201+
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'Cannot enqueue Query after fatal error.' });
202+
203+
applyOtelSpanData(span as Span, { finalizeStatus: true });
204+
205+
expect(spanToJSON(span as Span).status).toBe('Cannot enqueue Query after fatal error.');
206+
});
207+
195208
it('infers route source, op, and name for HTTP server spans', () => {
196209
const span = trace.getTracer('test').startSpan('GET', {
197210
kind: SpanKind.SERVER,

0 commit comments

Comments
 (0)