Description
Package + Version
@sentry/cloudflare@10.42.0
wrangler@4.71.0
- Tested with both raw
DurableObject and agents framework (Agent base class)
Description
When a Durable Object instrumented with instrumentDurableObjectWithSentry creates child spans (via Sentry.startSpan) during request handling, the transaction is delivered to Sentry but only the root transaction span is indexed. The child spans embedded in the transaction's spans array are silently dropped.
This was confirmed using a beforeSendTransaction hook which shows the correct span count (e.g., 51 spans) before the transaction is sent. The transaction arrives in Sentry (visible in the traces explorer), but only the root span appears — the 50 child spans are missing.
Evidence
With the agents framework (Agent base class):
beforeSendTransaction fires: tx=GET /agents/stream-with-spans spans=51
- Sentry shows: 1 span in trace (just the root
http.server span)
With a plain DurableObject:
- Each
Sentry.startSpan call creates its own transaction (not a child span)
beforeSendTransaction fires 50 times, each with spans=0
- Sentry shows: 50+ spans in trace (because each is a separate transaction/root span)
This suggests that child spans embedded inside a transaction envelope are not being extracted/indexed by Sentry's span ingestion pipeline, at least for transactions originating from @sentry/cloudflare Durable Objects.
Impact
This blocks AI monitoring for Cloudflare DO-based applications. The Vercel AI SDK integration (vercelAIIntegration) correctly enriches spans with gen_ai.* attributes, and beforeSendTransaction confirms the spans are present in the transaction. But since child spans aren't indexed, they never appear in Sentry's traces explorer or AI monitoring views.
Minimal Reproduction
Files
wrangler.toml:
name = "sentry-do-streaming-repro"
main = "worker.ts"
compatibility_date = "2026-03-01"
compatibility_flags = ["nodejs_compat"]
[durable_objects]
bindings = [
{ name = "PLAIN_DO", class_name = "PlainDO" },
{ name = "AGENT_DO", class_name = "AgentDO" }
]
[[migrations]]
tag = "v1"
new_sqlite_classes = ["PlainDO", "AgentDO"]
worker.ts:
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';
import { Agent, getAgentByName } from 'agents';
function makeStream(): ReadableStream {
return new ReadableStream({
start(controller) {
let i = 0;
const interval = setInterval(() => {
controller.enqueue(new TextEncoder().encode(`chunk ${i}\n`));
i++;
if (i >= 5) {
clearInterval(interval);
controller.close();
}
}, 200);
},
});
}
function handlePath(path: string): Response {
if (path.endsWith('/no-stream')) {
return new Response('ok');
}
if (path.endsWith('/stream-with-spans')) {
// Create 50 child spans — these should appear in the trace but don't
for (let i = 0; i < 50; i++) {
Sentry.startSpan({ name: `work-${i}`, op: 'db' }, () => {});
}
return new Response(makeStream(), {
headers: { 'content-type': 'text/event-stream; charset=utf-8' },
});
}
return new Response('not found', { status: 404 });
}
// ─── Plain DurableObject ───
class PlainDOBase extends DurableObject<Env> {
async fetch(request: Request): Promise<Response> {
return handlePath(new URL(request.url).pathname);
}
}
export const PlainDO = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN || 'https://example@sentry.io/0',
tracesSampleRate: 1.0,
enabled: !!env.SENTRY_DSN,
beforeSendTransaction(event: any) {
console.log(`[plain] tx=${event.transaction} spans=${(event.spans ?? []).length}`);
return event;
},
}),
PlainDOBase,
);
// ─── Agent-based DO ───
class AgentDOBase extends Agent<Env> {
async onRequest(request: Request): Promise<Response> {
return handlePath(new URL(request.url).pathname);
}
}
export const AgentDO = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN || 'https://example@sentry.io/0',
tracesSampleRate: 1.0,
enabled: !!env.SENTRY_DSN,
beforeSendTransaction(event: any) {
console.log(`[agent] tx=${event.transaction} spans=${(event.spans ?? []).length}`);
return event;
},
}),
AgentDOBase,
);
// ─── Worker ───
interface Env {
SENTRY_DSN: string;
PLAIN_DO: DurableObjectNamespace;
AGENT_DO: DurableObjectNamespace;
}
const worker = {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname.startsWith('/agents/')) {
const agent = await getAgentByName(env.AGENT_DO, 'test');
return agent.fetch(request);
}
const id = env.PLAIN_DO.idFromName('test');
const stub = env.PLAIN_DO.get(id);
return stub.fetch(request);
},
};
export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN || 'https://example@sentry.io/0',
tracesSampleRate: 1.0,
enabled: !!env.SENTRY_DSN,
}),
worker,
);
Steps to Reproduce
npm init -y && npm install @sentry/cloudflare agents
- Deploy:
npx wrangler deploy
- Set secret:
npx wrangler secret put SENTRY_DSN
- Monitor logs:
npx wrangler tail
Test the agents framework path:
curl https://<your-url>/agents/stream-with-spans
Observe in wrangler tail:
[agent] tx=GET /agents/stream-with-spans spans=51
→ 51 spans in beforeSendTransaction
Check Sentry traces explorer:
→ Transaction appears with 1 span (root only). The 50 child work-* spans are missing.
Compare with plain DO path:
curl https://<your-url>/stream-with-spans
Observe in wrangler tail:
[plain] tx=work-0 spans=0
[plain] tx=work-1 spans=0
... (50 separate transactions)
→ Each span becomes its own transaction (not a child span). All 50 appear in Sentry individually.
Expected Behavior
The 50 child spans created inside Sentry.startSpan should appear as children of the request transaction in Sentry's traces explorer, the same as they would in a Node.js application using @sentry/node.
Actual Behavior
- The transaction is delivered to Sentry (root span visible)
beforeSendTransaction confirms 51 spans are in the transaction envelope
- Only the root span is indexed; 50 child spans are silently dropped
Additional Context
In a plain DurableObject (not using the agents framework), Sentry.startSpan creates separate transactions instead of child spans. This suggests instrumentDurableObjectWithSentry may not be correctly establishing a parent span context for the DO's fetch handler, so each startSpan call becomes a root span/transaction.
The agents framework DOES correctly parent spans (the beforeSendTransaction shows 51 spans under one transaction), but the child spans aren't indexed by Sentry after delivery.
Related Issues
- #15342 — Original issue about DO support (led to
instrumentDurableObjectWithSentry)
- #15975 — WebSocket DO events not captured
- JS-889 —
waitUntil spans/errors lost in Workers
Description
Package + Version
@sentry/cloudflare@10.42.0wrangler@4.71.0DurableObjectandagentsframework (Agentbase class)Description
When a Durable Object instrumented with
instrumentDurableObjectWithSentrycreates child spans (viaSentry.startSpan) during request handling, the transaction is delivered to Sentry but only the root transaction span is indexed. The child spans embedded in the transaction'sspansarray are silently dropped.This was confirmed using a
beforeSendTransactionhook which shows the correct span count (e.g., 51 spans) before the transaction is sent. The transaction arrives in Sentry (visible in the traces explorer), but only the root span appears — the 50 child spans are missing.Evidence
With the
agentsframework (Agentbase class):beforeSendTransactionfires:tx=GET /agents/stream-with-spans spans=51http.serverspan)With a plain
DurableObject:Sentry.startSpancall creates its own transaction (not a child span)beforeSendTransactionfires 50 times, each withspans=0This suggests that child spans embedded inside a transaction envelope are not being extracted/indexed by Sentry's span ingestion pipeline, at least for transactions originating from
@sentry/cloudflareDurable Objects.Impact
This blocks AI monitoring for Cloudflare DO-based applications. The Vercel AI SDK integration (
vercelAIIntegration) correctly enriches spans withgen_ai.*attributes, andbeforeSendTransactionconfirms the spans are present in the transaction. But since child spans aren't indexed, they never appear in Sentry's traces explorer or AI monitoring views.Minimal Reproduction
Files
wrangler.toml:
worker.ts:
Steps to Reproduce
npm init -y && npm install @sentry/cloudflare agentsnpx wrangler deploynpx wrangler secret put SENTRY_DSNnpx wrangler tailTest the agents framework path:
Observe in wrangler tail:
→ 51 spans in
beforeSendTransactionCheck Sentry traces explorer:
→ Transaction appears with 1 span (root only). The 50 child
work-*spans are missing.Compare with plain DO path:
Observe in wrangler tail:
→ Each span becomes its own transaction (not a child span). All 50 appear in Sentry individually.
Expected Behavior
The 50 child spans created inside
Sentry.startSpanshould appear as children of the request transaction in Sentry's traces explorer, the same as they would in a Node.js application using@sentry/node.Actual Behavior
beforeSendTransactionconfirms 51 spans are in the transaction envelopeAdditional Context
In a plain
DurableObject(not using theagentsframework),Sentry.startSpancreates separate transactions instead of child spans. This suggestsinstrumentDurableObjectWithSentrymay not be correctly establishing a parent span context for the DO'sfetchhandler, so eachstartSpancall becomes a root span/transaction.The
agentsframework DOES correctly parent spans (thebeforeSendTransactionshows 51 spans under one transaction), but the child spans aren't indexed by Sentry after delivery.Related Issues
instrumentDurableObjectWithSentry)waitUntilspans/errors lost in Workers