Skip to content
Open
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
62 changes: 62 additions & 0 deletions sentry-javascript/19815/README.md
Original file line number Diff line number Diff line change
@@ -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://<key>@sentry.io/<project>
```

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
36 changes: 36 additions & 0 deletions sentry-javascript/19815/app.js
Original file line number Diff line number Diff line change
@@ -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);
17 changes: 17 additions & 0 deletions sentry-javascript/19815/instrument.js
Original file line number Diff line number Diff line change
@@ -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;
},
});
13 changes: 13 additions & 0 deletions sentry-javascript/19815/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
26 changes: 26 additions & 0 deletions sentry-javascript/19815/test.sh
Original file line number Diff line number Diff line change
@@ -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."
2 changes: 2 additions & 0 deletions sentry-javascript/19883/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.wrangler
56 changes: 56 additions & 0 deletions sentry-javascript/19883/README.md
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions sentry-javascript/19883/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
70 changes: 70 additions & 0 deletions sentry-javascript/19883/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<Env, MyParams> {
async run(event: WorkflowEvent<MyParams>, 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<Response> {
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=<id> — check workflow status\n",
{ headers: { "Content-Type": "text/plain" } },
);
},
};
12 changes: 12 additions & 0 deletions sentry-javascript/19883/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
9 changes: 9 additions & 0 deletions sentry-javascript/19883/wrangler.toml
Original file line number Diff line number Diff line change
@@ -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"
Loading