Skip to content

Commit edc02af

Browse files
msonnbclaude
andauthored
feat(node)!: Always report the encoded body size on HTTP spans (#23576)
## What Always set `http.(request|response).body.size` instead of `.size` and `.decoded_size` in HTTP server spans. Browser resource spans are untouched and still report the latter, where the Resource Timing API measures it directly. ## Why The `content-length` header always reports the encoded body size, and we only set the `.decoded_size` attribute when `content-encoding` was `identity`, i.e. there was no encoding and the "decoded" size would equal the encoded size. I'd argue that "decoded" or "uncompressed" has no real meaning and thus value in this case and that we should just always set `http.request.body.size`. part of #18895 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 964e438 commit edc02af

9 files changed

Lines changed: 38 additions & 68 deletions

File tree

‎dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe('outgoing http spans - strip query', () => {
3030
'http.request.method': 'GET',
3131
'url.query': 'id=1',
3232
'http.response.status_code': 200,
33-
'http.response.body.decoded_size': 0,
33+
'http.response.body.size': 0,
3434
'http.response.status_text': 'OK',
3535
'network.peer.address': '::1',
3636
'server.address': 'localhost',

‎dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ describe('httpIntegration', () => {
136136
expect(transaction.contexts?.trace?.data).toEqual({
137137
'http.request.method': 'POST',
138138
'url.query': 'a=1&b=2',
139-
'http.request.body.decoded_size': 9,
139+
'http.request.body.size': 9,
140140
'http.response.status_code': 200,
141141
'http.route': '/test',
142142
'url.scheme': 'http',

‎docs/migration/v11-end-state.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,8 @@ Legacy HTTP span attributes were replaced by their current semantic-convention e
638638
639639
`SanitizedRequestData` — the shape used for `http` breadcrumb data and `http.client` span data — now uses `http.request.method` instead of `http.method` as a key for the request method.
640640
641+
On server-side HTTP spans, the `content-length` header is now always reported as `http.request.body.size`/`http.response.body.size` instead of switching to `http.request_body_size_uncompressed` when the no encoding was present.
642+
641643
#### Network attributes
642644
643645
Network-related span attributes now use the current Sentry semantic conventions, aligned across SDKs. If you query, transform, or alert on the legacy `net.*` fields, update those references:

‎packages/core/src/integrations/http/get-outgoing-span-data.ts‎

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Span, SpanAttributes } from '../../types/span';
22
import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '../../semanticAttributes';
33
import { filterCollectedUrl } from '../../utils/data-collection/filterCollectedUrl';
4+
import { getContentLengthFromHeaders } from '../../utils/request';
45
import { getHttpSpanDetailsFromUrlObject, parseStringToURLObject } from '../../utils/url';
56
import type { HttpClientRequest, HttpIncomingMessage } from './types';
67
import { getRequestUrlFromClientRequest } from './get-request-url';
@@ -67,7 +68,7 @@ export function setIncomingResponseSpanData(response: HttpIncomingMessage, span:
6768
[NETWORK_PROTOCOL_VERSION]: httpVersion,
6869
[NETWORK_TRANSPORT]: transport,
6970
'http.response.status_text': statusMessage?.toUpperCase(),
70-
...getResponseContentLengthAttributes(response),
71+
[HTTP_RESPONSE_BODY_SIZE]: getContentLengthFromHeaders(response.headers),
7172
...getSocketAttrs(socket),
7273
});
7374
}
@@ -82,15 +83,3 @@ function getSocketAttrs(socket: HttpIncomingMessage['socket']): SpanAttributes {
8283
[NETWORK_PEER_PORT]: remotePort,
8384
};
8485
}
85-
86-
function getResponseContentLengthAttributes(response: HttpIncomingMessage): SpanAttributes {
87-
const { headers } = response;
88-
const contentLengthHeader = headers['content-length'];
89-
const length = contentLengthHeader ? parseInt(String(contentLengthHeader), 10) : -1;
90-
const encoding = headers['content-encoding'];
91-
return length >= 0
92-
? encoding && encoding !== 'identity'
93-
? { [HTTP_RESPONSE_BODY_SIZE]: length }
94-
: { 'http.response.body.decoded_size': length }
95-
: {};
96-
}

‎packages/core/src/integrations/http/server-subscription.ts‎

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ import { DEBUG_BUILD } from '../../debug-build';
2525
import { debug } from '../../utils/debug-logger';
2626
import { getClient, getCurrentScope, getIsolationScope, withIsolationScope } from '../../currentScopes';
2727
import { hasSpansEnabled } from '../../utils/hasSpansEnabled';
28-
import { headersToDict, httpHeadersToSpanAttributes, httpRequestToRequestData } from '../../utils/request';
28+
import {
29+
getContentLengthFromHeaders,
30+
headersToDict,
31+
httpHeadersToSpanAttributes,
32+
httpRequestToRequestData,
33+
} from '../../utils/request';
2934
import { patchRequestToCaptureBody } from './patch-request-to-capture-body';
3035
import { getUrlFragment, getUrlQuery, parseStringToURLObject, stripUrlQueryAndFragment } from '../../utils/url';
3136
import { recordRequestSession } from './record-request-session';
@@ -34,7 +39,6 @@ import { continueTrace, startSpanManual } from '../../tracing/trace';
3439
import { getSpanStatusFromHttpCode, SPAN_STATUS_ERROR } from '../../tracing';
3540
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes';
3641
import { safeMathRandom } from '../../utils/randomSafeContext';
37-
import type { SpanAttributes } from '../../types/span';
3842
import type { SpanStatus } from '../../types/spanStatus';
3943
import {
4044
CLIENT_ADDRESS,
@@ -335,7 +339,7 @@ function buildServerSpanWrap(
335339
[USER_AGENT_ORIGINAL]: userAgent,
336340
[URL_SCHEME]: scheme,
337341
[NETWORK_TRANSPORT]: httpVersion?.toUpperCase() === 'QUIC' ? 'udp' : 'tcp',
338-
...getRequestContentLengthAttribute(request),
342+
'http.request.body.size': getContentLengthFromHeaders(request.headers),
339343
...httpHeadersToSpanAttributes(normalizedRequest.headers || {}, dataCollectionOptions),
340344
},
341345
},
@@ -439,15 +443,3 @@ function isKnownPrefetchRequest(req: HttpIncomingMessage): boolean {
439443
// Currently only handles Next.js prefetch requests but may check other frameworks in the future.
440444
return req.headers['next-router-prefetch'] === '1';
441445
}
442-
443-
function getRequestContentLengthAttribute(request: HttpIncomingMessage): SpanAttributes {
444-
const { headers } = request;
445-
const contentLengthHeader = headers['content-length'];
446-
const length = contentLengthHeader ? parseInt(String(contentLengthHeader), 10) : -1;
447-
const encoding = headers['content-encoding'];
448-
return length >= 0
449-
? encoding && encoding !== 'identity'
450-
? { 'http.request.body.size': length }
451-
: { 'http.request.body.decoded_size': length }
452-
: {};
453-
}

‎packages/core/src/shared-exports.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export {
135135
extractQueryParamsFromUrl,
136136
headersToDict,
137137
httpHeadersToSpanAttributes,
138+
getContentLengthFromHeaders,
138139
getMaxBodyByteLength,
139140
MAX_BODY_BYTE_LENGTH,
140141
} from './utils/request';

‎packages/core/src/utils/request.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,22 @@ export function extractQueryParamsFromUrl(url: string): string | undefined {
375375
return undefined;
376376
}
377377
}
378+
379+
/**
380+
* Read the `content-length` header as a number, if it holds a valid one.
381+
*
382+
* `content-length` is the encoded (on-the-wire) body size whether or not a `content-encoding` is
383+
* applied, so it maps to `http.request.body.size` / `http.response.body.size`. The decoded body size
384+
* cannot be derived from it.
385+
*/
386+
export function getContentLengthFromHeaders(
387+
headers: Record<string, string | string[] | undefined>,
388+
): number | undefined {
389+
const contentLength = headers['content-length'];
390+
if (typeof contentLength !== 'string') {
391+
return undefined;
392+
}
393+
394+
const length = parseInt(contentLength, 10);
395+
return length >= 0 ? length : undefined;
396+
}

‎packages/core/test/lib/integrations/http/get-outgoing-span-data.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,16 +159,16 @@ describe('setIncomingResponseSpanData', () => {
159159
);
160160
});
161161

162-
it('includes uncompressed content-length when content-encoding is identity', () => {
162+
it('includes content-length as the encoded body size when content-encoding is identity', () => {
163163
const span = makeMockSpan();
164164
const response = makeMockResponse({
165165
headers: { 'content-length': '42', 'content-encoding': 'identity' },
166166
});
167167
setIncomingResponseSpanData(response, span);
168-
expect(span.setAttributes).toHaveBeenCalledWith(expect.objectContaining({ 'http.response.body.decoded_size': 42 }));
168+
expect(span.setAttributes).toHaveBeenCalledWith(expect.objectContaining({ 'http.response.body.size': 42 }));
169169
});
170170

171-
it('includes compressed content-length when content-encoding is gzip', () => {
171+
it('includes content-length as the encoded body size when content-encoding is gzip', () => {
172172
const span = makeMockSpan();
173173
const response = makeMockResponse({
174174
headers: { 'content-length': '100', 'content-encoding': 'gzip' },

‎packages/node/src/integrations/http/httpServerSpansIntegration.ts‎

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* eslint-disable max-lines */
22
import { errorMonitor } from 'node:events';
3-
import type { IncomingHttpHeaders } from 'node:http';
43
import {
54
SENTRY_SEGMENT_NAME_SOURCE,
65
HTTP_REQUEST_METHOD,
@@ -39,6 +38,7 @@ import {
3938
debug,
4039
getSpanStatusFromHttpCode,
4140
httpHeadersToSpanAttributes,
41+
getContentLengthFromHeaders,
4242
parseStringToURLObject,
4343
SEMANTIC_ATTRIBUTE_SENTRY_OP,
4444
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
@@ -173,7 +173,7 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions
173173
[NETWORK_PROTOCOL_NAME]: 'http',
174174
[NETWORK_PROTOCOL_VERSION]: httpVersion,
175175
[NETWORK_TRANSPORT]: httpVersion?.toUpperCase() === 'QUIC' ? 'udp' : 'tcp',
176-
...getRequestContentLengthAttribute(request),
176+
'http.request.body.size': getContentLengthFromHeaders(request.headers),
177177
...httpHeadersToSpanAttributes(normalizedRequest.headers || {}, client.getDataCollectionOptions()),
178178
},
179179
});
@@ -338,39 +338,6 @@ function shouldIgnoreSpansForIncomingRequest(
338338
return false;
339339
}
340340

341-
function getRequestContentLengthAttribute(request: HttpIncomingMessage): SpanAttributes {
342-
const length = getContentLength(request.headers);
343-
if (length == null) {
344-
return {};
345-
}
346-
347-
if (isCompressed(request.headers)) {
348-
return {
349-
['http.request.body.size']: length,
350-
};
351-
} else {
352-
return {
353-
['http.request.body.decoded_size']: length,
354-
};
355-
}
356-
}
357-
358-
function getContentLength(headers: IncomingHttpHeaders): number | null {
359-
const contentLengthHeader = headers['content-length'];
360-
if (contentLengthHeader === undefined) return null;
361-
362-
const contentLength = parseInt(contentLengthHeader, 10);
363-
if (isNaN(contentLength)) return null;
364-
365-
return contentLength;
366-
}
367-
368-
function isCompressed(headers: IncomingHttpHeaders): boolean {
369-
const encoding = headers['content-encoding'];
370-
371-
return !!encoding && encoding !== 'identity';
372-
}
373-
374341
/**
375342
* First entry of `X-Forwarded-For`: the client as seen by the outermost proxy.
376343
* https://opentelemetry.io/docs/specs/semconv/registry/attributes/client/#client-address

0 commit comments

Comments
 (0)