From e301b009cc60c988bd034702d399233bd53c8cda Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 10:42:14 +0000 Subject: [PATCH] feat: Emit OTel-aligned http.client attributes from the net integration Align the `ElectronNet` outgoing-request span with the `http.client` spans that `@sentry/node`'s fetch instrumentation emits, so Relay can infer the low-cardinality span name and high-cardinality description from the same conventions rules: - Name is now the sanitized URL (`${method} ${sanitizedUrl}`) via `getSanitizedUrlStringFromUrlObject`. - Emit OTel attributes `http.request.method`, `server.address`, `server.port`, `url.full`, `url.path`, `url.query`, `url.scheme` (filtered for PII) instead of the bespoke `url`/`http.method`/ `type: 'net.request'`. - `onlyIfParent` now follows `hasSpanStreamingEnabled(client)` so the span is emitted without an active parent under span streaming, matching the node/browser fetch instrumentation. The Electron-specific `sentry.origin` (`auto.http.electron.net`) is kept. Update the net tracing e2e assertions to the new attribute set (the span name is unchanged for URLs without query/credentials). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01PbscLoW7dZTXEm8ebixtgS --- src/main/integrations/net-breadcrumbs.ts | 62 +++++++++++++++---- .../other/net-breadcrumbs-tracing/test.ts | 9 ++- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/main/integrations/net-breadcrumbs.ts b/src/main/integrations/net-breadcrumbs.ts index bc54b4de..2ccb7a26 100644 --- a/src/main/integrations/net-breadcrumbs.ts +++ b/src/main/integrations/net-breadcrumbs.ts @@ -1,11 +1,17 @@ -import type { ClientOptions } from '@sentry/core'; +import type { Client, SpanAttributes } from '@sentry/core'; import { addBreadcrumb, debug, defineIntegration, fill, + filterCollectedUrl, + filterCollectedUrlQuery, getBreadcrumbLogLevelFromHttpStatusCode, + getSanitizedUrlStringFromUrlObject, getTraceData, + getUrlQuery, + hasSpanStreamingEnabled, + isURLObjectRelative, LRUMap, parseStringToURLObject, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -103,10 +109,49 @@ type RequestOptions = string | ClientRequestConstructorOptions; type RequestMethod = (opt: RequestOptions, ...args: unknown[]) => ClientRequest; type WrappedRequestMethodFactory = (original: RequestMethod) => RequestMethod; +/** + * Builds the span name and OpenTelemetry-aligned attributes for an outgoing `net` request, mirroring + * the `http.client` spans emitted by `@sentry/node`'s fetch instrumentation. The SDK always sends the + * sanitized URL as the span name and the URL attributes; Relay then infers the low-cardinality name + * and high-cardinality description from these via the conventions rules. + */ +function getSpanDetails(method: string, url: string, client: Client): { name: string; attributes: SpanAttributes } { + const parsed = parseStringToURLObject(url); + const sanitizedUrl = parsed ? getSanitizedUrlStringFromUrlObject(parsed) : url; + + const attributes: SpanAttributes = { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.electron.net', + 'http.request.method': method, + 'url.full': filterCollectedUrl(url, client), + }; + + if (parsed) { + attributes['url.path'] = parsed.pathname; + + const query = filterCollectedUrlQuery(getUrlQuery(parsed.search), client); + if (query) { + attributes['url.query'] = query; + } + + if (!isURLObjectRelative(parsed)) { + attributes['server.address'] = parsed.hostname; + attributes['url.scheme'] = parsed.protocol.replace(/:$/, ''); + + if (parsed.port) { + attributes['server.port'] = Number(parsed.port); + } + } + } + + return { name: `${method} ${sanitizedUrl}`, attributes }; +} + function createWrappedRequestFactory( { tracing, breadcrumbs, logs }: NetOptions, - { tracePropagationTargets, propagateTraceparent }: ClientOptions, + client: Client, ): WrappedRequestMethodFactory { + const { tracePropagationTargets, propagateTraceparent } = client.getOptions(); + const streamingEnabled = hasSpanStreamingEnabled(client); // We're caching results so we don't have to recompute regexp every time we create a request. const createSpanUrlMap = new LRUMap(100); const headersUrlMap = new LRUMap(100); @@ -218,19 +263,12 @@ function createWrappedRequestFactory( const span = shouldCreateSpan(method, url) ? startInactiveSpan({ - name: `${method} ${url}`, - onlyIfParent: true, - attributes: { - url, - type: 'net.request', - 'http.method': method, - }, + ...getSpanDetails(method, url, client), + onlyIfParent: !streamingEnabled, op: 'http.client', }) : new SentryNonRecordingSpan(); - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.http.electron.net'); - if (shouldAttachTraceData(method, url)) { const inject = (): void => { for (const [key, value] of Object.entries(getTraceData({ propagateTraceparent }))) { @@ -282,7 +320,7 @@ export const electronNetIntegration = defineIntegration((options: NetOptions = { return; } - fill(electronNet, 'request', createWrappedRequestFactory(options, client.getOptions())); + fill(electronNet, 'request', createWrappedRequestFactory(options, client)); }, }; }); diff --git a/test/e2e/test-apps/other/net-breadcrumbs-tracing/test.ts b/test/e2e/test-apps/other/net-breadcrumbs-tracing/test.ts index cf869cf6..61df4ed6 100644 --- a/test/e2e/test-apps/other/net-breadcrumbs-tracing/test.ts +++ b/test/e2e/test-apps/other/net-breadcrumbs-tracing/test.ts @@ -38,12 +38,15 @@ electronTestRunner(__dirname, { skipEsmAutoTransform: true }, async (ctx) => { status: 'error', attributes: expect.objectContaining({ 'sentry.op': { value: 'http.client', type: 'string' }, - url: { + 'http.request.method': { value: 'GET', type: 'string' }, + 'server.address': { value: 'localhost', type: 'string' }, + 'server.port': { value: 8123, type: 'integer' }, + 'url.full': { value: 'http://localhost:8123/something', type: 'string', }, - type: { value: 'net.request', type: 'string' }, - 'http.method': { value: 'GET', type: 'string' }, + 'url.path': { value: '/something', type: 'string' }, + 'url.scheme': { value: 'http', type: 'string' }, 'sentry.origin': { value: 'auto.http.electron.net', type: 'string' }, 'http.response.status_code': { value: 500, type: 'integer' }, 'sentry.release': {