Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 50 additions & 12 deletions src/main/integrations/net-breadcrumbs.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<string, boolean>(100);
const headersUrlMap = new LRUMap<string, boolean>(100);
Expand Down Expand Up @@ -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 }))) {
Expand Down Expand Up @@ -282,7 +320,7 @@ export const electronNetIntegration = defineIntegration((options: NetOptions = {
return;
}

fill(electronNet, 'request', createWrappedRequestFactory(options, client.getOptions()));
fill(electronNet, 'request', createWrappedRequestFactory(options, client));
},
};
});
9 changes: 6 additions & 3 deletions test/e2e/test-apps/other/net-breadcrumbs-tracing/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': {
Expand Down