Skip to content
Draft
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
Expand Up @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare';

export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
}));
Original file line number Diff line number Diff line change
@@ -1,50 +1,49 @@
import type { TransactionEvent } from '@sentry/core';
import { expect, it } from 'vitest';
import { createRunner } from '../../../runner';

// The Durable Object, reached from inside the entrypoint, emits an `http.server`
// transaction whose only children are the two
// `auto.db.cloudflare.durable_object` storage spans (`get` + `put`) — present
// only when the class was auto-instrumented.
function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void {
expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare');
expect(transactionEvent.spans).toEqual([
expect.objectContaining({
op: 'db',
description: 'durable_object_storage_get',
origin: 'auto.db.cloudflare.durable_object',
}),
expect.objectContaining({
op: 'db',
description: 'durable_object_storage_put',
origin: 'auto.db.cloudflare.durable_object',
}),
]);
}

function expectPlainTransaction(name: string) {
return (transactionEvent: TransactionEvent): void => {
expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare');
expect(transactionEvent.transaction).toBe(name);
expect(transactionEvent.spans).toHaveLength(0);
};
}
import { getSpanOp } from '../../../spanUtils';

// A single request fans out through the whole auto-wrapped chain: default
// handler (`/chain`) → self-bound `CounterEntrypoint` (`/work`) → `Counter`
// Durable Object. All three transactions arrive only if the build-time transform
// wrapped the default export, the entrypoint, and the DO — and it proves a DO
// invoked from *within* an auto-instrumented entrypoint is itself instrumented.
// Durable Object. All three segment spans arrive only if the build-time
// transform wrapped the default export, the entrypoint, and the DO — and it
// proves a DO invoked from *within* an auto-instrumented entrypoint is itself
// instrumented.
//
// Every route here is a raw URL, so the streamed segment names keep the method
// only and each hop is identified by its `url.path` attribute.
it('auto-instruments a Durable Object invoked from within a WorkerEntrypoint', async ({ signal }) => {
const runner = createRunner(__dirname)
.unordered()
.expect(envelope => expectPlainTransaction('GET /chain')(envelope[1]?.[0]?.[1] as TransactionEvent))
.expect(envelope => expectPlainTransaction('GET /work')(envelope[1]?.[0]?.[1] as TransactionEvent))
.expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent))
.start(signal);
const runner = createRunner(__dirname).start(signal);

// Each hop streams from its own isolate, so the three segment spans of the trace arrive in
// separate envelopes.
const spansPromise = runner.collectStreamedSpans(
spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 3,
);

await runner.makeRequest('get', '/chain');
await runner.completed();

const spans = await spansPromise;
const chainSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/chain');
const entrypointSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/work');
const durableObjectSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/increment');

expect(getSpanOp(chainSpan!)).toBe('http.server');
expect(chainSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' });

expect(getSpanOp(entrypointSpan!)).toBe('http.server');
expect(entrypointSpan?.parent_span_id).toBe(chainSpan?.span_id);

expect(getSpanOp(durableObjectSpan!)).toBe('http.server');
expect(durableObjectSpan?.parent_span_id).toBe(entrypointSpan?.span_id);

// The `auto.db.cloudflare.durable_object` storage pair (`get` + `put`) is the fingerprint of an
// instrumented Durable Object.
expect(
spans
.filter(span => span.parent_span_id === durableObjectSpan?.span_id)
.map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })),
).toEqual([
{ name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' },
{ name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' },
]);
});
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class CounterImpl extends DurableObject<Env> {
// untouched (no double-wrap) while still auto-wrapping the plain entrypoint and
// default export in the same file.
export const Counter = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({ dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0 }),
(env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0 }),
CounterImpl,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare';

export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
}));
Original file line number Diff line number Diff line change
@@ -1,56 +1,69 @@
import type { TransactionEvent } from '@sentry/core';
import { expect, it } from 'vitest';
import { createRunner } from '../../../runner';

// A Durable Object invoked via `fetch` emits an `http.server` transaction whose
// only children are the two `auto.db.cloudflare.durable_object` storage spans
// (`get` + `put`). Here they come from the manual wrap — the assertion also
// proves the transform did NOT double-wrap (a double-wrap would nest proxies or
// break the build).
function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void {
expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare');
expect(transactionEvent.spans).toEqual([
expect.objectContaining({
op: 'db',
description: 'durable_object_storage_get',
origin: 'auto.db.cloudflare.durable_object',
}),
expect.objectContaining({
op: 'db',
description: 'durable_object_storage_put',
origin: 'auto.db.cloudflare.durable_object',
}),
]);
}

function expectPlainTransaction(name: string) {
return (transactionEvent: TransactionEvent): void => {
expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare');
expect(transactionEvent.transaction).toBe(name);
expect(transactionEvent.spans).toHaveLength(0);
};
}
import { getSpanOp } from '../../../spanUtils';

// The Durable Object is manually wrapped with
// `Sentry.instrumentDurableObjectWithSentry`; the `GreeterEntrypoint` and the
// default export are plain. The transform must skip the manual DO (no
// double-wrap) yet still auto-wrap the entrypoint and default handler — so the
// manual DO transaction (with storage spans) and both auto-wrapped transactions
// all arrive exactly once.
// manual DO span (with storage children) and both auto-wrapped spans all arrive
// exactly once.
//
// Every route here is a raw URL, so the streamed segment names keep the method
// only and each hop is identified by its `url.path` attribute.
it('leaves a manually wrapped Durable Object untouched while auto-wrapping a sibling WorkerEntrypoint', async ({
signal,
}) => {
const runner = createRunner(__dirname)
.unordered()
.expect(envelope => expectPlainTransaction('GET /call-entrypoint')(envelope[1]?.[0]?.[1] as TransactionEvent))
.expect(envelope => expectPlainTransaction('GET /greet')(envelope[1]?.[0]?.[1] as TransactionEvent))
.expect(envelope => expectPlainTransaction('GET /increment')(envelope[1]?.[0]?.[1] as TransactionEvent))
.expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent))
.start(signal);
const runner = createRunner(__dirname).start(signal);

// Each hop streams from its own isolate, so the segment spans of a trace arrive in separate
// envelopes.
const entrypointSpansPromise = runner.collectStreamedSpans(spansOfTrace => {
const paths = spansOfTrace.filter(span => span.is_segment).map(span => span.attributes['url.path']?.value);
return paths.includes('/call-entrypoint') && paths.includes('/greet');
});

await runner.makeRequest('get', '/call-entrypoint');

const entrypointTraceSpans = await entrypointSpansPromise;
const callEntrypointSpan = entrypointTraceSpans.find(
span => span.is_segment && span.attributes['url.path']?.value === '/call-entrypoint',
);
const greetSpan = entrypointTraceSpans.find(
span => span.is_segment && span.attributes['url.path']?.value === '/greet',
);

expect(getSpanOp(callEntrypointSpan!)).toBe('http.server');
expect(callEntrypointSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' });

expect(getSpanOp(greetSpan!)).toBe('http.server');
expect(greetSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' });
expect(greetSpan?.parent_span_id).toBe(callEntrypointSpan?.span_id);

const durableObjectSpansPromise = runner.collectStreamedSpans(
spansOfTrace =>
spansOfTrace.filter(span => span.is_segment).length === 2 &&
spansOfTrace.some(span => span.attributes['url.path']?.value === '/increment'),
);

await runner.makeRequest('get', '/increment');
await runner.completed();

const durableObjectTraceSpans = await durableObjectSpansPromise;
const workerSpan = durableObjectTraceSpans.find(span => span.is_segment && !span.parent_span_id);
const durableObjectSpan = durableObjectTraceSpans.find(span => span.is_segment && span.parent_span_id);

expect(getSpanOp(workerSpan!)).toBe('http.server');
expect(getSpanOp(durableObjectSpan!)).toBe('http.server');
expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id);

// The storage pair comes from the manual wrap here. Exactly two of them also rules out a
// double-wrap, which would nest proxies or break the build.
expect(
durableObjectTraceSpans
.filter(span => span.parent_span_id === durableObjectSpan?.span_id)
.map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })),
).toEqual([
{ name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' },
{ name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' },
]);
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare';

export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
}));
Original file line number Diff line number Diff line change
@@ -1,55 +1,67 @@
import type { TransactionEvent } from '@sentry/core';
import { expect, it } from 'vitest';
import { createRunner } from '../../../runner';

// A Durable Object invoked via `fetch` emits an `http.server` transaction whose
// only children are the two `auto.db.cloudflare.durable_object` storage spans
// (`get` + `put`) — present only when the class was auto-instrumented.
function expectDurableObjectTransaction(transactionEvent: TransactionEvent): void {
expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare');
expect(transactionEvent.spans).toEqual([
expect.objectContaining({
op: 'db',
description: 'durable_object_storage_get',
origin: 'auto.db.cloudflare.durable_object',
}),
expect.objectContaining({
op: 'db',
description: 'durable_object_storage_put',
origin: 'auto.db.cloudflare.durable_object',
}),
]);
}

// A plain `http.server` transaction with no child spans, identified by its
// transaction name. Used for the two main-worker entries and the entrypoint —
// asserting the name keeps each expectation disjoint under unordered matching.
function expectPlainTransaction(name: string) {
return (transactionEvent: TransactionEvent): void => {
expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare');
expect(transactionEvent.transaction).toBe(name);
expect(transactionEvent.spans).toHaveLength(0);
};
}
import { getSpanOp } from '../../../spanUtils';

// A single worker exports a plain `WorkerEntrypoint`, a plain `DurableObject`,
// and a plain default handler. The runner builds it with the Sentry Vite plugin
// (auto-instrumentation on) and serves the output — so every transaction below
// only arrives if the build-time transform wrapped all three: `withSentry` for
// the default export, the self-bound `GreeterEntrypoint`, and `Counter` via
// (auto-instrumentation on) and serves the output — so every span below only
// arrives if the build-time transform wrapped all three: `withSentry` for the
// default export, the self-bound `GreeterEntrypoint`, and `Counter` via
// `instrumentDurableObjectWithSentry`.
//
// Every route here is a raw URL, so the streamed segment names keep the method
// only and each hop is identified by its `url.path` attribute.
it('auto-instruments a WorkerEntrypoint and a Durable Object exported from the same worker', async ({ signal }) => {
const runner = createRunner(__dirname)
.unordered()
.expect(envelope => expectPlainTransaction('GET /call-entrypoint')(envelope[1]?.[0]?.[1] as TransactionEvent))
.expect(envelope => expectPlainTransaction('GET /greet')(envelope[1]?.[0]?.[1] as TransactionEvent))
.expect(envelope => expectPlainTransaction('GET /increment')(envelope[1]?.[0]?.[1] as TransactionEvent))
.expect(envelope => expectDurableObjectTransaction(envelope[1]?.[0]?.[1] as TransactionEvent))
.start(signal);
const runner = createRunner(__dirname).start(signal);

// Each hop streams from its own isolate, so the segment spans of a trace arrive in separate
// envelopes.
const entrypointSpansPromise = runner.collectStreamedSpans(spansOfTrace => {
const paths = spansOfTrace.filter(span => span.is_segment).map(span => span.attributes['url.path']?.value);
return paths.includes('/call-entrypoint') && paths.includes('/greet');
});

await runner.makeRequest('get', '/call-entrypoint');

const entrypointTraceSpans = await entrypointSpansPromise;
const callEntrypointSpan = entrypointTraceSpans.find(
span => span.is_segment && span.attributes['url.path']?.value === '/call-entrypoint',
);
const greetSpan = entrypointTraceSpans.find(
span => span.is_segment && span.attributes['url.path']?.value === '/greet',
);

expect(getSpanOp(callEntrypointSpan!)).toBe('http.server');
expect(callEntrypointSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' });

expect(getSpanOp(greetSpan!)).toBe('http.server');
expect(greetSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' });
expect(greetSpan?.parent_span_id).toBe(callEntrypointSpan?.span_id);

const durableObjectSpansPromise = runner.collectStreamedSpans(
spansOfTrace =>
spansOfTrace.filter(span => span.is_segment).length === 2 &&
spansOfTrace.some(span => span.attributes['url.path']?.value === '/increment'),
);

await runner.makeRequest('get', '/increment');
await runner.completed();

const durableObjectTraceSpans = await durableObjectSpansPromise;
const workerSpan = durableObjectTraceSpans.find(span => span.is_segment && !span.parent_span_id);
const durableObjectSpan = durableObjectTraceSpans.find(span => span.is_segment && span.parent_span_id);

expect(getSpanOp(workerSpan!)).toBe('http.server');
expect(getSpanOp(durableObjectSpan!)).toBe('http.server');
expect(durableObjectSpan?.parent_span_id).toBe(workerSpan?.span_id);

// The `auto.db.cloudflare.durable_object` storage pair (`get` + `put`) is the fingerprint of an
// instrumented Durable Object.
expect(
durableObjectTraceSpans
.filter(span => span.parent_span_id === durableObjectSpan?.span_id)
.map(span => ({ name: span.name, op: getSpanOp(span), origin: span.attributes['sentry.origin']?.value })),
).toEqual([
{ name: 'durable_object_storage_get', op: 'db', origin: 'auto.db.cloudflare.durable_object' },
{ name: 'durable_object_storage_put', op: 'db', origin: 'auto.db.cloudflare.durable_object' },
]);
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare';

export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
}));
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import type { TransactionEvent } from '@sentry/core';
import { expect, it } from 'vitest';
import { createRunner } from '../../../runner';
import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils';

// The worker entry is a plain, unwrapped `export default {...}`. The runner
// detects `vite.config.mts`, runs `vite build`, and serves the generated output
// — so this transaction only arrives if the build-time transform wrapped the
// default export with `withSentry`.
// — so this span only arrives if the build-time transform wrapped the default
// export with `withSentry`.
it('auto-instruments a plain default-export handler', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent;
expect(transactionEvent.transaction).toBe('GET /hello');
expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare');
const segmentSpan = getSpansFromEnvelope(envelope).find(span => span.is_segment);

// `/hello` is a raw URL, so the streamed segment name keeps the method only.
expect(segmentSpan?.name).toBe('GET');
expect(segmentSpan?.attributes['url.path']).toEqual({ type: 'string', value: '/hello' });
expect(getSpanOp(segmentSpan!)).toBe('http.server');
expect(segmentSpan?.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.cloudflare' });
})
.start(signal);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class CounterImpl extends DurableObject<Env> {
// leave it untouched — no second wrap — while still wrapping the plain default
// export below.
export const Counter = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({ dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0 }),
(env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0 }),
CounterImpl,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare';

export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({
dsn: env.SENTRY_DSN,
traceLifecycle: 'static',
tracesSampleRate: 1.0,
}));
Loading
Loading