Skip to content

Commit c4a76b4

Browse files
committed
feat(core): Add updateSpanName helper function
1 parent 3339829 commit c4a76b4

File tree

4 files changed

+169
-13
lines changed

4 files changed

+169
-13
lines changed

packages/core/src/utils/spanUtils.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { getMainCarrier } from '../carrier';
33
import { getCurrentScope } from '../currentScopes';
44
import { getMetricSummaryJsonForSpan, updateMetricSummaryOnSpan } from '../metrics/metric-summary';
55
import type { MetricType } from '../metrics/types';
6-
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
6+
import {
7+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
8+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
9+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
10+
} from '../semanticAttributes';
711
import type { SentrySpan } from '../tracing/sentrySpan';
812
import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus';
913
import type {
@@ -310,3 +314,24 @@ export function showSpanDropWarning(): void {
310314
hasShownSpanDropWarning = true;
311315
}
312316
}
317+
318+
/**
319+
* Updates the name of the given span and ensures that the span name is not
320+
* overwritten by the Sentry SDK.
321+
*
322+
* Use this function instead of `span.updateName()` if you want to make sure that
323+
* your name is kept. For some spans, for example root `http.server` spans the
324+
* Sentry SDK would otherwise overwrite the span name with a high-quality name
325+
* it infers when the span ends.
326+
*
327+
* Use this function in server code or when your span is started on the server
328+
* and on the client (browser). If you only update a span name on the client,
329+
* you can also use `span.updateName()` the SDK does not overwrite the name.
330+
*
331+
* @param span - The span to update the name of.
332+
* @param name - The name to set on the span.
333+
*/
334+
export function updateSpanName(span: Span, name: string): void {
335+
span.updateName(name);
336+
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');
337+
}

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
SEMANTIC_ATTRIBUTE_SENTRY_OP,
33
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
4+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
45
SPAN_STATUS_ERROR,
56
SPAN_STATUS_OK,
67
SPAN_STATUS_UNSET,
@@ -14,8 +15,14 @@ import {
1415
} from '../../../src';
1516
import type { Span, SpanAttributes, SpanStatus, SpanTimeInput } from '../../../src/types-hoist';
1617
import type { OpenTelemetrySdkTraceBaseSpan } from '../../../src/utils/spanUtils';
17-
import { spanToTraceContext } from '../../../src/utils/spanUtils';
18-
import { getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../../../src/utils/spanUtils';
18+
import {
19+
getRootSpan,
20+
spanIsSampled,
21+
spanTimeInputToSeconds,
22+
spanToJSON,
23+
spanToTraceContext,
24+
updateSpanName,
25+
} from '../../../src/utils/spanUtils';
1926
import { TestClient, getDefaultTestClientOptions } from '../../mocks/client';
2027

2128
function createMockedOtelSpan({
@@ -332,3 +339,13 @@ describe('getRootSpan', () => {
332339
});
333340
});
334341
});
342+
343+
describe('updateSpanName', () => {
344+
it('updates the span name and source', () => {
345+
const span = new SentrySpan({ name: 'old-name', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } });
346+
updateSpanName(span, 'new-name');
347+
const spanJSON = spanToJSON(span);
348+
expect(spanJSON.description).toBe('new-name');
349+
expect(spanJSON.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toBe('custom');
350+
});
351+
});

packages/opentelemetry/src/utils/parseSpanDescription.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type { SpanAttributes, TransactionSource } from '@sentry/core';
1717
import {
1818
SEMANTIC_ATTRIBUTE_SENTRY_OP,
1919
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
20+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
2021
getSanitizedUrlString,
2122
parseUrl,
2223
stripUrlQueryAndFragment,
@@ -36,12 +37,12 @@ interface SpanDescription {
3637
/**
3738
* Infer the op & description for a set of name, attributes and kind of a span.
3839
*/
39-
export function inferSpanData(name: string, attributes: SpanAttributes, kind: SpanKind): SpanDescription {
40+
export function inferSpanData(originalName: string, attributes: SpanAttributes, kind: SpanKind): SpanDescription {
4041
// if http.method exists, this is an http request span
4142
// eslint-disable-next-line deprecation/deprecation
4243
const httpMethod = attributes[ATTR_HTTP_REQUEST_METHOD] || attributes[SEMATTRS_HTTP_METHOD];
4344
if (httpMethod) {
44-
return descriptionForHttpMethod({ attributes, name, kind }, httpMethod);
45+
return descriptionForHttpMethod({ attributes, name: originalName, kind }, httpMethod);
4546
}
4647

4748
// eslint-disable-next-line deprecation/deprecation
@@ -53,17 +54,19 @@ export function inferSpanData(name: string, attributes: SpanAttributes, kind: Sp
5354
// If db.type exists then this is a database call span
5455
// If the Redis DB is used as a cache, the span description should not be changed
5556
if (dbSystem && !opIsCache) {
56-
return descriptionForDbSystem({ attributes, name });
57+
return descriptionForDbSystem({ attributes, name: originalName });
5758
}
5859

60+
const customSourceOrRoute = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom' ? 'custom' : 'route';
61+
5962
// If rpc.service exists then this is a rpc call span.
6063
// eslint-disable-next-line deprecation/deprecation
6164
const rpcService = attributes[SEMATTRS_RPC_SERVICE];
6265
if (rpcService) {
6366
return {
6467
op: 'rpc',
65-
description: name,
66-
source: 'route',
68+
description: originalName,
69+
source: customSourceOrRoute,
6770
};
6871
}
6972

@@ -73,24 +76,28 @@ export function inferSpanData(name: string, attributes: SpanAttributes, kind: Sp
7376
if (messagingSystem) {
7477
return {
7578
op: 'message',
76-
description: name,
77-
source: 'route',
79+
description: originalName,
80+
source: customSourceOrRoute,
7881
};
7982
}
8083

8184
// If faas.trigger exists then this is a function as a service span.
8285
// eslint-disable-next-line deprecation/deprecation
8386
const faasTrigger = attributes[SEMATTRS_FAAS_TRIGGER];
8487
if (faasTrigger) {
85-
return { op: faasTrigger.toString(), description: name, source: 'route' };
88+
return { op: faasTrigger.toString(), description: originalName, source: customSourceOrRoute };
8689
}
8790

88-
return { op: undefined, description: name, source: 'custom' };
91+
return { op: undefined, description: originalName, source: 'custom' };
8992
}
9093

9194
/**
9295
* Extract better op/description from an otel span.
9396
*
97+
* Does not overwrite the span name if the source is already set to custom to ensure
98+
* that user-updated span names are preserved. In this case, we only adjust the op but
99+
* leave span description and source unchanged.
100+
*
94101
* Based on https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/7422ce2a06337f68a59b552b8c5a2ac125d6bae5/exporter/sentryexporter/sentry_exporter.go#L306
95102
*/
96103
export function parseSpanDescription(span: AbstractSpan): SpanDescription {
@@ -102,6 +109,11 @@ export function parseSpanDescription(span: AbstractSpan): SpanDescription {
102109
}
103110

104111
function descriptionForDbSystem({ attributes, name }: { attributes: Attributes; name: string }): SpanDescription {
112+
// if we already set the source to custom, we don't overwrite the span description but just adjust the op
113+
if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom') {
114+
return { op: 'db', description: name, source: 'custom' };
115+
}
116+
105117
// Use DB statement (Ex "SELECT * FROM table") if possible as description.
106118
// eslint-disable-next-line deprecation/deprecation
107119
const statement = attributes[SEMATTRS_DB_STATEMENT];
@@ -174,7 +186,10 @@ export function descriptionForHttpMethod(
174186
const origin = attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] || 'manual';
175187
const isManualSpan = !`${origin}`.startsWith('auto');
176188

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

179194
return {
180195
op: opParts.join('.'),

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

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
SEMATTRS_RPC_SERVICE,
1616
} from '@opentelemetry/semantic-conventions';
1717

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

2021
describe('parseSpanDescription', () => {
@@ -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)