Skip to content

Commit c7b6718

Browse files
committed
node tests and jsdoc
1 parent a8b1c99 commit c7b6718

File tree

12 files changed

+265
-12
lines changed

12 files changed

+265
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
// disable attaching headers to /test/* endpoints
8+
tracePropagationTargets: [/^(?!.*test).*$/],
9+
tracesSampleRate: 1.0,
10+
transport: loggingTransport,
11+
});
12+
13+
// express must be required after Sentry is initialized
14+
const express = require('express');
15+
const cors = require('cors');
16+
const bodyParser = require('body-parser');
17+
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');
18+
19+
const app = express();
20+
21+
app.use(cors());
22+
app.use(bodyParser.json());
23+
app.use(bodyParser.text());
24+
app.use(bodyParser.raw());
25+
26+
app.get('/test/:id/span-updateName', (_req, res) => {
27+
const span = Sentry.getActiveSpan();
28+
const rootSpan = Sentry.getRootSpan(span);
29+
rootSpan.updateName('new-name');
30+
res.send({ response: 'response 1' });
31+
});
32+
33+
app.get('/test/:id/span-updateName-source', (_req, res) => {
34+
const span = Sentry.getActiveSpan();
35+
const rootSpan = Sentry.getRootSpan(span);
36+
rootSpan.updateName('new-name');
37+
rootSpan.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');
38+
res.send({ response: 'response 2' });
39+
});
40+
41+
app.get('/test/:id/updateSpanName', (_req, res) => {
42+
const span = Sentry.getActiveSpan();
43+
const rootSpan = Sentry.getRootSpan(span);
44+
Sentry.updateSpanName(rootSpan, 'new-name');
45+
res.send({ response: 'response 3' });
46+
});
47+
48+
Sentry.setupExpressErrorHandler(app);
49+
50+
startExpressServerAndSendPortToRunner(app);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
4+
describe('express tracing', () => {
5+
afterAll(() => {
6+
cleanupChildProcesses();
7+
});
8+
9+
describe('CJS', () => {
10+
// This test documents the unfortunate behaviour of using `span.updateName` on the server-side.
11+
// For http.server root spans (which is the root span on the server 99% of the time), Otel's http instrumentation
12+
// calls `span.updateName` and overwrites whatever the name was set to before (by us or by users).
13+
test("calling just `span.updateName` doesn't update the final name in express (missing source)", done => {
14+
createRunner(__dirname, 'server.js')
15+
.expect({
16+
transaction: {
17+
transaction: 'GET /test/:id/span-updateName',
18+
transaction_info: {
19+
source: 'route',
20+
},
21+
},
22+
})
23+
.start(done)
24+
.makeRequest('get', '/test/123/span-updateName');
25+
});
26+
27+
// Also calling `updateName` AND setting a source doesn't change anything - Otel has no concept of source, this is sentry-internal.
28+
// Therefore, only the source is updated but the name is still overwritten by Otel.
29+
test("calling `span.updateName` and setting attribute source doesn't update the final name in express but it updates the source", done => {
30+
createRunner(__dirname, 'server.js')
31+
.expect({
32+
transaction: {
33+
transaction: 'GET /test/:id/span-updateName-source',
34+
transaction_info: {
35+
source: 'custom',
36+
},
37+
},
38+
})
39+
.start(done)
40+
.makeRequest('get', '/test/123/span-updateName-source');
41+
});
42+
43+
// This test documents the correct way to update the span name (and implicitly the source) in Node:
44+
test('calling `Sentry.updateSpanName` updates the final name and source in express', done => {
45+
createRunner(__dirname, 'server.js')
46+
.expect({
47+
transaction: txnEvent => {
48+
expect(txnEvent).toMatchObject({
49+
transaction: 'new-name',
50+
transaction_info: {
51+
source: 'custom',
52+
},
53+
contexts: {
54+
trace: {
55+
data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
56+
},
57+
},
58+
});
59+
// ensure we delete the internal attribute once we're done with it
60+
expect(txnEvent.contexts?.trace?.data?.['_sentry_span_name_set_by_user']).toBeUndefined();
61+
},
62+
})
63+
.start(done)
64+
.makeRequest('get', '/test/123/updateSpanName');
65+
});
66+
});
67+
});
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
1+
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node';
12
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
23

34
afterAll(() => {
45
cleanupChildProcesses();
56
});
67

7-
test('should send a manually started root span', done => {
8+
test('sends a manually started root span with source custom', done => {
89
createRunner(__dirname, 'scenario.ts')
9-
.expect({ transaction: { transaction: 'test_span' } })
10+
.expect({
11+
transaction: {
12+
transaction: 'test_span',
13+
transaction_info: { source: 'custom' },
14+
contexts: {
15+
trace: {
16+
span_id: expect.any(String),
17+
trace_id: expect.any(String),
18+
data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
19+
},
20+
},
21+
},
22+
})
1023
.start(done);
1124
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
2+
import * as Sentry from '@sentry/node';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
10+
11+
Sentry.startSpan(
12+
{ name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } },
13+
(span: Sentry.Span) => {
14+
span.updateName('new name');
15+
},
16+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
test('updates the span name when calling `span.updateName`', done => {
9+
createRunner(__dirname, 'scenario.ts')
10+
.expect({
11+
transaction: {
12+
transaction: 'new name',
13+
transaction_info: { source: 'url' },
14+
contexts: {
15+
trace: {
16+
span_id: expect.any(String),
17+
trace_id: expect.any(String),
18+
data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' },
19+
},
20+
},
21+
},
22+
})
23+
.start(done);
24+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
2+
import * as Sentry from '@sentry/node';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
10+
11+
Sentry.startSpan(
12+
{ name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } },
13+
(span: Sentry.Span) => {
14+
Sentry.updateSpanName(span, 'new name');
15+
},
16+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
test('updates the span name and source when calling `updateSpanName`', done => {
9+
createRunner(__dirname, 'scenario.ts')
10+
.expect({
11+
transaction: {
12+
transaction: 'new name',
13+
transaction_info: { source: 'custom' },
14+
contexts: {
15+
trace: {
16+
span_id: expect.any(String),
17+
trace_id: expect.any(String),
18+
data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
19+
},
20+
},
21+
},
22+
})
23+
.start(done);
24+
});

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

+6
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,10 @@ describe('httpIntegration', () => {
169169
runner.makeRequest('get', '/testRequest');
170170
});
171171
});
172+
173+
describe('does not override the span name set by the user', () => {
174+
test('via `span.updateName`', done => {
175+
createRunner(__dirname, 'server-updateName.js').start(done);
176+
});
177+
});
172178
});

packages/core/src/envelope.ts

+8
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ export function createEventEnvelope(
9191
// have temporarily added, etc. Even if we don't happen to be using it at some point in the future, let's not get rid
9292
// of this `delete`, lest we miss putting it back in the next time the property is in use.)
9393
delete event.sdkProcessingMetadata;
94+
try {
95+
// @ts-expect-error - for bundle size we try/catch the access to this property
96+
delete event.contexts.trace.data._sentry_span_name_set_by_user;
97+
// @ts-expect-error - for bundle size we try/catch the access to this property
98+
event.spans.forEach(span => delete span.data._sentry_span_name_set_by_user);
99+
} catch {
100+
// Do nothing
101+
}
94102

95103
const eventItem: EventItem = [{ type: eventType }, event];
96104
return createEnvelope<EventEnvelope>(envelopeHeaders, [eventItem]);

packages/core/src/utils/spanUtils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -307,4 +307,5 @@ export function updateMetricSummaryOnActiveSpan(
307307
export function updateSpanName(span: Span, name: string): void {
308308
span.updateName(name);
309309
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');
310+
span.setAttribute('_sentry_span_name_set_by_user', name);
310311
}

packages/opentelemetry/src/utils/parseSpanDescription.ts

+22-10
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ interface SpanDescription {
3636
/**
3737
* Infer the op & description for a set of name, attributes and kind of a span.
3838
*/
39-
export function inferSpanData(originalName: string, attributes: SpanAttributes, kind: SpanKind): SpanDescription {
39+
export function inferSpanData(spanName: string, attributes: SpanAttributes, kind: SpanKind): SpanDescription {
4040
// if http.method exists, this is an http request span
4141
// eslint-disable-next-line deprecation/deprecation
4242
const httpMethod = attributes[ATTR_HTTP_REQUEST_METHOD] || attributes[SEMATTRS_HTTP_METHOD];
4343
if (httpMethod) {
44-
return descriptionForHttpMethod({ attributes, name: originalName, kind }, httpMethod);
44+
return descriptionForHttpMethod({ attributes, name: getOriginalName(spanName, attributes), kind }, httpMethod);
4545
}
4646

4747
// eslint-disable-next-line deprecation/deprecation
@@ -53,7 +53,7 @@ export function inferSpanData(originalName: string, attributes: SpanAttributes,
5353
// If db.type exists then this is a database call span
5454
// If the Redis DB is used as a cache, the span description should not be changed
5555
if (dbSystem && !opIsCache) {
56-
return descriptionForDbSystem({ attributes, name: originalName });
56+
return descriptionForDbSystem({ attributes, name: spanName });
5757
}
5858

5959
const customSourceOrRoute = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom' ? 'custom' : 'route';
@@ -64,7 +64,7 @@ export function inferSpanData(originalName: string, attributes: SpanAttributes,
6464
if (rpcService) {
6565
return {
6666
op: 'rpc',
67-
description: originalName,
67+
description: getOriginalName(spanName, attributes),
6868
source: customSourceOrRoute,
6969
};
7070
}
@@ -75,7 +75,7 @@ export function inferSpanData(originalName: string, attributes: SpanAttributes,
7575
if (messagingSystem) {
7676
return {
7777
op: 'message',
78-
description: originalName,
78+
description: getOriginalName(spanName, attributes),
7979
source: customSourceOrRoute,
8080
};
8181
}
@@ -84,10 +84,14 @@ export function inferSpanData(originalName: string, attributes: SpanAttributes,
8484
// eslint-disable-next-line deprecation/deprecation
8585
const faasTrigger = attributes[SEMATTRS_FAAS_TRIGGER];
8686
if (faasTrigger) {
87-
return { op: faasTrigger.toString(), description: originalName, source: customSourceOrRoute };
87+
return {
88+
op: faasTrigger.toString(),
89+
description: getOriginalName(spanName, attributes),
90+
source: customSourceOrRoute,
91+
};
8892
}
8993

90-
return { op: undefined, description: originalName, source: 'custom' };
94+
return { op: undefined, description: spanName, source: 'custom' };
9195
}
9296

9397
/**
@@ -110,7 +114,7 @@ export function parseSpanDescription(span: AbstractSpan): SpanDescription {
110114
function descriptionForDbSystem({ attributes, name }: { attributes: Attributes; name: string }): SpanDescription {
111115
// if we already set the source to custom, we don't overwrite the span description but just adjust the op
112116
if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom') {
113-
return { op: 'db', description: name, source: 'custom' };
117+
return { op: 'db', description: getOriginalName(name, attributes), source: 'custom' };
114118
}
115119

116120
// Use DB statement (Ex "SELECT * FROM table") if possible as description.
@@ -146,7 +150,7 @@ export function descriptionForHttpMethod(
146150
const { urlPath, url, query, fragment, hasRoute } = getSanitizedUrl(attributes, kind);
147151

148152
if (!urlPath) {
149-
return { op: opParts.join('.'), description: name, source: 'custom' };
153+
return { op: opParts.join('.'), description: getOriginalName(name, attributes), source: 'custom' };
150154
}
151155

152156
const graphqlOperationsAttribute = attributes[SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION];
@@ -192,7 +196,7 @@ export function descriptionForHttpMethod(
192196

193197
return {
194198
op: opParts.join('.'),
195-
description: useInferredDescription ? description : name,
199+
description: useInferredDescription ? description : getOriginalName(name, attributes),
196200
source: useInferredDescription ? source : 'custom',
197201
data,
198202
};
@@ -258,3 +262,11 @@ export function getSanitizedUrl(
258262

259263
return { urlPath: undefined, url, query, fragment, hasRoute: false };
260264
}
265+
266+
function getOriginalName(name: string, attributes: Attributes): string {
267+
return attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom' &&
268+
attributes['_sentry_span_name_set_by_user'] &&
269+
typeof attributes['_sentry_span_name_set_by_user'] === 'string'
270+
? attributes['_sentry_span_name_set_by_user']
271+
: name;
272+
}

packages/types/src/span.ts

+16
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,22 @@ export interface Span {
234234

235235
/**
236236
* Update the name of the span.
237+
*
238+
* **Important:** You most likely want to use `Sentry.updateSpanName(span, name)` instead!
239+
*
240+
* This method will update the current span name but cannot guarantee that the new name will be
241+
* the final name of the span. Instrumentation might still overwrite the name with an automatically
242+
* computed name, for example in `http.server` or `db` spans.
243+
*
244+
* You can ensure that your name is kept and not overwritten by
245+
* - either calling `Sentry.updateSpanName(span, name)`
246+
* - or by calling `span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom')`
247+
* in addition to `span.updateName`.
248+
*
249+
* If you want to update a span name in a browser-only app, `span.updateName` and `Sentry.updateSpanName`
250+
* are identical: In both cases, the name will not be overwritten by the Sentry SDK.
251+
*
252+
* @param name the new name of the span
237253
*/
238254
updateName(name: string): this;
239255

0 commit comments

Comments
 (0)