Skip to content

Commit

Permalink
feat(core): Add OpenTelemetry-specific getTraceData implementation (#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
Lms24 authored Aug 12, 2024
1 parent 0654dd0 commit 5aef4a0
Show file tree
Hide file tree
Showing 12 changed files with 192 additions and 89 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
const Sentry = require('@sentry/node');

Sentry.init({
dsn: 'https://[email protected]/1337',
transport: loggingTransport,
});

// express must be required after Sentry is initialized
const express = require('express');
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');

const app = express();

app.get('/test', (_req, res) => {
res.send({
response: `
<html>
<head>
${Sentry.getTraceMetaTags()}
</head>
<body>
Hi :)
</body>
</html>
`,
});
});

Sentry.setupExpressErrorHandler(app);

startExpressServerAndSendPortToRunner(app);
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';

describe('getTraceMetaTags', () => {
afterAll(() => {
cleanupChildProcesses();
});

test('injects sentry tracing <meta> tags without sampled flag for Tracing Without Performance', async () => {
const runner = createRunner(__dirname, 'server.js').start();

const response = await runner.makeRequest('get', '/test');

// @ts-ignore - response is defined, types just don't reflect it
const html = response?.response as unknown as string;

const [, traceId, spanId] = html.match(/<meta name="sentry-trace" content="([a-f0-9]{32})-([a-f0-9]{16})"\/>/) || [
undefined,
undefined,
undefined,
];

expect(traceId).toBeDefined();
expect(spanId).toBeDefined();

const sentryBaggageContent = html.match(/<meta name="baggage" content="(.*)"\/>/)?.[1];

expect(sentryBaggageContent).toContain('sentry-environment=production');
expect(sentryBaggageContent).toContain('sentry-public_key=public');
expect(sentryBaggageContent).toContain(`sentry-trace_id=${traceId}`);
});
});
9 changes: 4 additions & 5 deletions packages/astro/src/server/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
startSpan,
withIsolationScope,
} from '@sentry/node';
import type { Client, Scope, Span, SpanAttributes } from '@sentry/types';
import type { Scope, SpanAttributes } from '@sentry/types';
import {
addNonEnumerableProperty,
objectify,
Expand Down Expand Up @@ -151,7 +151,6 @@ async function instrumentRequest(
setHttpStatus(span, originalResponse.status);
}

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

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

if (!metaTags) {
return htmlChunk;
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/asyncContext/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Scope } from '@sentry/types';
import type { getTraceData } from '../utils/traceData';
import type {
startInactiveSpan,
startSpan,
Expand Down Expand Up @@ -64,4 +65,7 @@ export interface AsyncContextStrategy {

/** Suppress tracing in the given callback, ensuring no spans are generated inside of it. */
suppressTracing?: typeof suppressTracing;

/** Get trace data as serialized string values for propagation via `sentry-trace` and `baggage`. */
getTraceData?: typeof getTraceData;
}
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type { ClientClass } from './sdk';
export type { ClientClass as SentryCoreCurrentScopes } from './sdk';
export type { AsyncContextStrategy } from './asyncContext/types';
export type { Carrier } from './carrier';
export type { OfflineStore, OfflineTransportOptions } from './transports/offline';
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/utils/meta.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Client, Scope, Span } from '@sentry/types';
import { getTraceData } from './traceData';

/**
Expand All @@ -22,8 +21,8 @@ import { getTraceData } from './traceData';
* ```
*
*/
export function getTraceMetaTags(span?: Span, scope?: Scope, client?: Client): string {
return Object.entries(getTraceData(span, scope, client))
export function getTraceMetaTags(): string {
return Object.entries(getTraceData())
.map(([key, value]) => `<meta name="${key}" content="${value}"/>`)
.join('\n');
}
37 changes: 18 additions & 19 deletions packages/core/src/utils/traceData.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import type { Client, Scope, Span } from '@sentry/types';
import type { SerializedTraceData } from '@sentry/types';
import {
TRACEPARENT_REGEXP,
dynamicSamplingContextToSentryBaggageHeader,
generateSentryTraceHeader,
logger,
} from '@sentry/utils';
import { getAsyncContextStrategy } from '../asyncContext';
import { getMainCarrier } from '../carrier';
import { getClient, getCurrentScope } from '../currentScopes';
import { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from '../tracing';
import { getActiveSpan, getRootSpan, spanToTraceHeader } from './spanUtils';

type TraceData = {
'sentry-trace'?: string;
baggage?: string;
};

/**
* Extracts trace propagation data from the current span or from the client's scope (via transaction or propagation
* context) and serializes it to `sentry-trace` and `baggage` values to strings. These values can be used to propagate
Expand All @@ -22,29 +19,31 @@ type TraceData = {
* This function also applies some validation to the generated sentry-trace and baggage values to ensure that
* only valid strings are returned.
*
* @param span a span to take the trace data from. By default, the currently active span is used.
* @param scope the scope to take trace data from By default, the active current scope is used.
* @param client the SDK's client to take trace data from. By default, the current client is used.
*
* @returns an object with the tracing data values. The object keys are the name of the tracing key to be used as header
* or meta tag name.
*/
export function getTraceData(span?: Span, scope?: Scope, client?: Client): TraceData {
const clientToUse = client || getClient();
const scopeToUse = scope || getCurrentScope();
const spanToUse = span || getActiveSpan();
export function getTraceData(): SerializedTraceData {
const carrier = getMainCarrier();
const acs = getAsyncContextStrategy(carrier);
if (acs.getTraceData) {
return acs.getTraceData();
}

const client = getClient();
const scope = getCurrentScope();
const span = getActiveSpan();

const { dsc, sampled, traceId } = scopeToUse.getPropagationContext();
const rootSpan = spanToUse && getRootSpan(spanToUse);
const { dsc, sampled, traceId } = scope.getPropagationContext();
const rootSpan = span && getRootSpan(span);

const sentryTrace = spanToUse ? spanToTraceHeader(spanToUse) : generateSentryTraceHeader(traceId, undefined, sampled);
const sentryTrace = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled);

const dynamicSamplingContext = rootSpan
? getDynamicSamplingContextFromSpan(rootSpan)
: dsc
? dsc
: clientToUse
? getDynamicSamplingContextFromClient(traceId, clientToUse)
: client
? getDynamicSamplingContextFromClient(traceId, client)
: undefined;

const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
Expand Down
124 changes: 65 additions & 59 deletions packages/core/test/lib/utils/traceData.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { SentrySpan, getTraceData } from '../../../src/';
import * as SentryCoreCurrentScopes from '../../../src/currentScopes';
import * as SentryCoreTracing from '../../../src/tracing';
import * as SentryCoreSpanUtils from '../../../src/utils/spanUtils';

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

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

const tags = getTraceData(mockedSpan, mockedScope, mockedClient);
const data = getTraceData();

expect(tags).toEqual({
expect(data).toEqual({
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
baggage: 'sentry-environment=production',
});
}
});

it('returns propagationContext DSC data if no span is available', () => {
const traceData = getTraceData(
undefined,
{
getPropagationContext: () => ({
traceId: '12345678901234567890123456789012',
sampled: true,
spanId: '1234567890123456',
dsc: {
environment: 'staging',
public_key: 'key',
trace_id: '12345678901234567890123456789012',
},
}),
} as any,
mockedClient,
jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => undefined);
jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce(
() =>
({
getPropagationContext: () => ({
traceId: '12345678901234567890123456789012',
sampled: true,
spanId: '1234567890123456',
dsc: {
environment: 'staging',
public_key: 'key',
trace_id: '12345678901234567890123456789012',
},
}),
}) as any,
);
jest.spyOn(SentryCoreCurrentScopes, 'getClient').mockImplementationOnce(() => mockedClient);

const traceData = getTraceData();

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

const traceData = getTraceData(
// @ts-expect-error - we don't need to provide all the properties
{
isRecording: () => true,
spanContext: () => {
return {
traceId: '12345678901234567890123456789012',
spanId: '1234567890123456',
traceFlags: TRACE_FLAG_SAMPLED,
};
},
// @ts-expect-error - we don't need to provide all the properties
jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => ({
isRecording: () => true,
spanContext: () => {
return {
traceId: '12345678901234567890123456789012',
spanId: '1234567890123456',
traceFlags: TRACE_FLAG_SAMPLED,
};
},
mockedScope,
mockedClient,
);
}));

jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce(() => mockedScope);
jest.spyOn(SentryCoreCurrentScopes, 'getClient').mockImplementationOnce(() => mockedClient);

const traceData = getTraceData();

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

const traceData = getTraceData(
// @ts-expect-error - we don't need to provide all the properties
{
isRecording: () => true,
spanContext: () => {
return {
traceId: '12345678901234567890123456789012',
spanId: '1234567890123456',
traceFlags: TRACE_FLAG_SAMPLED,
};
},
// @ts-expect-error - we don't need to provide all the properties
jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => ({
isRecording: () => true,
spanContext: () => {
return {
traceId: '12345678901234567890123456789012',
spanId: '1234567890123456',
traceFlags: TRACE_FLAG_SAMPLED,
};
},
mockedScope,
undefined,
);
}));
jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce(() => mockedScope);
jest.spyOn(SentryCoreCurrentScopes, 'getClient').mockImplementationOnce(() => undefined);

const traceData = getTraceData();

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

it('returns an empty object if the `sentry-trace` value is invalid', () => {
const traceData = getTraceData(
// @ts-expect-error - we don't need to provide all the properties
{
isRecording: () => true,
spanContext: () => {
return {
traceId: '1234567890123456789012345678901+',
spanId: '1234567890123456',
traceFlags: TRACE_FLAG_SAMPLED,
};
},
// @ts-expect-error - we don't need to provide all the properties
jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => ({
isRecording: () => true,
spanContext: () => {
return {
traceId: '1234567890123456789012345678901+',
spanId: '1234567890123456',
traceFlags: TRACE_FLAG_SAMPLED,
};
},
mockedScope,
mockedClient,
);
}));

const traceData = getTraceData();

expect(traceData).toEqual({});
});
Expand Down
4 changes: 3 additions & 1 deletion packages/opentelemetry/src/asyncContextStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from '.
import type { CurrentScopes } from './types';
import { getScopesFromContext } from './utils/contextData';
import { getActiveSpan } from './utils/getActiveSpan';
import { getTraceData } from './utils/getTraceData';
import { suppressTracing } from './utils/suppressTracing';

/**
Expand Down Expand Up @@ -102,9 +103,10 @@ export function setOpenTelemetryContextAsyncContextStrategy(): void {
startSpanManual,
startInactiveSpan,
getActiveSpan,
suppressTracing,
getTraceData,
// The types here don't fully align, because our own `Span` type is narrower
// than the OTEL one - but this is OK for here, as we now we'll only have OTEL spans passed around
withActiveSpan: withActiveSpan as typeof defaultWithActiveSpan,
suppressTracing: suppressTracing,
});
}
Loading

0 comments on commit 5aef4a0

Please sign in to comment.