From d7a4aca18b380bb379cb64bbfaeddda502d477f1 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 16 Mar 2026 12:48:58 +0100 Subject: [PATCH 1/2] Add reproduction for sentry-javascript#19815 Demonstrates that when tracing is disabled (no tracesSampleRate), all errors from different Express requests share the same traceId instead of each getting a unique one. Made-with: Cursor --- sentry-javascript/19815/README.md | 62 +++++++++++++++++++++++++++ sentry-javascript/19815/app.js | 36 ++++++++++++++++ sentry-javascript/19815/instrument.js | 17 ++++++++ sentry-javascript/19815/package.json | 13 ++++++ sentry-javascript/19815/test.sh | 26 +++++++++++ 5 files changed, 154 insertions(+) create mode 100644 sentry-javascript/19815/README.md create mode 100644 sentry-javascript/19815/app.js create mode 100644 sentry-javascript/19815/instrument.js create mode 100644 sentry-javascript/19815/package.json create mode 100755 sentry-javascript/19815/test.sh diff --git a/sentry-javascript/19815/README.md b/sentry-javascript/19815/README.md new file mode 100644 index 0000000..4271d52 --- /dev/null +++ b/sentry-javascript/19815/README.md @@ -0,0 +1,62 @@ +# Reproduction for sentry-javascript#19815 + +**Issue:** https://github.com/getsentry/sentry-javascript/issues/19815 + +## Description + +When tracing is disabled (no `tracesSampleRate` set), all errors from different +requests are assigned the **same** `traceId`. Without an active span per request, +the SDK never creates a fresh propagation context for each incoming request, so +every event ends up on the same static trace. + +## Steps to Reproduce + +1. Install dependencies: + ```bash + npm install + ``` + +2. (Optional) Set your Sentry DSN to also see events in Sentry UI: + ```bash + export SENTRY_DSN=https://@sentry.io/ + ``` + +3. Run the automated test (starts the server, fires 5 requests, then exits): + ```bash + bash test.sh + ``` + + Or run the server manually and send requests yourself: + ```bash + npm start + # in another terminal: + curl http://localhost:3000 + curl http://localhost:3000 + curl http://localhost:3000 + ``` + +## Expected Behavior + +Each request produces a **unique** `traceId`, so unrelated errors are not +grouped into the same trace. + +## Actual Behavior + +All requests share the same `traceId`. Example output: + +``` +[request #1] propagation traceId: e551c9b4398346c88486608a44c0a2a2 +[request #2] propagation traceId: e551c9b4398346c88486608a44c0a2a2 +[request #3] propagation traceId: e551c9b4398346c88486608a44c0a2a2 +[request #4] propagation traceId: e551c9b4398346c88486608a44c0a2a2 +[request #5] propagation traceId: e551c9b4398346c88486608a44c0a2a2 +``` + +This creates a false impression in Sentry that unrelated errors from different +requests belong to the same execution flow. + +## Environment + +- Node.js: v22.15.0 +- @sentry/node: 10.43.0 +- express: ^5.2.1 diff --git a/sentry-javascript/19815/app.js b/sentry-javascript/19815/app.js new file mode 100644 index 0000000..965a8d3 --- /dev/null +++ b/sentry-javascript/19815/app.js @@ -0,0 +1,36 @@ +import * as Sentry from '@sentry/node'; +import express from 'express'; + +const app = express(); + +let requestCount = 0; + +app.get('/', async (_req, res) => { + requestCount++; + const reqNum = requestCount; + + // Capture the current trace context before captureException + const scope = Sentry.getCurrentScope(); + const propagationContext = scope.getPropagationContext(); + console.log(`[request #${reqNum}] propagation traceId: ${propagationContext.traceId}`); + + Sentry.captureException(new Error(`Test Error from request #${reqNum}`)); + + res.end(`ok (request #${reqNum})\n`); +}); + +const server = app.listen(3000, () => { + console.log('App listening on port 3000'); + console.log(''); + console.log('Bug: All requests share the same traceId when tracing is disabled.'); + console.log('Each request should produce a different traceId.'); + console.log(''); + console.log('Run: curl http://localhost:3000 several times and observe the trace_id in the output.'); + console.log(''); +}); + +// Auto-shutdown after 30 seconds to simplify testing +setTimeout(() => { + server.close(); + process.exit(0); +}, 30_000); diff --git a/sentry-javascript/19815/instrument.js b/sentry-javascript/19815/instrument.js new file mode 100644 index 0000000..d08f80d --- /dev/null +++ b/sentry-javascript/19815/instrument.js @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + // Set your DSN here: export SENTRY_DSN=https://... + dsn: process.env.SENTRY_DSN, + // Tracing is intentionally disabled (no tracesSampleRate) + beforeSend(event) { + const traceId = event.contexts?.trace?.trace_id; + console.log(`[beforeSend] event type: error | trace_id: ${traceId}`); + return event; + }, + beforeSendTransaction(event) { + const traceId = event.contexts?.trace?.trace_id; + console.log(`[beforeSend] event type: transaction | trace_id: ${traceId}`); + return event; + }, +}); diff --git a/sentry-javascript/19815/package.json b/sentry-javascript/19815/package.json new file mode 100644 index 0000000..91f154c --- /dev/null +++ b/sentry-javascript/19815/package.json @@ -0,0 +1,13 @@ +{ + "name": "sentry-repro-19815", + "version": "1.0.0", + "description": "Reproduction for sentry-javascript#19815: All errors assigned to same trace when tracing is disabled", + "type": "module", + "scripts": { + "start": "node --import ./instrument.js app.js" + }, + "dependencies": { + "@sentry/node": "10.43.0", + "express": "^5.2.1" + } +} diff --git a/sentry-javascript/19815/test.sh b/sentry-javascript/19815/test.sh new file mode 100755 index 0000000..db555b4 --- /dev/null +++ b/sentry-javascript/19815/test.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Automated test: starts the server, fires 5 requests, and prints the trace IDs. +# All trace IDs should be the same (the bug), but each should be unique (the fix). + +set -e + +echo "Starting server..." +node --import ./instrument.js app.js & +SERVER_PID=$! + +# Wait for server to be ready +sleep 2 + +echo "" +echo "Sending 5 requests..." +for i in 1 2 3 4 5; do + curl -s http://localhost:3000 > /dev/null + sleep 0.2 +done + +echo "" +echo "Waiting for events to flush..." +sleep 1 + +kill $SERVER_PID 2>/dev/null || true +echo "Done." From 7386aedac0fb5fff6e59564c75991eff36bd5279 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 19 Mar 2026 18:27:32 +0100 Subject: [PATCH 2/2] Add reproduction for sentry-javascript#19883 Made-with: Cursor --- sentry-javascript/19883/.gitignore | 2 + sentry-javascript/19883/README.md | 56 +++++++++++++++++++++ sentry-javascript/19883/package.json | 17 +++++++ sentry-javascript/19883/src/index.ts | 70 +++++++++++++++++++++++++++ sentry-javascript/19883/tsconfig.json | 12 +++++ sentry-javascript/19883/wrangler.toml | 9 ++++ 6 files changed, 166 insertions(+) create mode 100644 sentry-javascript/19883/.gitignore create mode 100644 sentry-javascript/19883/README.md create mode 100644 sentry-javascript/19883/package.json create mode 100644 sentry-javascript/19883/src/index.ts create mode 100644 sentry-javascript/19883/tsconfig.json create mode 100644 sentry-javascript/19883/wrangler.toml diff --git a/sentry-javascript/19883/.gitignore b/sentry-javascript/19883/.gitignore new file mode 100644 index 0000000..1048609 --- /dev/null +++ b/sentry-javascript/19883/.gitignore @@ -0,0 +1,2 @@ +node_modules +.wrangler diff --git a/sentry-javascript/19883/README.md b/sentry-javascript/19883/README.md new file mode 100644 index 0000000..b47865b --- /dev/null +++ b/sentry-javascript/19883/README.md @@ -0,0 +1,56 @@ +# Reproduction for sentry-javascript#19883 + +**Issue:** https://github.com/getsentry/sentry-javascript/issues/19883 + +## Description + +`instrumentWorkflowWithSentry` from `@sentry/cloudflare` swallows the +`WorkflowStepContext` (`ctx`) parameter in `step.do` callbacks. This means +`ctx.attempt` (and any future properties on the context) is `undefined` when +Sentry instrumentation is active. + +## Steps to Reproduce + +1. Install dependencies: + ```bash + npm install + ``` + +2. Start the worker: + ```bash + npm run dev + ``` + +3. Trigger the workflow: + ```bash + curl http://localhost:8787/run + ``` + +4. Observe the wrangler console output. + +## Expected Behavior + +``` +ctx: {"attempt":1} +ctx?.attempt: 1 +OK: ctx.attempt = 1 +``` + +## Actual Behavior + +``` +ctx: undefined +ctx?.attempt: undefined +BUG: ctx is undefined — Sentry wrapper swallowed it! +``` + +The JSON response also shows no `attempt` field: +```json +{"id":"...","details":{"status":"complete","output":{"message":"hello from repro"}}} +``` + +## Environment + +- `@sentry/cloudflare`: 10.45.0 +- wrangler: 4.75.0 +- `compatibility_date`: 2025-12-18 diff --git a/sentry-javascript/19883/package.json b/sentry-javascript/19883/package.json new file mode 100644 index 0000000..985d2ce --- /dev/null +++ b/sentry-javascript/19883/package.json @@ -0,0 +1,17 @@ +{ + "name": "sentry-javascript-19883-repro", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "wrangler dev", + "start": "wrangler dev" + }, + "dependencies": { + "@sentry/cloudflare": "10.45.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250312.0", + "typescript": "^5.7.0", + "wrangler": "^4.75.0" + } +} diff --git a/sentry-javascript/19883/src/index.ts b/sentry-javascript/19883/src/index.ts new file mode 100644 index 0000000..7d9f25e --- /dev/null +++ b/sentry-javascript/19883/src/index.ts @@ -0,0 +1,70 @@ +import * as Sentry from "@sentry/cloudflare"; +import { WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from "cloudflare:workers"; + +interface Env { + MY_WORKFLOW: Workflow; + SENTRY_DSN: string; +} + +interface MyParams { + message: string; +} + +// Workflow class — accesses ctx.attempt in step.do callback +export class MyWorkflow extends WorkflowEntrypoint { + async run(event: WorkflowEvent, step: WorkflowStep) { + const result = await step.do("my step", async (ctx) => { + // ctx.attempt should be a number (1 on first try) per: + // https://developers.cloudflare.com/changelog/post/2026-03-06-step-context-available/ + console.log("ctx:", JSON.stringify(ctx)); + console.log("ctx?.attempt:", ctx?.attempt); + + if (ctx === undefined) { + console.log("BUG: ctx is undefined — Sentry wrapper swallowed it!"); + } else { + console.log("OK: ctx.attempt =", ctx.attempt); + } + + return { attempt: ctx?.attempt, message: event.payload.message }; + }); + + return result; + } +} + +// Wrap with Sentry instrumentation — this is where ctx gets lost +export const InstrumentedWorkflow = Sentry.instrumentWorkflowWithSentry( + (env: Env) => ({ + dsn: env.SENTRY_DSN || "", + }), + MyWorkflow, +); + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + if (url.pathname === "/run") { + const instance = await env.MY_WORKFLOW.create({ + params: { message: "hello from repro" }, + }); + return Response.json({ + id: instance.id, + details: await instance.status(), + }); + } + + if (url.pathname === "/status") { + const id = url.searchParams.get("id"); + if (!id) return new Response("Missing ?id=", { status: 400 }); + const instance = await env.MY_WORKFLOW.get(id); + return Response.json(await instance.status()); + } + + return new Response( + "GET /run — start a workflow instance\n" + + "GET /status?id= — check workflow status\n", + { headers: { "Content-Type": "text/plain" } }, + ); + }, +}; diff --git a/sentry-javascript/19883/tsconfig.json b/sentry-javascript/19883/tsconfig.json new file mode 100644 index 0000000..61b2ce5 --- /dev/null +++ b/sentry-javascript/19883/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ESNext"], + "types": ["@cloudflare/workers-types/2023-07-01"], + "strict": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/sentry-javascript/19883/wrangler.toml b/sentry-javascript/19883/wrangler.toml new file mode 100644 index 0000000..306db2f --- /dev/null +++ b/sentry-javascript/19883/wrangler.toml @@ -0,0 +1,9 @@ +name = "sentry-19883-repro" +main = "src/index.ts" +compatibility_date = "2025-12-18" +compatibility_flags = ["nodejs_compat"] + +[[workflows]] +name = "my-workflow" +binding = "MY_WORKFLOW" +class_name = "InstrumentedWorkflow"