Skip to content
Open
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
7 changes: 0 additions & 7 deletions packages/browser/src/tracing/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
getClient,
getCurrentScope,
getDynamicSamplingContextFromSpan,
getIsolationScope,
getLocationHref,
GLOBAL_OBJ,
hasSpansEnabled,
Expand Down Expand Up @@ -554,12 +553,6 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption

maybeEndActiveSpan();

getIsolationScope().setPropagationContext({
traceId: generateTraceId(),
sampleRand: Math.random(),
propagationSpanId: hasSpansEnabled() ? undefined : generateSpanId(),
});

const scope = getCurrentScope();
scope.setPropagationContext({
traceId: generateTraceId(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -937,37 +937,24 @@ describe('browserTracingIntegration', () => {
setCurrentClient(client);
client.init();

const oldIsolationScopePropCtx = getIsolationScope().getPropagationContext();
const oldCurrentScopePropCtx = getCurrentScope().getPropagationContext();

startBrowserTracingNavigationSpan(client, { name: 'test navigation span' });

const newIsolationScopePropCtx = getIsolationScope().getPropagationContext();
const newCurrentScopePropCtx = getCurrentScope().getPropagationContext();

expect(oldCurrentScopePropCtx).toEqual({
traceId: expect.stringMatching(/[a-f0-9]{32}/),
propagationSpanId: expect.stringMatching(/[a-f0-9]{16}/),
sampleRand: expect.any(Number),
});
expect(oldIsolationScopePropCtx).toEqual({
traceId: expect.stringMatching(/[a-f0-9]{32}/),
sampleRand: expect.any(Number),
});

expect(newCurrentScopePropCtx).toEqual({
traceId: expect.stringMatching(/[a-f0-9]{32}/),
propagationSpanId: expect.stringMatching(/[a-f0-9]{16}/),
sampleRand: expect.any(Number),
});
expect(newIsolationScopePropCtx).toEqual({
traceId: expect.stringMatching(/[a-f0-9]{32}/),
propagationSpanId: expect.stringMatching(/[a-f0-9]{16}/),
sampleRand: expect.any(Number),
});

expect(newIsolationScopePropCtx.traceId).not.toEqual(oldIsolationScopePropCtx.traceId);
expect(newCurrentScopePropCtx.traceId).not.toEqual(oldCurrentScopePropCtx.traceId);
expect(newIsolationScopePropCtx.propagationSpanId).not.toEqual(oldIsolationScopePropCtx.propagationSpanId);
});

it("saves the span's positive sampling decision and its DSC on the propagationContext when the span finishes", () => {
Expand Down
77 changes: 1 addition & 76 deletions packages/core/src/exports.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import type { AttributeObject, RawAttribute, RawAttributes } from './attributes';
import { getClient, getCurrentScope, getIsolationScope, withIsolationScope } from './currentScopes';
import { getClient, getCurrentScope, getIsolationScope } from './currentScopes';
import { DEBUG_BUILD } from './debug-build';
import type { CaptureContext } from './scope';
import { closeSession, makeSession, updateSession } from './session';
import { startNewTrace } from './tracing/trace';
import type { CheckIn, FinishedCheckIn, MonitorConfig } from './types/checkin';
import type { Event, EventHint } from './types/event';
import type { EventProcessor } from './types/eventprocessor';
import type { Extra, Extras } from './types/extra';
Expand All @@ -13,12 +11,9 @@ import type { Session, SessionContext } from './types/session';
import type { SeverityLevel } from './types/severity';
import type { User } from './types/user';
import { debug } from './utils/debug-logger';
import { isThenable } from './utils/is';
import { uuid4 } from './utils/misc';
import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent';
import { parseEventHintOrCaptureContext } from './utils/prepareEvent';
import { getCombinedScopeData } from './utils/scopeData';
import { timestampInSeconds } from './utils/time';
import { GLOBAL_OBJ } from './utils/worldwide';

/**
Expand Down Expand Up @@ -182,76 +177,6 @@ export function lastEventId(): string | undefined {
return getIsolationScope().lastEventId();
}

/**
* Create a cron monitor check in and send it to Sentry.
*
* @param checkIn An object that describes a check in.
* @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want
* to create a monitor automatically when sending a check in.
*/
export function captureCheckIn(checkIn: CheckIn, upsertMonitorConfig?: MonitorConfig): string {
const scope = getCurrentScope();
const client = getClient();
if (!client) {
DEBUG_BUILD && debug.warn('Cannot capture check-in. No client defined.');
} else if (!client.captureCheckIn) {
DEBUG_BUILD && debug.warn('Cannot capture check-in. Client does not support sending check-ins.');
} else {
return client.captureCheckIn(checkIn, upsertMonitorConfig, scope);
}

return uuid4();
}

/**
* Wraps a callback with a cron monitor check in. The check in will be sent to Sentry when the callback finishes.
*
* @param monitorSlug The distinct slug of the monitor.
* @param callback Callback to be monitored
* @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want
* to create a monitor automatically when sending a check in.
*/
export function withMonitor<T>(
monitorSlug: CheckIn['monitorSlug'],
callback: () => T,
upsertMonitorConfig?: MonitorConfig,
): T {
function runCallback(): T {
const checkInId = captureCheckIn({ monitorSlug, status: 'in_progress' }, upsertMonitorConfig);
const now = timestampInSeconds();

function finishCheckIn(status: FinishedCheckIn['status']): void {
captureCheckIn({ monitorSlug, status, checkInId, duration: timestampInSeconds() - now });
}
// Default behavior without isolateTrace
let maybePromiseResult: T;
try {
maybePromiseResult = callback();
} catch (e) {
finishCheckIn('error');
throw e;
}

if (isThenable(maybePromiseResult)) {
return maybePromiseResult.then(
r => {
finishCheckIn('ok');
return r;
},
e => {
finishCheckIn('error');
throw e;
},
) as T;
}
finishCheckIn('ok');

return maybePromiseResult;
}

return withIsolationScope(() => (upsertMonitorConfig?.isolateTrace ? startNewTrace(runCallback) : runCallback()));
}

/**
* Call `flush()` on the current client, if there is one. See {@link Client.flush}.
*
Expand Down
93 changes: 93 additions & 0 deletions packages/core/src/monitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { getClient, getCurrentScope, withIsolationScope } from './currentScopes';
import { DEBUG_BUILD } from './debug-build';
import { startNewTrace } from './tracing/trace';
import type { CheckIn, FinishedCheckIn, MonitorConfig } from './types/checkin';
import { debug } from './utils/debug-logger';
import { isThenable } from './utils/is';
import { uuid4 } from './utils/misc';
import { timestampInSeconds } from './utils/time';

/**
* Wraps a callback with a cron monitor check in. The check in will be sent to Sentry when the callback finishes.
*
* @param monitorSlug The distinct slug of the monitor.
* @param callback Callback to be monitored
* @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want
* to create a monitor automatically when sending a check in.
*/
export function withMonitor<T>(
monitorSlug: CheckIn['monitorSlug'],
callback: () => T,
upsertMonitorConfig?: MonitorConfig,
): T {
function runCallback(): T {
const checkInId = captureCheckIn({ monitorSlug, status: 'in_progress' }, upsertMonitorConfig);
const now = timestampInSeconds();

function finishCheckIn(status: FinishedCheckIn['status']): void {
captureCheckIn({ monitorSlug, status, checkInId, duration: timestampInSeconds() - now });
}
// Default behavior without isolateTrace
let maybePromiseResult: T;
try {
maybePromiseResult = callback();
} catch (e) {
finishCheckIn('error');
throw e;
}

if (isThenable(maybePromiseResult)) {
return maybePromiseResult.then(
r => {
finishCheckIn('ok');
return r;
},
e => {
finishCheckIn('error');
throw e;
},
) as T;
}
finishCheckIn('ok');

return maybePromiseResult;
}

// With isolation scope resets the propagation context, so if we do not pass isolateTace, we want to manually continue the trace
const oldPropagationContext = getCurrentScope().getPropagationContext();

return withIsolationScope(() => {
if (upsertMonitorConfig?.isolateTrace) {
return startNewTrace(runCallback);
}

// If we are not isolating the trace, in this case we want to keep the same trace as the parent
const newPropagationContext = getCurrentScope().getPropagationContext();
if (!newPropagationContext.parentSpanId) {
getCurrentScope().setPropagationContext(oldPropagationContext);
}
Comment thread
cursor[bot] marked this conversation as resolved.

return runCallback();
});
}

/**
* Create a cron monitor check in and send it to Sentry.
*
* @param checkIn An object that describes a check in.
* @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want
* to create a monitor automatically when sending a check in.
*/
export function captureCheckIn(checkIn: CheckIn, upsertMonitorConfig?: MonitorConfig): string {
const scope = getCurrentScope();
const client = getClient();
if (!client) {
DEBUG_BUILD && debug.warn('Cannot capture check-in. No client defined.');
} else if (!client.captureCheckIn) {
DEBUG_BUILD && debug.warn('Cannot capture check-in. Client does not support sending check-ins.');
} else {
return client.captureCheckIn(checkIn, upsertMonitorConfig, scope);
}

return uuid4();
}
3 changes: 1 addition & 2 deletions packages/core/src/shared-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ export * from './tracing';
export * from './semanticAttributes';
export { createEventEnvelope, createSessionEnvelope } from './envelope';
export {
captureCheckIn,
withMonitor,
captureException,
captureEvent,
captureMessage,
Expand All @@ -36,6 +34,7 @@ export {
captureSession,
addEventProcessor,
} from './exports';
export { withMonitor, captureCheckIn } from './monitor';
export {
getCurrentScope,
getIsolationScope,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/tracing/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ function createChildOrRootSpan({
const isolationScope = getIsolationScope();

if (!hasSpansEnabled()) {
const scopePropagationContext = { ...isolationScope.getPropagationContext(), ...scope.getPropagationContext() };
const scopePropagationContext = scope.getPropagationContext();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it seems like this removes a footgun where we potentially incorrectly merged a propagation context before here. I like it!

const traceId = parentSpan ? parentSpan.spanContext().traceId : scopePropagationContext.traceId;

// The placeholder is a thin marker; it carries no sampling decision or DSC. Both are read from
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/lib/sdk.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, type Mock, test, vi } from 'vitest';
import type { Client } from '../../src/client';
import { getCurrentScope } from '../../src/currentScopes';
import { captureCheckIn } from '../../src/exports';
import { captureCheckIn } from '../../src/monitor';
import { installedIntegrations } from '../../src/integration';
import { initAndBind, setCurrentClient } from '../../src/sdk';
import type { Integration } from '../../src/types/integration';
Expand Down
14 changes: 14 additions & 0 deletions packages/opentelemetry/src/asyncContextStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as api from '@opentelemetry/api';
import type { Scope, TracingChannelBinding } from '@sentry/core';
import {
getAsyncContextStrategy,
_INTERNAL_safeMathRandom,
generateTraceId,
getDefaultCurrentScope,
getDefaultIsolationScope,
getMainCarrier,
Expand Down Expand Up @@ -86,6 +88,18 @@ export function setOpenTelemetryContextAsyncContextStrategy(): AsyncLocalStorage
// the OTEL context manager, which uses the presence of this key to determine if it should
// fork the isolation scope, or not
return api.context.with(ctx.setValue(SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY, true), () => {
// When forking an isolation scope, unless we are continuing an incoming trace (identified by a `parentSpanId`),
// we give the freshly forked scope its own trace.
// This way, new root spans in an isolation scope will get separate traces
const scope = getCurrentScope();
const propagationContext = scope.getPropagationContext();
if (!propagationContext.parentSpanId) {
scope.setPropagationContext({
...propagationContext,
traceId: generateTraceId(),
sampleRand: _INTERNAL_safeMathRandom(),
});
}
return callback(getIsolationScope());
});
}
Expand Down
27 changes: 21 additions & 6 deletions packages/opentelemetry/test/asyncContextStrategy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ import { TraceState } from '../src/utils/TraceState';
import { mockSdkInit } from './helpers/mockSdkInit';

describe('asyncContextStrategy', () => {
// `withIsolationScope` gives the forked current scope a fresh propagation context (unless it is
// continuing an incoming trace), so scope data is expected to match apart from that context.
function scopeDataWithoutPropagationContext(
scope: Scope,
): Omit<ReturnType<Scope['getScopeData']>, 'propagationContext'> {
const { propagationContext: _propagationContext, ...rest } = scope.getScopeData();
return rest;
}

beforeEach(() => {
getCurrentScope().clear();
getIsolationScope().clear();
Expand All @@ -45,7 +54,8 @@ describe('asyncContextStrategy', () => {
expect(scope1).not.toBe(initialScope);
expect(isolationScope1).not.toBe(initialIsolationScope);

expect(scope1.getScopeData()).toEqual(initialScope.getScopeData());
expect(scopeDataWithoutPropagationContext(scope1)).toEqual(scopeDataWithoutPropagationContext(initialScope));
expect(scope1.getPropagationContext().traceId).not.toBe(initialScope.getPropagationContext().traceId);
expect(isolationScope1.getScopeData()).toEqual(initialIsolationScope.getScopeData());

scope1.setExtra('b', 'b');
Expand Down Expand Up @@ -162,7 +172,8 @@ describe('asyncContextStrategy', () => {
expect(scope1).not.toBe(initialScope);
expect(isolationScope1).not.toBe(initialIsolationScope);

expect(scope1.getScopeData()).toEqual(initialScope.getScopeData());
expect(scopeDataWithoutPropagationContext(scope1)).toEqual(scopeDataWithoutPropagationContext(initialScope));
expect(scope1.getPropagationContext().traceId).not.toBe(initialScope.getPropagationContext().traceId);
expect(isolationScope1.getScopeData()).toEqual(initialIsolationScope.getScopeData());

await asyncSetExtra(scope1, 'b', 'b');
Expand Down Expand Up @@ -207,7 +218,8 @@ describe('asyncContextStrategy', () => {
expect(scope1).not.toBe(initialScope);
expect(isolationScope1).not.toBe(initialIsolationScope);

expect(scope1.getScopeData()).toEqual(initialScope.getScopeData());
expect(scopeDataWithoutPropagationContext(scope1)).toEqual(scopeDataWithoutPropagationContext(initialScope));
expect(scope1.getPropagationContext().traceId).not.toBe(initialScope.getPropagationContext().traceId);
expect(isolationScope1.getScopeData()).toEqual(initialIsolationScope.getScopeData());

scope1.setExtra('b', 'b');
Expand Down Expand Up @@ -244,7 +256,8 @@ describe('asyncContextStrategy', () => {
expect(scope1).not.toBe(initialScope);
expect(isolationScope1).not.toBe(initialIsolationScope);

expect(scope1.getScopeData()).toEqual(initialScope.getScopeData());
expect(scopeDataWithoutPropagationContext(scope1)).toEqual(scopeDataWithoutPropagationContext(initialScope));
expect(scope1.getPropagationContext().traceId).not.toBe(initialScope.getPropagationContext().traceId);
expect(isolationScope1.getScopeData()).toEqual(initialIsolationScope.getScopeData());

scope1.setExtra('b2', 'b');
Expand Down Expand Up @@ -294,7 +307,8 @@ describe('asyncContextStrategy', () => {
expect(scope1).not.toBe(initialScope);
expect(isolationScope1).not.toBe(initialIsolationScope);

expect(scope1.getScopeData()).toEqual(initialScope.getScopeData());
expect(scopeDataWithoutPropagationContext(scope1)).toEqual(scopeDataWithoutPropagationContext(initialScope));
expect(scope1.getPropagationContext().traceId).not.toBe(initialScope.getPropagationContext().traceId);
expect(isolationScope1.getScopeData()).toEqual(initialIsolationScope.getScopeData());

await asyncSetExtra(scope1, 'b', 'b');
Expand Down Expand Up @@ -331,7 +345,8 @@ describe('asyncContextStrategy', () => {
expect(scope1).not.toBe(initialScope);
expect(isolationScope1).not.toBe(initialIsolationScope);

expect(scope1.getScopeData()).toEqual(initialScope.getScopeData());
expect(scopeDataWithoutPropagationContext(scope1)).toEqual(scopeDataWithoutPropagationContext(initialScope));
expect(scope1.getPropagationContext().traceId).not.toBe(initialScope.getPropagationContext().traceId);
expect(isolationScope1.getScopeData()).toEqual(initialIsolationScope.getScopeData());

scope1.setExtra('b2', 'b');
Expand Down
Loading
Loading