Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Add OpenTelemetry-specific getTraceData implementation #13281

Merged
merged 7 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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://public@dsn.ingest.sentry.io/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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: is this on purpose? 😅 seems unintentional (and probably breaking, technically...)

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
Loading