Skip to content

Commit 5aef4a0

Browse files
authored
feat(core): Add OpenTelemetry-specific getTraceData implementation (#13281)
Add an Otel-specific implementation of `getTraceData` and add the `getTraceData` function to the `AsyncContextStrategy` interface. This allows us to dynamically choose either the default implementation (which works correctly for browser/non-POTEL SDKs) and the Otel-specific version.
1 parent 0654dd0 commit 5aef4a0

File tree

12 files changed

+192
-89
lines changed

12 files changed

+192
-89
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
transport: loggingTransport,
7+
});
8+
9+
// express must be required after Sentry is initialized
10+
const express = require('express');
11+
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');
12+
13+
const app = express();
14+
15+
app.get('/test', (_req, res) => {
16+
res.send({
17+
response: `
18+
<html>
19+
<head>
20+
${Sentry.getTraceMetaTags()}
21+
</head>
22+
<body>
23+
Hi :)
24+
</body>
25+
</html>
26+
`,
27+
});
28+
});
29+
30+
Sentry.setupExpressErrorHandler(app);
31+
32+
startExpressServerAndSendPortToRunner(app);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
2+
3+
describe('getTraceMetaTags', () => {
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
test('injects sentry tracing <meta> tags without sampled flag for Tracing Without Performance', async () => {
9+
const runner = createRunner(__dirname, 'server.js').start();
10+
11+
const response = await runner.makeRequest('get', '/test');
12+
13+
// @ts-ignore - response is defined, types just don't reflect it
14+
const html = response?.response as unknown as string;
15+
16+
const [, traceId, spanId] = html.match(/<meta name="sentry-trace" content="([a-f0-9]{32})-([a-f0-9]{16})"\/>/) || [
17+
undefined,
18+
undefined,
19+
undefined,
20+
];
21+
22+
expect(traceId).toBeDefined();
23+
expect(spanId).toBeDefined();
24+
25+
const sentryBaggageContent = html.match(/<meta name="baggage" content="(.*)"\/>/)?.[1];
26+
27+
expect(sentryBaggageContent).toContain('sentry-environment=production');
28+
expect(sentryBaggageContent).toContain('sentry-public_key=public');
29+
expect(sentryBaggageContent).toContain(`sentry-trace_id=${traceId}`);
30+
});
31+
});

packages/astro/src/server/middleware.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
startSpan,
1212
withIsolationScope,
1313
} from '@sentry/node';
14-
import type { Client, Scope, Span, SpanAttributes } from '@sentry/types';
14+
import type { Scope, SpanAttributes } from '@sentry/types';
1515
import {
1616
addNonEnumerableProperty,
1717
objectify,
@@ -151,7 +151,6 @@ async function instrumentRequest(
151151
setHttpStatus(span, originalResponse.status);
152152
}
153153

154-
const scope = getCurrentScope();
155154
const client = getClient();
156155
const contentType = originalResponse.headers.get('content-type');
157156

@@ -175,7 +174,7 @@ async function instrumentRequest(
175174
start: async controller => {
176175
for await (const chunk of originalBody) {
177176
const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
178-
const modifiedHtml = addMetaTagToHead(html, scope, client, span);
177+
const modifiedHtml = addMetaTagToHead(html);
179178
controller.enqueue(new TextEncoder().encode(modifiedHtml));
180179
}
181180
controller.close();
@@ -199,11 +198,11 @@ async function instrumentRequest(
199198
* This function optimistically assumes that the HTML coming in chunks will not be split
200199
* within the <head> tag. If this still happens, we simply won't replace anything.
201200
*/
202-
function addMetaTagToHead(htmlChunk: string, scope: Scope, client: Client, span?: Span): string {
201+
function addMetaTagToHead(htmlChunk: string): string {
203202
if (typeof htmlChunk !== 'string') {
204203
return htmlChunk;
205204
}
206-
const metaTags = getTraceMetaTags(span, scope, client);
205+
const metaTags = getTraceMetaTags();
207206

208207
if (!metaTags) {
209208
return htmlChunk;

packages/core/src/asyncContext/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Scope } from '@sentry/types';
2+
import type { getTraceData } from '../utils/traceData';
23
import type {
34
startInactiveSpan,
45
startSpan,
@@ -64,4 +65,7 @@ export interface AsyncContextStrategy {
6465

6566
/** Suppress tracing in the given callback, ensuring no spans are generated inside of it. */
6667
suppressTracing?: typeof suppressTracing;
68+
69+
/** Get trace data as serialized string values for propagation via `sentry-trace` and `baggage`. */
70+
getTraceData?: typeof getTraceData;
6771
}

packages/core/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type { ClientClass } from './sdk';
1+
export type { ClientClass as SentryCoreCurrentScopes } from './sdk';
22
export type { AsyncContextStrategy } from './asyncContext/types';
33
export type { Carrier } from './carrier';
44
export type { OfflineStore, OfflineTransportOptions } from './transports/offline';

packages/core/src/utils/meta.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { Client, Scope, Span } from '@sentry/types';
21
import { getTraceData } from './traceData';
32

43
/**
@@ -22,8 +21,8 @@ import { getTraceData } from './traceData';
2221
* ```
2322
*
2423
*/
25-
export function getTraceMetaTags(span?: Span, scope?: Scope, client?: Client): string {
26-
return Object.entries(getTraceData(span, scope, client))
24+
export function getTraceMetaTags(): string {
25+
return Object.entries(getTraceData())
2726
.map(([key, value]) => `<meta name="${key}" content="${value}"/>`)
2827
.join('\n');
2928
}

packages/core/src/utils/traceData.ts

+18-19
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
1-
import type { Client, Scope, Span } from '@sentry/types';
1+
import type { SerializedTraceData } from '@sentry/types';
22
import {
33
TRACEPARENT_REGEXP,
44
dynamicSamplingContextToSentryBaggageHeader,
55
generateSentryTraceHeader,
66
logger,
77
} from '@sentry/utils';
8+
import { getAsyncContextStrategy } from '../asyncContext';
9+
import { getMainCarrier } from '../carrier';
810
import { getClient, getCurrentScope } from '../currentScopes';
911
import { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from '../tracing';
1012
import { getActiveSpan, getRootSpan, spanToTraceHeader } from './spanUtils';
1113

12-
type TraceData = {
13-
'sentry-trace'?: string;
14-
baggage?: string;
15-
};
16-
1714
/**
1815
* Extracts trace propagation data from the current span or from the client's scope (via transaction or propagation
1916
* context) and serializes it to `sentry-trace` and `baggage` values to strings. These values can be used to propagate
@@ -22,29 +19,31 @@ type TraceData = {
2219
* This function also applies some validation to the generated sentry-trace and baggage values to ensure that
2320
* only valid strings are returned.
2421
*
25-
* @param span a span to take the trace data from. By default, the currently active span is used.
26-
* @param scope the scope to take trace data from By default, the active current scope is used.
27-
* @param client the SDK's client to take trace data from. By default, the current client is used.
28-
*
2922
* @returns an object with the tracing data values. The object keys are the name of the tracing key to be used as header
3023
* or meta tag name.
3124
*/
32-
export function getTraceData(span?: Span, scope?: Scope, client?: Client): TraceData {
33-
const clientToUse = client || getClient();
34-
const scopeToUse = scope || getCurrentScope();
35-
const spanToUse = span || getActiveSpan();
25+
export function getTraceData(): SerializedTraceData {
26+
const carrier = getMainCarrier();
27+
const acs = getAsyncContextStrategy(carrier);
28+
if (acs.getTraceData) {
29+
return acs.getTraceData();
30+
}
31+
32+
const client = getClient();
33+
const scope = getCurrentScope();
34+
const span = getActiveSpan();
3635

37-
const { dsc, sampled, traceId } = scopeToUse.getPropagationContext();
38-
const rootSpan = spanToUse && getRootSpan(spanToUse);
36+
const { dsc, sampled, traceId } = scope.getPropagationContext();
37+
const rootSpan = span && getRootSpan(span);
3938

40-
const sentryTrace = spanToUse ? spanToTraceHeader(spanToUse) : generateSentryTraceHeader(traceId, undefined, sampled);
39+
const sentryTrace = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled);
4140

4241
const dynamicSamplingContext = rootSpan
4342
? getDynamicSamplingContextFromSpan(rootSpan)
4443
: dsc
4544
? dsc
46-
: clientToUse
47-
? getDynamicSamplingContextFromClient(traceId, clientToUse)
45+
: client
46+
? getDynamicSamplingContextFromClient(traceId, client)
4847
: undefined;
4948

5049
const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);

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

+65-59
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { SentrySpan, getTraceData } from '../../../src/';
2+
import * as SentryCoreCurrentScopes from '../../../src/currentScopes';
23
import * as SentryCoreTracing from '../../../src/tracing';
4+
import * as SentryCoreSpanUtils from '../../../src/utils/spanUtils';
35

46
import { isValidBaggageString } from '../../../src/utils/traceData';
57

@@ -25,33 +27,38 @@ describe('getTraceData', () => {
2527
jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({
2628
environment: 'production',
2729
});
30+
jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => mockedSpan);
31+
jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce(() => mockedScope);
2832

29-
const tags = getTraceData(mockedSpan, mockedScope, mockedClient);
33+
const data = getTraceData();
3034

31-
expect(tags).toEqual({
35+
expect(data).toEqual({
3236
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
3337
baggage: 'sentry-environment=production',
3438
});
3539
}
3640
});
3741

3842
it('returns propagationContext DSC data if no span is available', () => {
39-
const traceData = getTraceData(
40-
undefined,
41-
{
42-
getPropagationContext: () => ({
43-
traceId: '12345678901234567890123456789012',
44-
sampled: true,
45-
spanId: '1234567890123456',
46-
dsc: {
47-
environment: 'staging',
48-
public_key: 'key',
49-
trace_id: '12345678901234567890123456789012',
50-
},
51-
}),
52-
} as any,
53-
mockedClient,
43+
jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => undefined);
44+
jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce(
45+
() =>
46+
({
47+
getPropagationContext: () => ({
48+
traceId: '12345678901234567890123456789012',
49+
sampled: true,
50+
spanId: '1234567890123456',
51+
dsc: {
52+
environment: 'staging',
53+
public_key: 'key',
54+
trace_id: '12345678901234567890123456789012',
55+
},
56+
}),
57+
}) as any,
5458
);
59+
jest.spyOn(SentryCoreCurrentScopes, 'getClient').mockImplementationOnce(() => mockedClient);
60+
61+
const traceData = getTraceData();
5562

5663
expect(traceData).toEqual({
5764
'sentry-trace': expect.stringMatching(/12345678901234567890123456789012-(.{16})-1/),
@@ -65,21 +72,22 @@ describe('getTraceData', () => {
6572
public_key: undefined,
6673
});
6774

68-
const traceData = getTraceData(
69-
// @ts-expect-error - we don't need to provide all the properties
70-
{
71-
isRecording: () => true,
72-
spanContext: () => {
73-
return {
74-
traceId: '12345678901234567890123456789012',
75-
spanId: '1234567890123456',
76-
traceFlags: TRACE_FLAG_SAMPLED,
77-
};
78-
},
75+
// @ts-expect-error - we don't need to provide all the properties
76+
jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => ({
77+
isRecording: () => true,
78+
spanContext: () => {
79+
return {
80+
traceId: '12345678901234567890123456789012',
81+
spanId: '1234567890123456',
82+
traceFlags: TRACE_FLAG_SAMPLED,
83+
};
7984
},
80-
mockedScope,
81-
mockedClient,
82-
);
85+
}));
86+
87+
jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce(() => mockedScope);
88+
jest.spyOn(SentryCoreCurrentScopes, 'getClient').mockImplementationOnce(() => mockedClient);
89+
90+
const traceData = getTraceData();
8391

8492
expect(traceData).toEqual({
8593
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
@@ -92,21 +100,21 @@ describe('getTraceData', () => {
92100
public_key: undefined,
93101
});
94102

95-
const traceData = getTraceData(
96-
// @ts-expect-error - we don't need to provide all the properties
97-
{
98-
isRecording: () => true,
99-
spanContext: () => {
100-
return {
101-
traceId: '12345678901234567890123456789012',
102-
spanId: '1234567890123456',
103-
traceFlags: TRACE_FLAG_SAMPLED,
104-
};
105-
},
103+
// @ts-expect-error - we don't need to provide all the properties
104+
jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => ({
105+
isRecording: () => true,
106+
spanContext: () => {
107+
return {
108+
traceId: '12345678901234567890123456789012',
109+
spanId: '1234567890123456',
110+
traceFlags: TRACE_FLAG_SAMPLED,
111+
};
106112
},
107-
mockedScope,
108-
undefined,
109-
);
113+
}));
114+
jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce(() => mockedScope);
115+
jest.spyOn(SentryCoreCurrentScopes, 'getClient').mockImplementationOnce(() => undefined);
116+
117+
const traceData = getTraceData();
110118

111119
expect(traceData).toEqual({
112120
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
@@ -115,21 +123,19 @@ describe('getTraceData', () => {
115123
});
116124

117125
it('returns an empty object if the `sentry-trace` value is invalid', () => {
118-
const traceData = getTraceData(
119-
// @ts-expect-error - we don't need to provide all the properties
120-
{
121-
isRecording: () => true,
122-
spanContext: () => {
123-
return {
124-
traceId: '1234567890123456789012345678901+',
125-
spanId: '1234567890123456',
126-
traceFlags: TRACE_FLAG_SAMPLED,
127-
};
128-
},
126+
// @ts-expect-error - we don't need to provide all the properties
127+
jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => ({
128+
isRecording: () => true,
129+
spanContext: () => {
130+
return {
131+
traceId: '1234567890123456789012345678901+',
132+
spanId: '1234567890123456',
133+
traceFlags: TRACE_FLAG_SAMPLED,
134+
};
129135
},
130-
mockedScope,
131-
mockedClient,
132-
);
136+
}));
137+
138+
const traceData = getTraceData();
133139

134140
expect(traceData).toEqual({});
135141
});

packages/opentelemetry/src/asyncContextStrategy.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from '.
1212
import type { CurrentScopes } from './types';
1313
import { getScopesFromContext } from './utils/contextData';
1414
import { getActiveSpan } from './utils/getActiveSpan';
15+
import { getTraceData } from './utils/getTraceData';
1516
import { suppressTracing } from './utils/suppressTracing';
1617

1718
/**
@@ -102,9 +103,10 @@ export function setOpenTelemetryContextAsyncContextStrategy(): void {
102103
startSpanManual,
103104
startInactiveSpan,
104105
getActiveSpan,
106+
suppressTracing,
107+
getTraceData,
105108
// The types here don't fully align, because our own `Span` type is narrower
106109
// than the OTEL one - but this is OK for here, as we now we'll only have OTEL spans passed around
107110
withActiveSpan: withActiveSpan as typeof defaultWithActiveSpan,
108-
suppressTracing: suppressTracing,
109111
});
110112
}

0 commit comments

Comments
 (0)