Skip to content

Commit 88a9c2d

Browse files
authored
Merge branch 'develop' into fix/vercel-keepalive-repeated-init
2 parents 2c481c7 + ca05d2d commit 88a9c2d

32 files changed

Lines changed: 1235 additions & 425 deletions

File tree

‎dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc-overlapping/instrument.server.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare';
22

33
export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({
44
dsn: env.SENTRY_DSN,
5-
traceLifecycle: 'static',
65
tracesSampleRate: 1.0,
76
}));
Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,35 @@
1-
import type { TransactionEvent } from '@sentry/core';
21
import { expect, it } from 'vitest';
32
import { createRunner } from '../../../../runner';
3+
import { getSpanOp } from '../../../../spanUtils';
44

55
it('propagates the worker trace into each of two overlapping Durable Object RPC calls', async ({ signal }) => {
6-
const doTraces: Record<string, TransactionEvent['contexts']> = {};
7-
let workerTraceId: string | undefined;
8-
let workerSpanId: string | undefined;
9-
10-
const runner = createRunner(__dirname)
11-
.unordered()
12-
.expect(envelope => {
13-
const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent;
14-
expect(transactionEvent.transaction).toBe('work');
15-
expect(transactionEvent.contexts?.trace?.op).toBe('rpc');
16-
expect(transactionEvent.contexts?.trace?.data?.['test.label']).toBe('a');
17-
doTraces.a = transactionEvent.contexts;
18-
})
19-
.expect(envelope => {
20-
const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent;
21-
expect(transactionEvent.transaction).toBe('work');
22-
expect(transactionEvent.contexts?.trace?.op).toBe('rpc');
23-
expect(transactionEvent.contexts?.trace?.data?.['test.label']).toBe('b');
24-
doTraces.b = transactionEvent.contexts;
25-
})
26-
.expect(envelope => {
27-
const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent;
28-
expect(transactionEvent.transaction).toBe('GET /overlapping');
29-
expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
30-
workerTraceId = transactionEvent.contexts?.trace?.trace_id;
31-
workerSpanId = transactionEvent.contexts?.trace?.span_id;
32-
})
33-
.start(signal);
6+
const runner = createRunner(__dirname).start(signal);
7+
8+
// The worker and the Durable Object stream from separate isolates, and both RPC segments come from
9+
// the same Durable Object, so they can share an envelope.
10+
const spansPromise = runner.collectStreamedSpans(
11+
spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 3,
12+
);
3413

3514
const response = await runner.makeRequest<string>('get', '/overlapping');
3615
expect(response).toBe('a,b');
3716

38-
await runner.completed();
17+
const segmentSpans = (await spansPromise).filter(span => span.is_segment);
18+
const workerSpan = segmentSpans.find(span => getSpanOp(span) === 'http.server');
19+
const rpcSpanA = segmentSpans.find(span => span.attributes['test.label']?.value === 'a');
20+
const rpcSpanB = segmentSpans.find(span => span.attributes['test.label']?.value === 'b');
21+
22+
// `/overlapping` is a raw URL, so the streamed segment name keeps the method only.
23+
expect(workerSpan?.name).toBe('GET');
24+
expect(workerSpan?.attributes['url.path']).toEqual({ type: 'string', value: '/overlapping' });
3925

40-
expect(workerTraceId).toMatch(/^[\da-f]{32}$/);
41-
expect(workerSpanId).toMatch(/^[\da-f]{16}$/);
26+
expect(rpcSpanA?.name).toBe('work');
27+
expect(getSpanOp(rpcSpanA!)).toBe('rpc');
28+
expect(rpcSpanA?.parent_span_id).toBe(workerSpan?.span_id);
4229

43-
expect(doTraces.a?.trace?.trace_id).toBe(workerTraceId);
44-
expect(doTraces.a?.trace?.parent_span_id).toBe(workerSpanId);
45-
expect(doTraces.b?.trace?.trace_id).toBe(workerTraceId);
46-
expect(doTraces.b?.trace?.parent_span_id).toBe(workerSpanId);
30+
expect(rpcSpanB?.name).toBe('work');
31+
expect(getSpanOp(rpcSpanB!)).toBe('rpc');
32+
expect(rpcSpanB?.parent_span_id).toBe(workerSpan?.span_id);
4733

48-
expect(doTraces.a?.trace?.span_id).not.toBe(doTraces.b?.trace?.span_id);
34+
expect(rpcSpanA?.span_id).not.toBe(rpcSpanB?.span_id);
4935
});

‎dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workerentrypoint-reexport-instrumented/greeter.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ class GreeterImpl extends WorkerEntrypoint<Env> {
1414
// Manually instrumented here, in a module separate from the worker entry, which
1515
// only imports and re-exports the wrapped class.
1616
export const GreeterEntrypoint = Sentry.withSentry(
17-
(env: Env) => ({ dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0 }),
17+
(env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0 }),
1818
GreeterImpl,
1919
);

‎dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workerentrypoint-reexport-instrumented/instrument.server.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare';
22

33
export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({
44
dsn: env.SENTRY_DSN,
5-
traceLifecycle: 'static',
65
tracesSampleRate: 1.0,
76
}));
Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,38 @@
1-
import type { TransactionEvent } from '@sentry/core';
21
import { expect, it } from 'vitest';
32
import { createRunner } from '../../../runner';
3+
import { getSpanOp } from '../../../spanUtils';
44

55
// `GreeterEntrypoint` is hand-wrapped in `./greeter` and only re-exported by
66
// the entry, so the transform's emitted `_INTERNAL_wrapUnlessInstrumented`
77
// guard must hand the manual wrap back instead of nesting a second wrapper.
8-
// Nested entrypoint wrappers each instrument `fetch`, which shows up as extra
9-
// spans on the entrypoint transaction, the strict shape below catches that.
8+
// A nested wrapper throws inside the entrypoint and fails the request. The
9+
// child-less assertion below catches a nested wrapper that runs without an
10+
// error but instruments `fetch` a second time.
1011
it('does not double-instrument an imported, already-wrapped WorkerEntrypoint', async ({ signal }) => {
11-
const runner = createRunner(__dirname)
12-
.unordered()
13-
.expect(envelope => {
14-
const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent;
15-
// The entrypoint's own transaction, child-less when wrapped exactly once.
16-
expect(transactionEvent.transaction).toBe('GET /greet');
17-
expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
18-
expect(transactionEvent.spans ?? []).toHaveLength(0);
19-
})
20-
.expect(envelope => {
21-
const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent;
22-
// The auto-wrapped default export's transaction.
23-
expect(transactionEvent.transaction).toBe('GET /call-entrypoint');
24-
expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
25-
})
26-
.start(signal);
12+
const runner = createRunner(__dirname).start(signal);
13+
14+
// The default handler and the entrypoint stream from separate isolates, so the two segment spans
15+
// of the trace arrive in separate envelopes.
16+
const spansPromise = runner.collectStreamedSpans(
17+
spansOfTrace => spansOfTrace.filter(span => span.is_segment).length === 2,
18+
);
2719

2820
await runner.makeRequest('get', '/call-entrypoint');
29-
await runner.completed();
21+
22+
const spans = await spansPromise;
23+
// Both routes are raw URLs, so the streamed segment names keep the method only and the route is
24+
// read from `url.path`.
25+
const workerSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/call-entrypoint');
26+
const entrypointSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/greet');
27+
28+
// The auto-wrapped default export's segment span.
29+
expect(workerSpan?.name).toBe('GET');
30+
expect(getSpanOp(workerSpan!)).toBe('http.server');
31+
32+
expect(entrypointSpan?.name).toBe('GET');
33+
expect(getSpanOp(entrypointSpan!)).toBe('http.server');
34+
expect(entrypointSpan?.parent_span_id).toBe(workerSpan?.span_id);
35+
// The entrypoint's spans stay buffered until its invocation flushes, so a child span would arrive
36+
// in the same envelope as the entrypoint's segment span.
37+
expect(spans.filter(span => span.parent_span_id === entrypointSpan?.span_id)).toEqual([]);
3038
});

‎dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow-reexport-instrumented/index.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ export default {
1515
async fetch(request: Request, env: Env): Promise<Response> {
1616
const url = new URL(request.url);
1717

18-
// Issued by the test after `/trigger` returned, its transaction is the
18+
// Issued by the test after `/trigger` returned, its segment span is the
1919
// sentinel proving every earlier envelope (including a duplicate step
20-
// transaction from an accidental double wrap) has been delivered.
20+
// span from an accidental double wrap) has been delivered.
2121
if (url.pathname === '/sentinel') {
2222
return new Response('ok');
2323
}
@@ -26,7 +26,7 @@ export default {
2626
const instance = await env.MY_WORKFLOW.create();
2727
// Respond only once the workflow finished, so every step envelope (including
2828
// a duplicate from an accidental double wrap) is sent before this request's
29-
// own transaction completes the test's expectations.
29+
// own segment span completes the test's expectations.
3030
for (let i = 0; i < 20; i++) {
3131
try {
3232
const s = await instance.status();

‎dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow-reexport-instrumented/instrument.server.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@ import { defineCloudflareOptions } from '@sentry/cloudflare';
22

33
export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({
44
dsn: env.SENTRY_DSN,
5-
traceLifecycle: 'static',
65
tracesSampleRate: 1.0,
76
}));
Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,54 @@
1-
import type { TransactionEvent } from '@sentry/core';
21
import { expect, it } from 'vitest';
32
import { createRunner } from '../../../runner';
3+
import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils';
44

55
// `MyWorkflow` is hand-wrapped in `./workflow` and only re-exported by the
66
// entry, so the transform's emitted `_INTERNAL_wrapUnlessInstrumented` guard
77
// must hand the manual wrap back instead of nesting a second wrapper. Nested
88
// workflow wrappers each run the step through their own client, producing TWO
9-
// identical `step-one` transactions, each individually well-formed, so the
10-
// real check is the count assertion at the end.
9+
// identical `step-one` spans, each individually well-formed, so the real check
10+
// is the count assertion at the end.
1111
//
1212
// Ordering is anchored by a sentinel rather than by waiting: `/trigger`
1313
// responds only after the workflow finished (every step envelope, including a
1414
// duplicate, is flushed before then), and `/sentinel` is requested after that,
15-
// so its transaction arrives a full request/response cycle behind any
16-
// duplicate. Once the sentinel envelope has been matched, everything sent
17-
// before it is known to have been delivered.
15+
// so its span arrives a full request/response cycle behind any duplicate. Once
16+
// the sentinel envelope has been matched, everything sent before it is known to
17+
// have been delivered.
1818
it('does not double-instrument an imported, already-wrapped Workflow', async ({ signal }) => {
1919
const runner = createRunner(__dirname)
2020
.unordered()
2121
.expect(envelope => {
22-
const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent;
23-
expect(transactionEvent.transaction).toBe('step-one');
24-
expect(transactionEvent.contexts?.trace?.op).toBe('function');
25-
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.faas.cloudflare.workflow');
22+
const stepSpan = getSpansFromEnvelope(envelope).find(span => span.is_segment && span.name === 'step-one');
23+
expect(getSpanOp(stepSpan!)).toBe('function');
24+
expect(stepSpan?.attributes['sentry.origin']).toEqual({
25+
type: 'string',
26+
value: 'auto.faas.cloudflare.workflow',
27+
});
2628
})
2729
.expect(envelope => {
28-
const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent;
29-
// The auto-wrapped default export's own transaction.
30-
expect(transactionEvent.transaction).toBe('GET /trigger');
31-
expect(transactionEvent.contexts?.trace?.op).toBe('http.server');
30+
// The auto-wrapped default export's own segment span. `/trigger` is a raw URL, so the streamed
31+
// segment name keeps the method only.
32+
const segmentSpan = getSpansFromEnvelope(envelope).find(span => span.is_segment);
33+
expect(segmentSpan?.name).toBe('GET');
34+
expect(getSpanOp(segmentSpan!)).toBe('http.server');
35+
expect(segmentSpan?.attributes['url.path']).toEqual({ type: 'string', value: '/trigger' });
3236
})
3337
// The sentinel is part of the expected set, so the runner keeps everything
3438
// alive (and keeps receiving envelopes) until it has arrived.
3539
.expect(envelope => {
36-
expect((envelope[1]?.[0]?.[1] as TransactionEvent).transaction).toBe('GET /sentinel');
40+
const segmentSpan = getSpansFromEnvelope(envelope).find(span => span.is_segment);
41+
expect(segmentSpan?.attributes['url.path']).toEqual({ type: 'string', value: '/sentinel' });
3742
})
3843
.start(signal);
3944

4045
await runner.makeRequest('get', '/trigger');
4146
await runner.makeRequest('get', '/sentinel');
4247
await runner.completed();
4348

44-
const stepTransactions = runner
49+
const stepSpans = runner
4550
.getReceivedEnvelopes()
46-
.filter(envelope => (envelope[1]?.[0]?.[1] as TransactionEvent | undefined)?.transaction === 'step-one');
47-
expect(stepTransactions).toHaveLength(1);
51+
.flatMap(envelope => getSpansFromEnvelope(envelope))
52+
.filter(span => span.name === 'step-one');
53+
expect(stepSpans).toHaveLength(1);
4854
});

‎dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow-reexport-instrumented/workflow.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ class MyWorkflowImpl extends WorkflowEntrypoint<Env> {
1515
// Manually instrumented here, in a module separate from the worker entry, which
1616
// only imports and re-exports the wrapped class.
1717
export const MyWorkflow = Sentry.instrumentWorkflowWithSentry(
18-
(env: Env) => ({ dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1.0 }),
18+
(env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0 }),
1919
MyWorkflowImpl,
2020
);

0 commit comments

Comments
 (0)