Skip to content

Commit f994eb1

Browse files
committed
backfill source
1 parent 49564b3 commit f994eb1

3 files changed

Lines changed: 95 additions & 30 deletions

File tree

  • dev-packages/node-integration-tests/suites/tracing
    • http-client-spans/fetch-basic-streamed
    • httpIntegration-streamed
  • packages/core/src/tracing/spans

dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-basic-streamed/test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import { createTestServer } from '@sentry-internal/test-utils';
22
import { expect, test } from 'vitest';
33
import { createRunner } from '../../../../utils/runner';
44

5-
test('captures streamed spans with sentry.op for outgoing fetch requests', async () => {
6-
expect.assertions(2);
5+
test('infers sentry.op, name, and source for streamed outgoing fetch spans', async () => {
6+
expect.assertions(4);
77

88
const [SERVER_URL, closeTestServer] = await createTestServer()
99
.get('/api/v0', () => {
10-
// Just ensure we're called
1110
expect(true).toBe(true);
1211
})
1312
.start();
@@ -22,6 +21,8 @@ test('captures streamed spans with sentry.op for outgoing fetch requests', async
2221
);
2322

2423
expect(httpClientSpan).toBeDefined();
24+
expect(httpClientSpan?.name).toMatch(/^GET /);
25+
expect(httpClientSpan?.attributes?.['sentry.source']).toEqual({ type: 'string', value: 'url' });
2526
},
2627
})
2728
.start()

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ describe('httpIntegration-streamed', () => {
77
});
88

99
createEsmAndCjsTests(__dirname, 'server.mjs', 'instrument.mjs', (createRunner, test) => {
10-
test('infers sentry.op http.server on streamed server spans', async () => {
10+
test('infers sentry.op, name, and source for streamed server spans', async () => {
1111
const runner = createRunner()
1212
.expect({
1313
span: container => {
@@ -19,6 +19,8 @@ describe('httpIntegration-streamed', () => {
1919

2020
expect(serverSpan).toBeDefined();
2121
expect(serverSpan?.is_segment).toBe(true);
22+
expect(serverSpan?.name).toBe('GET /test');
23+
expect(serverSpan?.attributes?.['sentry.source']).toEqual({ type: 'string', value: 'route' });
2224
},
2325
})
2426
.start();

packages/core/src/tracing/spans/captureSpan.ts

Lines changed: 88 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import type { RawAttributes } from '../../attributes';
22
import type { Client } from '../../client';
33
import type { ScopeData } from '../../scope';
44
import {
5+
SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME,
56
SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT,
67
SEMANTIC_ATTRIBUTE_SENTRY_OP,
8+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
79
SEMANTIC_ATTRIBUTE_SENTRY_RELEASE,
810
SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME,
911
SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION,
@@ -15,6 +17,7 @@ import {
1517
SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS,
1618
SEMANTIC_ATTRIBUTE_USER_USERNAME,
1719
} from '../../semanticAttributes';
20+
import { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from '../../utils/url';
1821
import type { SerializedStreamedSpan, Span, StreamedSpanJSON } from '../../types-hoist/span';
1922
import { getCombinedScopeData } from '../../utils/scopeData';
2023
import {
@@ -168,6 +171,7 @@ const SPAN_KIND_CLIENT = 2;
168171
* This mirrors what the `SentrySpanExporter` does for non-streamed spans via `getSpanData`/`inferSpanData`.
169172
* Streamed spans skip the exporter, so we do the inference here during capture.
170173
*
174+
* Backfills: `sentry.op`, `sentry.source`, and `name` (description).
171175
* Uses `safeSetSpanJSONAttributes` so explicitly set attributes are never overwritten.
172176
*/
173177
function inferSpanDataFromOtelAttributes(spanJSON: StreamedSpanJSON, spanKind?: number): void {
@@ -178,49 +182,107 @@ function inferSpanDataFromOtelAttributes(spanJSON: StreamedSpanJSON, spanKind?:
178182

179183
const httpMethod = attributes['http.request.method'] || attributes['http.method'];
180184
if (httpMethod) {
181-
const opParts = ['http'];
182-
if (spanKind === SPAN_KIND_CLIENT) {
183-
opParts.push('client');
184-
} else if (spanKind === SPAN_KIND_SERVER) {
185-
opParts.push('server');
186-
}
187-
188-
if (attributes['sentry.http.prefetch']) {
189-
opParts.push('prefetch');
190-
}
191-
192-
safeSetSpanJSONAttributes(spanJSON, {
193-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: opParts.join('.'),
194-
});
185+
inferHttpSpanData(spanJSON, attributes, spanKind, httpMethod);
195186
return;
196187
}
197188

198189
const dbSystem = attributes['db.system.name'] || attributes['db.system'];
199190
if (dbSystem) {
200-
safeSetSpanJSONAttributes(spanJSON, {
201-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db',
202-
});
191+
inferDbSpanData(spanJSON, attributes);
203192
return;
204193
}
205194

206195
if (attributes['rpc.service']) {
207-
safeSetSpanJSONAttributes(spanJSON, {
208-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'rpc',
209-
});
196+
safeSetSpanJSONAttributes(spanJSON, { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'rpc' });
210197
return;
211198
}
212199

213200
if (attributes['messaging.system']) {
214-
safeSetSpanJSONAttributes(spanJSON, {
215-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'message',
216-
});
201+
safeSetSpanJSONAttributes(spanJSON, { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'message' });
217202
return;
218203
}
219204

220205
const faasTrigger = attributes['faas.trigger'];
221206
if (faasTrigger) {
222-
safeSetSpanJSONAttributes(spanJSON, {
223-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${faasTrigger}`,
224-
});
207+
safeSetSpanJSONAttributes(spanJSON, { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${faasTrigger}` });
208+
}
209+
}
210+
211+
function inferHttpSpanData(
212+
spanJSON: StreamedSpanJSON,
213+
attributes: RawAttributes<Record<string, unknown>>,
214+
spanKind: number | undefined,
215+
httpMethod: unknown,
216+
): void {
217+
// Infer op: http.client, http.server, or just http
218+
const opParts = ['http'];
219+
if (spanKind === SPAN_KIND_CLIENT) {
220+
opParts.push('client');
221+
} else if (spanKind === SPAN_KIND_SERVER) {
222+
opParts.push('server');
223+
}
224+
if (attributes['sentry.http.prefetch']) {
225+
opParts.push('prefetch');
226+
}
227+
safeSetSpanJSONAttributes(spanJSON, { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: opParts.join('.') });
228+
229+
// If the user already set a custom name or source, don't overwrite
230+
if (
231+
attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME] ||
232+
attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom'
233+
) {
234+
return;
235+
}
236+
237+
// Infer name and source from URL attributes
238+
const httpRoute = attributes['http.route'];
239+
const httpTarget = attributes['http.target'];
240+
const httpUrl = attributes['url.full'] || attributes['http.url'];
241+
const parsedUrl = typeof httpUrl === 'string' ? parseUrl(httpUrl) : undefined;
242+
const sanitizedUrl = parsedUrl ? getSanitizedUrlString(parsedUrl) : undefined;
243+
244+
let urlPath: string | undefined;
245+
let source: string | undefined;
246+
247+
if (typeof httpRoute === 'string') {
248+
urlPath = httpRoute;
249+
source = 'route';
250+
} else if (spanKind === SPAN_KIND_SERVER && typeof httpTarget === 'string') {
251+
urlPath = stripUrlQueryAndFragment(httpTarget);
252+
source = 'url';
253+
} else if (sanitizedUrl) {
254+
urlPath = sanitizedUrl;
255+
source = 'url';
256+
} else if (typeof httpTarget === 'string') {
257+
urlPath = stripUrlQueryAndFragment(httpTarget);
258+
source = 'url';
259+
}
260+
261+
if (urlPath) {
262+
const isClientOrServer = spanKind === SPAN_KIND_CLIENT || spanKind === SPAN_KIND_SERVER;
263+
const origin = attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] || 'manual';
264+
const isAutoSpan = `${origin}`.startsWith('auto');
265+
266+
if (isClientOrServer || isAutoSpan) {
267+
spanJSON.name = `${httpMethod} ${urlPath}`;
268+
safeSetSpanJSONAttributes(spanJSON, { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source });
269+
}
270+
}
271+
}
272+
273+
function inferDbSpanData(spanJSON: StreamedSpanJSON, attributes: RawAttributes<Record<string, unknown>>): void {
274+
safeSetSpanJSONAttributes(spanJSON, { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db' });
275+
276+
if (
277+
attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME] ||
278+
attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom'
279+
) {
280+
return;
281+
}
282+
283+
const statement = attributes['db.statement'];
284+
if (statement) {
285+
spanJSON.name = `${statement}`;
286+
safeSetSpanJSONAttributes(spanJSON, { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task' });
225287
}
226288
}

0 commit comments

Comments
 (0)