Skip to content

Commit ac6e2f1

Browse files
authored
fix(node): Pass inferred name & attributes to tracesSampler (#12945)
Previously, we passed the span name directly to the `tracesSampler` as-is. However, this is not ideal because this does not match the name we actually send to sentry later, making this confusing. The reason is that we infer the name from the attributes for some types of spans, but we do that at export-time. This PR adjust this so that we send the inferred name & attributes to the `tracesSampler`. This works reasonably enough. However, there is a big caveat: This still only has access to the initial attributes that a span has at creation time. This means that we'll never have e.g. the `http.route` correctly there, because all `http.server` spans originate from the http instrumentation where they do not have a route yet, and then more specific instrumentation (e.g. express or fastify) add the `http.route` to that span later. So the name will never be parametrized, for example, for `http.server` spans, which is not ideal (as it will still not match the final span exactly) but should still be better than what we had so far (where the name would always just be e.g. `GET`). Fixes #12944
1 parent 4549263 commit ac6e2f1

File tree

6 files changed

+131
-39
lines changed

6 files changed

+131
-39
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
transport: loggingTransport,
8+
tracesSampler: samplingContext => {
9+
// The name we get here is inferred at span creation time
10+
// At this point, we sadly do not have a http.route attribute yet,
11+
// so we infer the name from the unparametrized route instead
12+
return (
13+
samplingContext.name === 'GET /test/123' &&
14+
samplingContext.attributes['sentry.op'] === 'http.server' &&
15+
samplingContext.attributes['http.method'] === 'GET'
16+
);
17+
},
18+
debug: true,
19+
});
20+
21+
// express must be required after Sentry is initialized
22+
const express = require('express');
23+
const cors = require('cors');
24+
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');
25+
26+
const app = express();
27+
28+
app.use(cors());
29+
30+
app.get('/test/:id', (_req, res) => {
31+
res.send('Success');
32+
});
33+
34+
app.get('/test2', (_req, res) => {
35+
res.send('Success');
36+
});
37+
38+
Sentry.setupExpressErrorHandler(app);
39+
40+
startExpressServerAndSendPortToRunner(app);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
2+
3+
describe('express tracesSampler', () => {
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
describe('CJS', () => {
9+
test('correctly samples & passes data to tracesSampler', done => {
10+
const runner = createRunner(__dirname, 'server.js')
11+
.expect({
12+
transaction: {
13+
transaction: 'GET /test/:id',
14+
},
15+
})
16+
.start(done);
17+
18+
// This is not sampled
19+
runner.makeRequest('get', '/test2?q=1');
20+
// This is sampled
21+
runner.makeRequest('get', '/test/123?q=1');
22+
});
23+
});
24+
});

packages/opentelemetry/src/sampler.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import { isSpanContextValid, trace } from '@opentelemetry/api';
44
import { TraceState } from '@opentelemetry/core';
55
import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base';
66
import { SamplingDecision } from '@opentelemetry/sdk-trace-base';
7-
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, hasTracingEnabled, sampleSpan } from '@sentry/core';
7+
import {
8+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
9+
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
10+
hasTracingEnabled,
11+
sampleSpan,
12+
} from '@sentry/core';
813
import type { Client, SpanAttributes } from '@sentry/types';
914
import { logger } from '@sentry/utils';
1015
import { SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, SENTRY_TRACE_STATE_URL } from './constants';
@@ -13,6 +18,7 @@ import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic
1318
import { DEBUG_BUILD } from './debug-build';
1419
import { getPropagationContextFromSpan } from './propagator';
1520
import { getSamplingDecision } from './utils/getSamplingDecision';
21+
import { inferSpanData } from './utils/parseSpanDescription';
1622
import { setIsSetup } from './utils/setupCheck';
1723

1824
/**
@@ -56,12 +62,28 @@ export class SentrySampler implements Sampler {
5662

5763
const parentSampled = parentSpan ? getParentSampled(parentSpan, traceId, spanName) : undefined;
5864

65+
// We want to pass the inferred name & attributes to the sampler method
66+
const {
67+
description: inferredSpanName,
68+
data: inferredAttributes,
69+
op,
70+
} = inferSpanData(spanName, spanAttributes, spanKind);
71+
72+
const mergedAttributes = {
73+
...inferredAttributes,
74+
...spanAttributes,
75+
};
76+
77+
if (op) {
78+
mergedAttributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] = op;
79+
}
80+
5981
const mutableSamplingDecision = { decision: true };
6082
this._client.emit(
6183
'beforeSampling',
6284
{
63-
spanAttributes: spanAttributes,
64-
spanName: spanName,
85+
spanAttributes: mergedAttributes,
86+
spanName: inferredSpanName,
6587
parentSampled: parentSampled,
6688
parentContext: parentContext,
6789
},
@@ -72,10 +94,10 @@ export class SentrySampler implements Sampler {
7294
}
7395

7496
const [sampled, sampleRate] = sampleSpan(options, {
75-
name: spanName,
76-
attributes: spanAttributes,
97+
name: inferredSpanName,
98+
attributes: mergedAttributes,
7799
transactionContext: {
78-
name: spanName,
100+
name: inferredSpanName,
79101
parentSampled,
80102
},
81103
parentSampled,

packages/opentelemetry/src/trace.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ export function startSpan<T>(options: OpenTelemetrySpanContext, callback: (span:
4545
const spanOptions = getSpanOptions(options);
4646

4747
return tracer.startActiveSpan(name, spanOptions, ctx, span => {
48-
_applySentryAttributesToSpan(span, options);
49-
5048
return handleCallbackErrors(
5149
() => callback(span),
5250
() => {
@@ -90,8 +88,6 @@ export function startSpanManual<T>(
9088
const spanOptions = getSpanOptions(options);
9189

9290
return tracer.startActiveSpan(name, spanOptions, ctx, span => {
93-
_applySentryAttributesToSpan(span, options);
94-
9591
return handleCallbackErrors(
9692
() => callback(span, () => span.end()),
9793
() => {
@@ -131,8 +127,6 @@ export function startInactiveSpan(options: OpenTelemetrySpanContext): Span {
131127

132128
const span = tracer.startSpan(name, spanOptions, ctx);
133129

134-
_applySentryAttributesToSpan(span, options);
135-
136130
return span;
137131
});
138132
}
@@ -156,22 +150,19 @@ function getTracer(): Tracer {
156150
return (client && client.tracer) || trace.getTracer('@sentry/opentelemetry', SDK_VERSION);
157151
}
158152

159-
function _applySentryAttributesToSpan(span: Span, options: OpenTelemetrySpanContext): void {
160-
const { op } = options;
161-
162-
if (op) {
163-
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, op);
164-
}
165-
}
166-
167153
function getSpanOptions(options: OpenTelemetrySpanContext): SpanOptions {
168-
const { startTime, attributes, kind } = options;
154+
const { startTime, attributes, kind, op } = options;
169155

170156
// OTEL expects timestamps in ms, not seconds
171157
const fixedStartTime = typeof startTime === 'number' ? ensureTimestampInMilliseconds(startTime) : startTime;
172158

173159
return {
174-
attributes,
160+
attributes: op
161+
? {
162+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: op,
163+
...attributes,
164+
}
165+
: attributes,
175166
kind,
176167
startTime: fixedStartTime,
177168
};

packages/opentelemetry/src/utils/parseSpanDescription.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
SEMATTRS_MESSAGING_SYSTEM,
1212
SEMATTRS_RPC_SERVICE,
1313
} from '@opentelemetry/semantic-conventions';
14-
import type { TransactionSource } from '@sentry/types';
14+
import type { SpanAttributes, TransactionSource } from '@sentry/types';
1515
import { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from '@sentry/utils';
1616

1717
import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core';
@@ -27,14 +27,9 @@ interface SpanDescription {
2727
}
2828

2929
/**
30-
* Extract better op/description from an otel span.
31-
*
32-
* Based on https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/7422ce2a06337f68a59b552b8c5a2ac125d6bae5/exporter/sentryexporter/sentry_exporter.go#L306
30+
* Infer the op & description for a set of name, attributes and kind of a span.
3331
*/
34-
export function parseSpanDescription(span: AbstractSpan): SpanDescription {
35-
const attributes = spanHasAttributes(span) ? span.attributes : {};
36-
const name = spanHasName(span) ? span.name : '<unknown>';
37-
32+
export function inferSpanData(name: string, attributes: SpanAttributes, kind: SpanKind): SpanDescription {
3833
// This attribute is intentionally exported as a SEMATTR constant because it should stay intimite API
3934
if (attributes['sentry.skip_span_data_inference']) {
4035
return {
@@ -54,7 +49,7 @@ export function parseSpanDescription(span: AbstractSpan): SpanDescription {
5449
// conventions export an attribute key for it.
5550
const httpMethod = attributes['http.request.method'] || attributes[SEMATTRS_HTTP_METHOD];
5651
if (httpMethod) {
57-
return descriptionForHttpMethod({ attributes, name, kind: getSpanKind(span) }, httpMethod);
52+
return descriptionForHttpMethod({ attributes, name, kind }, httpMethod);
5853
}
5954

6055
const dbSystem = attributes[SEMATTRS_DB_SYSTEM];
@@ -97,6 +92,19 @@ export function parseSpanDescription(span: AbstractSpan): SpanDescription {
9792
return { op: undefined, description: name, source: 'custom' };
9893
}
9994

95+
/**
96+
* Extract better op/description from an otel span.
97+
*
98+
* Based on https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/7422ce2a06337f68a59b552b8c5a2ac125d6bae5/exporter/sentryexporter/sentry_exporter.go#L306
99+
*/
100+
export function parseSpanDescription(span: AbstractSpan): SpanDescription {
101+
const attributes = spanHasAttributes(span) ? span.attributes : {};
102+
const name = spanHasName(span) ? span.name : '<unknown>';
103+
const kind = getSpanKind(span);
104+
105+
return inferSpanData(name, attributes, kind);
106+
}
107+
100108
function descriptionForDbSystem({ attributes, name }: { attributes: Attributes; name: string }): SpanDescription {
101109
// Use DB statement (Ex "SELECT * FROM table") if possible as description.
102110
const statement = attributes[SEMATTRS_DB_STATEMENT];

packages/opentelemetry/test/trace.test.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,9 +1320,7 @@ describe('trace (sampling)', () => {
13201320
expect(tracesSampler).toHaveBeenLastCalledWith({
13211321
parentSampled: undefined,
13221322
name: 'outer',
1323-
attributes: {
1324-
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1,
1325-
},
1323+
attributes: {},
13261324
transactionContext: { name: 'outer', parentSampled: undefined },
13271325
});
13281326

@@ -1357,16 +1355,25 @@ describe('trace (sampling)', () => {
13571355

13581356
mockSdkInit({ tracesSampler });
13591357

1360-
startSpan({ name: 'outer' }, outerSpan => {
1361-
expect(outerSpan).toBeDefined();
1362-
});
1358+
startSpan(
1359+
{
1360+
name: 'outer',
1361+
op: 'test.op',
1362+
attributes: { attr1: 'yes', attr2: 1 },
1363+
},
1364+
outerSpan => {
1365+
expect(outerSpan).toBeDefined();
1366+
},
1367+
);
13631368

1364-
expect(tracesSampler).toBeCalledTimes(1);
1369+
expect(tracesSampler).toHaveBeenCalledTimes(1);
13651370
expect(tracesSampler).toHaveBeenLastCalledWith({
13661371
parentSampled: undefined,
13671372
name: 'outer',
13681373
attributes: {
1369-
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1,
1374+
attr1: 'yes',
1375+
attr2: 1,
1376+
'sentry.op': 'test.op',
13701377
},
13711378
transactionContext: { name: 'outer', parentSampled: undefined },
13721379
});

0 commit comments

Comments
 (0)