diff --git a/packages/core/src/tracing/spanstatus.ts b/packages/core/src/tracing/spanstatus.ts index b1fd046f10c3..81983e1ad599 100644 --- a/packages/core/src/tracing/spanstatus.ts +++ b/packages/core/src/tracing/spanstatus.ts @@ -10,6 +10,7 @@ export const SPAN_STATUS_ERROR = 2; * @param httpStatus The HTTP response status code. * @returns The span status or unknown_error. */ +// https://develop.sentry.dev/sdk/event-payloads/span/ export function getSpanStatusFromHttpCode(httpStatus: number): SpanStatus { if (httpStatus < 400 && httpStatus >= 100) { return { code: SPAN_STATUS_OK }; @@ -29,6 +30,8 @@ export function getSpanStatusFromHttpCode(httpStatus: number): SpanStatus { return { code: SPAN_STATUS_ERROR, message: 'failed_precondition' }; case 429: return { code: SPAN_STATUS_ERROR, message: 'resource_exhausted' }; + case 499: + return { code: SPAN_STATUS_ERROR, message: 'cancelled' }; default: return { code: SPAN_STATUS_ERROR, message: 'invalid_argument' }; } diff --git a/packages/opentelemetry/src/utils/mapStatus.ts b/packages/opentelemetry/src/utils/mapStatus.ts index ccde1c7ce178..ab40589e813c 100644 --- a/packages/opentelemetry/src/utils/mapStatus.ts +++ b/packages/opentelemetry/src/utils/mapStatus.ts @@ -1,28 +1,13 @@ import { SpanStatusCode } from '@opentelemetry/api'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { SPAN_STATUS_ERROR, SPAN_STATUS_OK } from '@sentry/core'; +import { SPAN_STATUS_ERROR, SPAN_STATUS_OK, getSpanStatusFromHttpCode } from '@sentry/core'; import type { SpanStatus } from '@sentry/types'; import type { AbstractSpan } from '../types'; import { spanHasAttributes, spanHasStatus } from './spanTypes'; -// canonicalCodesHTTPMap maps some HTTP codes to Sentry's span statuses. See possible mapping in https://develop.sentry.dev/sdk/event-payloads/span/ -const canonicalCodesHTTPMap: Record = { - '400': 'failed_precondition', - '401': 'unauthenticated', - '403': 'permission_denied', - '404': 'not_found', - '409': 'aborted', - '429': 'resource_exhausted', - '499': 'cancelled', - '500': 'internal_error', - '501': 'unimplemented', - '503': 'unavailable', - '504': 'deadline_exceeded', -} as const; - // canonicalCodesGrpcMap maps some GRPC codes to Sentry's span statuses. See description in grpc documentation. -const canonicalCodesGrpcMap: Record = { +const canonicalGrpcErrorCodesMap: Record = { '1': 'cancelled', '2': 'unknown_error', '3': 'invalid_argument', @@ -48,28 +33,40 @@ export function mapStatus(span: AbstractSpan): SpanStatus { const attributes = spanHasAttributes(span) ? span.attributes : {}; const status = spanHasStatus(span) ? span.status : undefined; - const httpCode = attributes[SemanticAttributes.HTTP_STATUS_CODE]; - const grpcCode = attributes[SemanticAttributes.RPC_GRPC_STATUS_CODE]; - - const code = typeof httpCode === 'string' ? httpCode : typeof httpCode === 'number' ? httpCode.toString() : undefined; - if (code) { - const sentryStatus = canonicalCodesHTTPMap[code]; - if (sentryStatus) { - return { code: SPAN_STATUS_ERROR, message: sentryStatus }; + if (status) { + // Since span status OK is not set by default, we give it priority: https://opentelemetry.io/docs/concepts/signals/traces/#span-status + if (status.code === SpanStatusCode.OK) { + return { code: SPAN_STATUS_OK }; + // If the span is already marked as erroneous we return that exact status + } else if (status.code === SpanStatusCode.ERROR) { + return { code: SPAN_STATUS_ERROR, message: status.message }; } } - if (typeof grpcCode === 'string') { - const sentryStatus = canonicalCodesGrpcMap[grpcCode]; - if (sentryStatus) { - return { code: SPAN_STATUS_ERROR, message: sentryStatus }; - } + // If the span status is UNSET, we try to infer it from HTTP or GRPC status codes. + + const httpCodeAttribute = attributes[SemanticAttributes.HTTP_STATUS_CODE]; + const grpcCodeAttribute = attributes[SemanticAttributes.RPC_GRPC_STATUS_CODE]; + + const numberHttpCode = + typeof httpCodeAttribute === 'number' + ? httpCodeAttribute + : typeof httpCodeAttribute === 'string' + ? parseInt(httpCodeAttribute) + : undefined; + + if (numberHttpCode) { + return getSpanStatusFromHttpCode(numberHttpCode); } - const statusCode = status && status.code; - if (statusCode === SpanStatusCode.OK || statusCode === SpanStatusCode.UNSET) { - return { code: SPAN_STATUS_OK }; + if (typeof grpcCodeAttribute === 'string') { + return { code: SPAN_STATUS_ERROR, message: canonicalGrpcErrorCodesMap[grpcCodeAttribute] || 'unknown_error' }; } - return { code: SPAN_STATUS_ERROR, message: 'unknown_error' }; + // We default to setting the spans status to ok. + if (status && status.code === SpanStatusCode.UNSET) { + return { code: SPAN_STATUS_OK }; + } else { + return { code: SPAN_STATUS_ERROR, message: 'unknown_error' }; + } } diff --git a/packages/opentelemetry/test/utils/mapStatus.test.ts b/packages/opentelemetry/test/utils/mapStatus.test.ts index eb9182973d23..5b9e65e36d76 100644 --- a/packages/opentelemetry/test/utils/mapStatus.test.ts +++ b/packages/opentelemetry/test/utils/mapStatus.test.ts @@ -6,79 +6,94 @@ import { mapStatus } from '../../src/utils/mapStatus'; import { createSpan } from '../helpers/createSpan'; describe('mapStatus', () => { - const statusTestTable: [number, undefined | number | string, undefined | string, SpanStatus][] = [ - [-1, undefined, undefined, { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], - [3, undefined, undefined, { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], - [0, undefined, undefined, { code: SPAN_STATUS_OK }], - [1, undefined, undefined, { code: SPAN_STATUS_OK }], - [2, undefined, undefined, { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], - + const statusTestTable: [undefined | number | string, undefined | string, SpanStatus][] = [ // http codes - [2, 400, undefined, { code: SPAN_STATUS_ERROR, message: 'failed_precondition' }], - [2, 401, undefined, { code: SPAN_STATUS_ERROR, message: 'unauthenticated' }], - [2, 403, undefined, { code: SPAN_STATUS_ERROR, message: 'permission_denied' }], - [2, 404, undefined, { code: SPAN_STATUS_ERROR, message: 'not_found' }], - [2, 409, undefined, { code: SPAN_STATUS_ERROR, message: 'aborted' }], - [2, 429, undefined, { code: SPAN_STATUS_ERROR, message: 'resource_exhausted' }], - [2, 499, undefined, { code: SPAN_STATUS_ERROR, message: 'cancelled' }], - [2, 500, undefined, { code: SPAN_STATUS_ERROR, message: 'internal_error' }], - [2, 501, undefined, { code: SPAN_STATUS_ERROR, message: 'unimplemented' }], - [2, 503, undefined, { code: SPAN_STATUS_ERROR, message: 'unavailable' }], - [2, 504, undefined, { code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }], - [2, 999, undefined, { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], + [400, undefined, { code: SPAN_STATUS_ERROR, message: 'invalid_argument' }], + [401, undefined, { code: SPAN_STATUS_ERROR, message: 'unauthenticated' }], + [403, undefined, { code: SPAN_STATUS_ERROR, message: 'permission_denied' }], + [404, undefined, { code: SPAN_STATUS_ERROR, message: 'not_found' }], + [409, undefined, { code: SPAN_STATUS_ERROR, message: 'already_exists' }], + [429, undefined, { code: SPAN_STATUS_ERROR, message: 'resource_exhausted' }], + [499, undefined, { code: SPAN_STATUS_ERROR, message: 'cancelled' }], + [500, undefined, { code: SPAN_STATUS_ERROR, message: 'internal_error' }], + [501, undefined, { code: SPAN_STATUS_ERROR, message: 'unimplemented' }], + [503, undefined, { code: SPAN_STATUS_ERROR, message: 'unavailable' }], + [504, undefined, { code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }], + [999, undefined, { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], - [2, '400', undefined, { code: SPAN_STATUS_ERROR, message: 'failed_precondition' }], - [2, '401', undefined, { code: SPAN_STATUS_ERROR, message: 'unauthenticated' }], - [2, '403', undefined, { code: SPAN_STATUS_ERROR, message: 'permission_denied' }], - [2, '404', undefined, { code: SPAN_STATUS_ERROR, message: 'not_found' }], - [2, '409', undefined, { code: SPAN_STATUS_ERROR, message: 'aborted' }], - [2, '429', undefined, { code: SPAN_STATUS_ERROR, message: 'resource_exhausted' }], - [2, '499', undefined, { code: SPAN_STATUS_ERROR, message: 'cancelled' }], - [2, '500', undefined, { code: SPAN_STATUS_ERROR, message: 'internal_error' }], - [2, '501', undefined, { code: SPAN_STATUS_ERROR, message: 'unimplemented' }], - [2, '503', undefined, { code: SPAN_STATUS_ERROR, message: 'unavailable' }], - [2, '504', undefined, { code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }], - [2, '999', undefined, { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], + ['400', undefined, { code: SPAN_STATUS_ERROR, message: 'invalid_argument' }], + ['401', undefined, { code: SPAN_STATUS_ERROR, message: 'unauthenticated' }], + ['403', undefined, { code: SPAN_STATUS_ERROR, message: 'permission_denied' }], + ['404', undefined, { code: SPAN_STATUS_ERROR, message: 'not_found' }], + ['409', undefined, { code: SPAN_STATUS_ERROR, message: 'already_exists' }], + ['429', undefined, { code: SPAN_STATUS_ERROR, message: 'resource_exhausted' }], + ['499', undefined, { code: SPAN_STATUS_ERROR, message: 'cancelled' }], + ['500', undefined, { code: SPAN_STATUS_ERROR, message: 'internal_error' }], + ['501', undefined, { code: SPAN_STATUS_ERROR, message: 'unimplemented' }], + ['503', undefined, { code: SPAN_STATUS_ERROR, message: 'unavailable' }], + ['504', undefined, { code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }], + ['999', undefined, { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], // grpc codes - [2, undefined, '1', { code: SPAN_STATUS_ERROR, message: 'cancelled' }], - [2, undefined, '2', { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], - [2, undefined, '3', { code: SPAN_STATUS_ERROR, message: 'invalid_argument' }], - [2, undefined, '4', { code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }], - [2, undefined, '5', { code: SPAN_STATUS_ERROR, message: 'not_found' }], - [2, undefined, '6', { code: SPAN_STATUS_ERROR, message: 'already_exists' }], - [2, undefined, '7', { code: SPAN_STATUS_ERROR, message: 'permission_denied' }], - [2, undefined, '8', { code: SPAN_STATUS_ERROR, message: 'resource_exhausted' }], - [2, undefined, '9', { code: SPAN_STATUS_ERROR, message: 'failed_precondition' }], - [2, undefined, '10', { code: SPAN_STATUS_ERROR, message: 'aborted' }], - [2, undefined, '11', { code: SPAN_STATUS_ERROR, message: 'out_of_range' }], - [2, undefined, '12', { code: SPAN_STATUS_ERROR, message: 'unimplemented' }], - [2, undefined, '13', { code: SPAN_STATUS_ERROR, message: 'internal_error' }], - [2, undefined, '14', { code: SPAN_STATUS_ERROR, message: 'unavailable' }], - [2, undefined, '15', { code: SPAN_STATUS_ERROR, message: 'data_loss' }], - [2, undefined, '16', { code: SPAN_STATUS_ERROR, message: 'unauthenticated' }], - [2, undefined, '999', { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], + [undefined, '1', { code: SPAN_STATUS_ERROR, message: 'cancelled' }], + [undefined, '2', { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], + [undefined, '3', { code: SPAN_STATUS_ERROR, message: 'invalid_argument' }], + [undefined, '4', { code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }], + [undefined, '5', { code: SPAN_STATUS_ERROR, message: 'not_found' }], + [undefined, '6', { code: SPAN_STATUS_ERROR, message: 'already_exists' }], + [undefined, '7', { code: SPAN_STATUS_ERROR, message: 'permission_denied' }], + [undefined, '8', { code: SPAN_STATUS_ERROR, message: 'resource_exhausted' }], + [undefined, '9', { code: SPAN_STATUS_ERROR, message: 'failed_precondition' }], + [undefined, '10', { code: SPAN_STATUS_ERROR, message: 'aborted' }], + [undefined, '11', { code: SPAN_STATUS_ERROR, message: 'out_of_range' }], + [undefined, '12', { code: SPAN_STATUS_ERROR, message: 'unimplemented' }], + [undefined, '13', { code: SPAN_STATUS_ERROR, message: 'internal_error' }], + [undefined, '14', { code: SPAN_STATUS_ERROR, message: 'unavailable' }], + [undefined, '15', { code: SPAN_STATUS_ERROR, message: 'data_loss' }], + [undefined, '16', { code: SPAN_STATUS_ERROR, message: 'unauthenticated' }], + [undefined, '999', { code: SPAN_STATUS_ERROR, message: 'unknown_error' }], // http takes precedence over grpc - [2, '400', '2', { code: SPAN_STATUS_ERROR, message: 'failed_precondition' }], + ['400', '2', { code: SPAN_STATUS_ERROR, message: 'invalid_argument' }], ]; - it.each(statusTestTable)( - 'works with otelStatus=%i, httpCode=%s, grpcCode=%s', - (otelStatus, httpCode, grpcCode, expected) => { - const span = createSpan(); - span.setStatus({ code: otelStatus }); + it.each(statusTestTable)('works with httpCode=%s, grpcCode=%s', (httpCode, grpcCode, expected) => { + const span = createSpan(); + span.setStatus({ code: 0 }); // UNSET + + if (httpCode) { + span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, httpCode); + } + + if (grpcCode) { + span.setAttribute(SemanticAttributes.RPC_GRPC_STATUS_CODE, grpcCode); + } + + const actual = mapStatus(span); + expect(actual).toEqual(expected); + }); + + it('returns ok span status when is UNSET present on span', () => { + const span = createSpan(); + span.setStatus({ code: 0 }); // UNSET + expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_OK }); + }); - if (httpCode) { - span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, httpCode); - } + it('returns ok span status when already present on span', () => { + const span = createSpan(); + span.setStatus({ code: 1 }); // OK + expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_OK }); + }); - if (grpcCode) { - span.setAttribute(SemanticAttributes.RPC_GRPC_STATUS_CODE, grpcCode); - } + it('returns error status when span already has error status', () => { + const span = createSpan(); + span.setStatus({ code: 2, message: 'invalid_argument' }); // ERROR + expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_ERROR, message: 'invalid_argument' }); + }); - const actual = mapStatus(span); - expect(actual).toEqual(expected); - }, - ); + it('returns unknown error status when code is unknown', () => { + const span = createSpan(); + span.setStatus({ code: -1 as 0 }); + expect(mapStatus(span)).toEqual({ code: SPAN_STATUS_ERROR, message: 'unknown_error' }); + }); }); diff --git a/packages/remix/test/integration/test/server/action.test.ts b/packages/remix/test/integration/test/server/action.test.ts index 884c11c64657..fe0b4a749c43 100644 --- a/packages/remix/test/integration/test/server/action.test.ts +++ b/packages/remix/test/integration/test/server/action.test.ts @@ -61,7 +61,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada assertSentryTransaction(transaction[2], { contexts: { trace: { - status: 'unknown_error', + status: 'internal_error', data: { 'http.response.status_code': 500, }, @@ -169,7 +169,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada contexts: { trace: { op: 'http.server', - status: 'unknown_error', + status: 'internal_error', data: { method: 'GET', 'http.response.status_code': 500, @@ -217,7 +217,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada contexts: { trace: { op: 'http.server', - status: 'unknown_error', + status: 'internal_error', data: { method: 'POST', 'http.response.status_code': 500, @@ -265,7 +265,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada contexts: { trace: { op: 'http.server', - status: 'unknown_error', + status: 'internal_error', data: { method: 'POST', 'http.response.status_code': 500, @@ -313,7 +313,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada contexts: { trace: { op: 'http.server', - status: 'unknown_error', + status: 'internal_error', data: { method: 'POST', 'http.response.status_code': 500, @@ -361,7 +361,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada contexts: { trace: { op: 'http.server', - status: 'unknown_error', + status: 'internal_error', data: { method: 'POST', 'http.response.status_code': 500, @@ -409,7 +409,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada contexts: { trace: { op: 'http.server', - status: 'unknown_error', + status: 'internal_error', data: { method: 'POST', 'http.response.status_code': 500, @@ -457,7 +457,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada contexts: { trace: { op: 'http.server', - status: 'unknown_error', + status: 'internal_error', data: { method: 'POST', 'http.response.status_code': 500, diff --git a/packages/remix/test/integration/test/server/loader.test.ts b/packages/remix/test/integration/test/server/loader.test.ts index 6e1c41ae54b8..be046cb2f5e0 100644 --- a/packages/remix/test/integration/test/server/loader.test.ts +++ b/packages/remix/test/integration/test/server/loader.test.ts @@ -19,7 +19,7 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada assertSentryTransaction(transaction, { contexts: { trace: { - status: 'unknown_error', + status: 'internal_error', data: { 'http.response.status_code': 500, }, @@ -61,7 +61,7 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada assertSentryTransaction(transaction, { contexts: { trace: { - status: 'unknown_error', + status: 'internal_error', data: { 'http.response.status_code': 500, }, @@ -148,7 +148,7 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada contexts: { trace: { op: 'http.server', - status: 'unknown_error', + status: 'internal_error', data: { method: 'GET', 'http.response.status_code': 500, diff --git a/packages/remix/test/integration/test/server/ssr.test.ts b/packages/remix/test/integration/test/server/ssr.test.ts index 2e96760d2bae..63b0fdf7d4cd 100644 --- a/packages/remix/test/integration/test/server/ssr.test.ts +++ b/packages/remix/test/integration/test/server/ssr.test.ts @@ -13,7 +13,7 @@ describe('Server Side Rendering', () => { assertSentryTransaction(transaction[2], { contexts: { trace: { - status: 'unknown_error', + status: 'internal_error', data: { 'http.response.status_code': 500, },