Skip to content

Commit 91ee3ad

Browse files
committed
feat(core): Add updateSpanName helper function
1 parent f4c5900 commit 91ee3ad

File tree

4 files changed

+172
-13
lines changed

4 files changed

+172
-13
lines changed

packages/core/src/utils/spanUtils.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import { getMainCarrier } from '../carrier';
2020
import { getCurrentScope } from '../currentScopes';
2121
import { getMetricSummaryJsonForSpan, updateMetricSummaryOnSpan } from '../metrics/metric-summary';
2222
import type { MetricType } from '../metrics/types';
23-
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
23+
import {
24+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
25+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
26+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
27+
} from '../semanticAttributes';
2428
import type { SentrySpan } from '../tracing/sentrySpan';
2529
import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus';
2630
import { _getSpanForScope } from './spanOnScope';
@@ -283,3 +287,24 @@ export function updateMetricSummaryOnActiveSpan(
283287
updateMetricSummaryOnSpan(span, metricType, sanitizedName, value, unit, tags, bucketKey);
284288
}
285289
}
290+
291+
/**
292+
* Updates the name of the given span and ensures that the span name is not
293+
* overwritten by the Sentry SDK.
294+
*
295+
* Use this function instead of `span.updateName()` if you want to make sure that
296+
* your name is kept. For some spans, for example root `http.server` spans the
297+
* Sentry SDK would otherwise overwrite the span name with a high-quality name
298+
* it infers when the span ends.
299+
*
300+
* Use this function in server code or when your span is started on the server
301+
* and on the client (browser). If you only update a span name on the client,
302+
* you can also use `span.updateName()` the SDK does not overwrite the name.
303+
*
304+
* @param span - The span to update the name of.
305+
* @param name - The name to set on the span.
306+
*/
307+
export function updateSpanName(span: Span, name: string): void {
308+
span.updateName(name);
309+
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');
310+
}

packages/core/test/lib/utils/spanUtils.test.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { TRACEPARENT_REGEXP, timestampInSeconds } from '@sentry/utils';
33
import {
44
SEMANTIC_ATTRIBUTE_SENTRY_OP,
55
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
6+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
67
SPAN_STATUS_ERROR,
78
SPAN_STATUS_OK,
89
SPAN_STATUS_UNSET,
@@ -13,7 +14,13 @@ import {
1314
startSpan,
1415
} from '../../../src';
1516
import type { OpenTelemetrySdkTraceBaseSpan } from '../../../src/utils/spanUtils';
16-
import { getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../../../src/utils/spanUtils';
17+
import {
18+
getRootSpan,
19+
spanIsSampled,
20+
spanTimeInputToSeconds,
21+
spanToJSON,
22+
updateSpanName,
23+
} from '../../../src/utils/spanUtils';
1724
import { TestClient, getDefaultTestClientOptions } from '../../mocks/client';
1825

1926
describe('spanToTraceHeader', () => {
@@ -245,3 +252,13 @@ describe('getRootSpan', () => {
245252
});
246253
});
247254
});
255+
256+
describe('updateSpanName', () => {
257+
it('updates the span name and source', () => {
258+
const span = new SentrySpan({ name: 'old-name', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } });
259+
updateSpanName(span, 'new-name');
260+
const spanJSON = spanToJSON(span);
261+
expect(spanJSON.description).toBe('new-name');
262+
expect(spanJSON.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toBe('custom');
263+
});
264+
});

packages/opentelemetry/src/utils/parseSpanDescription.ts

+29-11
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ import {
1616
import type { SpanAttributes, TransactionSource } from '@sentry/types';
1717
import { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from '@sentry/utils';
1818

19-
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
19+
import {
20+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
21+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
22+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
23+
} from '@sentry/core';
2024
import { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from '../semanticAttributes';
2125
import type { AbstractSpan } from '../types';
2226
import { getSpanKind } from './getSpanKind';
@@ -32,12 +36,12 @@ interface SpanDescription {
3236
/**
3337
* Infer the op & description for a set of name, attributes and kind of a span.
3438
*/
35-
export function inferSpanData(name: string, attributes: SpanAttributes, kind: SpanKind): SpanDescription {
39+
export function inferSpanData(originalName: string, attributes: SpanAttributes, kind: SpanKind): SpanDescription {
3640
// if http.method exists, this is an http request span
3741
// eslint-disable-next-line deprecation/deprecation
3842
const httpMethod = attributes[ATTR_HTTP_REQUEST_METHOD] || attributes[SEMATTRS_HTTP_METHOD];
3943
if (httpMethod) {
40-
return descriptionForHttpMethod({ attributes, name, kind }, httpMethod);
44+
return descriptionForHttpMethod({ attributes, name: originalName, kind }, httpMethod);
4145
}
4246

4347
// eslint-disable-next-line deprecation/deprecation
@@ -49,17 +53,19 @@ export function inferSpanData(name: string, attributes: SpanAttributes, kind: Sp
4953
// If db.type exists then this is a database call span
5054
// If the Redis DB is used as a cache, the span description should not be changed
5155
if (dbSystem && !opIsCache) {
52-
return descriptionForDbSystem({ attributes, name });
56+
return descriptionForDbSystem({ attributes, name: originalName });
5357
}
5458

59+
const customSourceOrRoute = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom' ? 'custom' : 'route';
60+
5561
// If rpc.service exists then this is a rpc call span.
5662
// eslint-disable-next-line deprecation/deprecation
5763
const rpcService = attributes[SEMATTRS_RPC_SERVICE];
5864
if (rpcService) {
5965
return {
6066
op: 'rpc',
61-
description: name,
62-
source: 'route',
67+
description: originalName,
68+
source: customSourceOrRoute,
6369
};
6470
}
6571

@@ -69,24 +75,28 @@ export function inferSpanData(name: string, attributes: SpanAttributes, kind: Sp
6975
if (messagingSystem) {
7076
return {
7177
op: 'message',
72-
description: name,
73-
source: 'route',
78+
description: originalName,
79+
source: customSourceOrRoute,
7480
};
7581
}
7682

7783
// If faas.trigger exists then this is a function as a service span.
7884
// eslint-disable-next-line deprecation/deprecation
7985
const faasTrigger = attributes[SEMATTRS_FAAS_TRIGGER];
8086
if (faasTrigger) {
81-
return { op: faasTrigger.toString(), description: name, source: 'route' };
87+
return { op: faasTrigger.toString(), description: originalName, source: customSourceOrRoute };
8288
}
8389

84-
return { op: undefined, description: name, source: 'custom' };
90+
return { op: undefined, description: originalName, source: 'custom' };
8591
}
8692

8793
/**
8894
* Extract better op/description from an otel span.
8995
*
96+
* Does not overwrite the span name if the source is already set to custom to ensure
97+
* that user-updated span names are preserved. In this case, we only adjust the op but
98+
* leave span description and source unchanged.
99+
*
90100
* Based on https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/7422ce2a06337f68a59b552b8c5a2ac125d6bae5/exporter/sentryexporter/sentry_exporter.go#L306
91101
*/
92102
export function parseSpanDescription(span: AbstractSpan): SpanDescription {
@@ -98,6 +108,11 @@ export function parseSpanDescription(span: AbstractSpan): SpanDescription {
98108
}
99109

100110
function descriptionForDbSystem({ attributes, name }: { attributes: Attributes; name: string }): SpanDescription {
111+
// if we already set the source to custom, we don't overwrite the span description but just adjust the op
112+
if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom') {
113+
return { op: 'db', description: name, source: 'custom' };
114+
}
115+
101116
// Use DB statement (Ex "SELECT * FROM table") if possible as description.
102117
// eslint-disable-next-line deprecation/deprecation
103118
const statement = attributes[SEMATTRS_DB_STATEMENT];
@@ -170,7 +185,10 @@ export function descriptionForHttpMethod(
170185
const origin = attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] || 'manual';
171186
const isManualSpan = !`${origin}`.startsWith('auto');
172187

173-
const useInferredDescription = isClientOrServerKind || !isManualSpan;
188+
// If users (or in very rare occasions we) set the source to custom, we don't overwrite it
189+
const alreadyHasCustomSource = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom';
190+
191+
const useInferredDescription = !alreadyHasCustomSource && (isClientOrServerKind || !isManualSpan);
174192

175193
return {
176194
op: opParts.join('.'),

packages/opentelemetry/test/utils/parseSpanDescription.test.ts

+99
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from '@opentelemetry/semantic-conventions';
1717

1818
import { descriptionForHttpMethod, getSanitizedUrl, parseSpanDescription } from '../../src/utils/parseSpanDescription';
19+
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
1920

2021
describe('parseSpanDescription', () => {
2122
it.each([
@@ -81,6 +82,21 @@ describe('parseSpanDescription', () => {
8182
source: 'task',
8283
},
8384
],
85+
[
86+
'works with db system and custom source',
87+
{
88+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
89+
[SEMATTRS_DB_SYSTEM]: 'mysql',
90+
[SEMATTRS_DB_STATEMENT]: 'SELECT * from users',
91+
},
92+
'test name',
93+
SpanKind.CLIENT,
94+
{
95+
description: 'test name',
96+
op: 'db',
97+
source: 'custom',
98+
},
99+
],
84100
[
85101
'works with db system without statement',
86102
{
@@ -107,6 +123,20 @@ describe('parseSpanDescription', () => {
107123
source: 'route',
108124
},
109125
],
126+
[
127+
'works with rpc service and custom source',
128+
{
129+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
130+
[SEMATTRS_RPC_SERVICE]: 'rpc-test-service',
131+
},
132+
'test name',
133+
undefined,
134+
{
135+
description: 'test name',
136+
op: 'rpc',
137+
source: 'custom',
138+
},
139+
],
110140
[
111141
'works with messaging system',
112142
{
@@ -120,6 +150,20 @@ describe('parseSpanDescription', () => {
120150
source: 'route',
121151
},
122152
],
153+
[
154+
'works with messaging system and custom source',
155+
{
156+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
157+
[SEMATTRS_MESSAGING_SYSTEM]: 'test-messaging-system',
158+
},
159+
'test name',
160+
undefined,
161+
{
162+
description: 'test name',
163+
op: 'message',
164+
source: 'custom',
165+
},
166+
],
123167
[
124168
'works with faas trigger',
125169
{
@@ -133,6 +177,20 @@ describe('parseSpanDescription', () => {
133177
source: 'route',
134178
},
135179
],
180+
[
181+
'works with faas trigger and custom source',
182+
{
183+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
184+
[SEMATTRS_FAAS_TRIGGER]: 'test-faas-trigger',
185+
},
186+
'test name',
187+
undefined,
188+
{
189+
description: 'test name',
190+
op: 'test-faas-trigger',
191+
source: 'custom',
192+
},
193+
],
136194
])('%s', (_, attributes, name, kind, expected) => {
137195
const actual = parseSpanDescription({ attributes, kind, name } as unknown as Span);
138196
expect(actual).toEqual(expected);
@@ -172,6 +230,26 @@ describe('descriptionForHttpMethod', () => {
172230
source: 'url',
173231
},
174232
],
233+
[
234+
'works with prefetch request',
235+
'GET',
236+
{
237+
[SEMATTRS_HTTP_METHOD]: 'GET',
238+
[SEMATTRS_HTTP_URL]: 'https://www.example.com/my-path',
239+
[SEMATTRS_HTTP_TARGET]: '/my-path',
240+
'sentry.http.prefetch': true,
241+
},
242+
'test name',
243+
SpanKind.CLIENT,
244+
{
245+
op: 'http.client.prefetch',
246+
description: 'GET https://www.example.com/my-path',
247+
data: {
248+
url: 'https://www.example.com/my-path',
249+
},
250+
source: 'url',
251+
},
252+
],
175253
[
176254
'works with basic server POST',
177255
'POST',
@@ -230,6 +308,27 @@ describe('descriptionForHttpMethod', () => {
230308
source: 'custom',
231309
},
232310
],
311+
[
312+
"doesn't overwrite name with source custom",
313+
'GET',
314+
{
315+
[SEMATTRS_HTTP_METHOD]: 'GET',
316+
[SEMATTRS_HTTP_URL]: 'https://www.example.com/my-path/123',
317+
[SEMATTRS_HTTP_TARGET]: '/my-path/123',
318+
[ATTR_HTTP_ROUTE]: '/my-path/:id',
319+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
320+
},
321+
'test name',
322+
SpanKind.CLIENT,
323+
{
324+
op: 'http.client',
325+
description: 'test name',
326+
data: {
327+
url: 'https://www.example.com/my-path/123',
328+
},
329+
source: 'custom',
330+
},
331+
],
233332
])('%s', (_, httpMethod, attributes, name, kind, expected) => {
234333
const actual = descriptionForHttpMethod({ attributes, kind, name }, httpMethod);
235334
expect(actual).toEqual(expected);

0 commit comments

Comments
 (0)