Thanks to visit codestin.com
Credit goes to github.com

Skip to content
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
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 @@ -586,12 +585,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 @@ -928,37 +928,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
22 changes: 21 additions & 1 deletion packages/cloudflare/src/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import { AsyncLocalStorage } from 'node:async_hooks';
import type { Scope } from '@sentry/core';
import {
_INTERNAL_createTracingChannelBinding,
_INTERNAL_safeMathRandom,
generateTraceId,
getDefaultCurrentScope,
getDefaultIsolationScope,
isContinuingTrace,
setAsyncContextStrategy,
} from '@sentry/core';

Expand Down Expand Up @@ -55,8 +58,25 @@ export function setAsyncLocalStorageAsyncContextStrategy(): void {
}

function withIsolationScope<T>(callback: (isolationScope: Scope) => T): T {
const scope = getScopes().scope;
// The current scope is forked alongside the isolation scope, matching the OpenTelemetry strategy
// (`buildContextWithSentryScopes` clones it on every fork). Sharing it by reference would let
// the propagation context reset below leak back out to the caller.
const scope = getScopes().scope.clone();
const isolationScope = getScopes().isolationScope.clone();

// When forking an isolation scope, unless we are continuing an incoming
// trace, we give the freshly forked scope its own trace. This way, new
// root spans in an isolation scope will get separate traces. The previous
// trace's `sampled` and `propagationSpanId` are dropped on purpose.
// Carrying them over would apply the old trace's sampling decision to the
// new one and propagate a span id from a different trace.
if (!isContinuingTrace(scope.getPropagationContext())) {
scope.setPropagationContext({
traceId: generateTraceId(),
sampleRand: _INTERNAL_safeMathRandom(),
});
}

return asyncStorage.run({ scope, isolationScope }, () => {
return callback(isolationScope);
});
Expand Down
88 changes: 88 additions & 0 deletions packages/cloudflare/test/async.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,92 @@ describe('withIsolationScope()', () => {
done();
});
}));

it('forks the current scope as well, so mutations do not leak out', () =>
new Promise<void>(done => {
const initialScope = getCurrentScope();
initialScope.setTag('aa', 'aa');

withIsolationScope(() => {
const scope = getCurrentScope();
expect(scope).not.toBe(initialScope);
expect(scope.getScopeData().tags).toEqual({ aa: 'aa' });

scope.setTag('bb', 'bb');
done();
});

expect(initialScope.getScopeData().tags).toEqual({ aa: 'aa' });
}));

it('gives each forked isolation scope its own trace id when not continuing an incoming trace', () => {
const traceIds: string[] = [];

withIsolationScope(() => {
traceIds.push(getCurrentScope().getPropagationContext().traceId);
});
withIsolationScope(() => {
traceIds.push(getCurrentScope().getPropagationContext().traceId);
});

expect(traceIds[0]).toMatch(/^[a-f0-9]{32}$/);
expect(traceIds[1]).toMatch(/^[a-f0-9]{32}$/);
expect(traceIds[0]).not.toBe(traceIds[1]);
});

it('keeps the trace id when continuing an incoming trace (parentSpanId set)', () => {
const incomingTraceId = 'cafecafecafecafecafecafecafecafe';
getCurrentScope().setPropagationContext({
traceId: incomingTraceId,
parentSpanId: '1234567890abcdef',
sampleRand: 0.42,
});

withIsolationScope(() => {
expect(getCurrentScope().getPropagationContext().traceId).toBe(incomingTraceId);
});
});

// A trace-id-only `sentry-trace` header yields a propagation context with a `dsc` but no
// `parentSpanId`, because the span id is optional in the header. Such a trace is still being
// continued, so its trace id must survive the fork.
it('keeps the trace id when continuing an incoming trace without a span id (dsc set)', () => {
const incomingTraceId = 'cafecafecafecafecafecafecafecafe';
getCurrentScope().setPropagationContext({
traceId: incomingTraceId,
sampleRand: 0.42,
dsc: { trace_id: incomingTraceId, sample_rate: '1' },
});

withIsolationScope(() => {
const propagationContext = getCurrentScope().getPropagationContext();

expect(propagationContext.traceId).toBe(incomingTraceId);
expect(propagationContext.dsc).toEqual({ trace_id: incomingTraceId, sample_rate: '1' });
});
});

// A new trace must not inherit the previous trace's sampling decision or propagation span id.
// Keeping them would apply the old trace's sampling decision to the new one and propagate a
// span id belonging to a different trace.
it('drops the previous trace data when giving a forked isolation scope its own trace', () => {
const oldTraceId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
getCurrentScope().setPropagationContext({
traceId: oldTraceId,
sampleRand: 0.1,
sampled: true,
propagationSpanId: 'bbbbbbbbbbbbbbbb',
});

withIsolationScope(() => {
const propagationContext = getCurrentScope().getPropagationContext();

expect(propagationContext.traceId).toMatch(/^[a-f0-9]{32}$/);
expect(propagationContext.traceId).not.toBe(oldTraceId);
expect(propagationContext.sampleRand).toEqual(expect.any(Number));
expect(propagationContext.sampled).toBeUndefined();
expect(propagationContext.propagationSpanId).toBeUndefined();
expect(propagationContext.dsc).toBeUndefined();
});
});
});
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
98 changes: 98 additions & 0 deletions packages/core/src/monitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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';
import { isContinuingTrace } from './utils/tracing';

/**
* 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;
}

// `withIsolationScope` gives the fork its own trace, so unless `isolateTrace` is set we restore the
// parent's trace below. Copied rather than aliased: sharing the object with the parent scope would
// let in-place writes inside the callback (e.g. the HTTP server integration assigning
// `propagationSpanId`) rewrite the parent's trace.
const oldPropagationContext = { ...getCurrentScope().getPropagationContext() };

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

// Mirrors the reset condition in the async context strategies: only a fork that was given a fresh
// trace needs the parent's trace put back.
const scope = getCurrentScope();
if (!isContinuingTrace(scope.getPropagationContext())) {
scope.setPropagationContext(oldPropagationContext);
}

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();
}
Loading
Loading