diff --git a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts index b25d32138aa9..60cf7bbae9aa 100644 --- a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts @@ -456,10 +456,16 @@ function _getOutgoingRequestSpanData(request: http.ClientRequest): [string, Span ]; } -function _getOutgoingRequestEndedSpanData(response: http.IncomingMessage): SpanAttributes { +/** + * Exported for testing purposes. + */ +export function _getOutgoingRequestEndedSpanData(response: http.IncomingMessage): SpanAttributes { const { statusCode, statusMessage, httpVersion, socket } = response; - const transport = httpVersion.toUpperCase() !== 'QUIC' ? 'ip_tcp' : 'ip_udp'; + // httpVersion can be undefined in some cases and we seem to have encountered this before: + // https://github.com/getsentry/sentry-javascript/blob/ec8c8c64cde6001123db0199a8ca017b8863eac8/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts#L158 + // see: #20415 + const transport = httpVersion?.toUpperCase() !== 'QUIC' ? 'ip_tcp' : 'ip_udp'; const additionalAttributes: SpanAttributes = { [ATTR_HTTP_RESPONSE_STATUS_CODE]: statusCode, diff --git a/packages/node-core/test/integrations/SentryHttpInstrumentation.test.ts b/packages/node-core/test/integrations/SentryHttpInstrumentation.test.ts new file mode 100644 index 000000000000..182abaa3663f --- /dev/null +++ b/packages/node-core/test/integrations/SentryHttpInstrumentation.test.ts @@ -0,0 +1,48 @@ +import type * as http from 'node:http'; +import { describe, expect, it } from 'vitest'; +import { _getOutgoingRequestEndedSpanData } from '../../src/integrations/http/SentryHttpInstrumentation'; + +function createResponse(overrides: Partial): http.IncomingMessage { + return { + statusCode: 200, + statusMessage: 'OK', + httpVersion: '1.1', + headers: {}, + socket: undefined, + ...overrides, + } as unknown as http.IncomingMessage; +} + +describe('_getOutgoingRequestEndedSpanData', () => { + it('sets ip_tcp transport for HTTP/1.1', () => { + const attributes = _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: '1.1' })); + + expect(attributes['network.transport']).toBe('ip_tcp'); + expect(attributes['net.transport']).toBe('ip_tcp'); + expect(attributes['network.protocol.version']).toBe('1.1'); + expect(attributes['http.flavor']).toBe('1.1'); + }); + + it('sets ip_udp transport for QUIC', () => { + const attributes = _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: 'QUIC' })); + + expect(attributes['network.transport']).toBe('ip_udp'); + expect(attributes['net.transport']).toBe('ip_udp'); + }); + + it('does not throw when httpVersion is null', () => { + expect(() => + _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: null as unknown as string })), + ).not.toThrow(); + + const attributes = _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: null as unknown as string })); + expect(attributes['network.transport']).toBe('ip_tcp'); + expect(attributes['net.transport']).toBe('ip_tcp'); + }); + + it('does not throw when httpVersion is undefined', () => { + expect(() => + _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: undefined as unknown as string })), + ).not.toThrow(); + }); +});