From d49a3104c35e6ce2c055ce9f371b2de3e0ac7d09 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 26 Jan 2026 13:19:56 +0100 Subject: [PATCH 1/3] Add reproduction for sentry-javascript#18962 This reproduction demonstrates that Sentry's instrumentation breaks OpenAI streaming. When Sentry is initialized, only the final response is received instead of incremental streaming chunks. Issue: https://github.com/getsentry/sentry-javascript/issues/18962 Co-Authored-By: Claude Sonnet 4.5 --- sentry-javascript/18962/.gitignore | 3 + sentry-javascript/18962/README.md | 83 ++++++++++++++++++++++++++ sentry-javascript/18962/index.js | 72 ++++++++++++++++++++++ sentry-javascript/18962/instrument.mjs | 26 ++++++++ sentry-javascript/18962/package.json | 19 ++++++ 5 files changed, 203 insertions(+) create mode 100644 sentry-javascript/18962/.gitignore create mode 100644 sentry-javascript/18962/README.md create mode 100644 sentry-javascript/18962/index.js create mode 100644 sentry-javascript/18962/instrument.mjs create mode 100644 sentry-javascript/18962/package.json diff --git a/sentry-javascript/18962/.gitignore b/sentry-javascript/18962/.gitignore new file mode 100644 index 0000000..1945131 --- /dev/null +++ b/sentry-javascript/18962/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +package-lock.json +.env diff --git a/sentry-javascript/18962/README.md b/sentry-javascript/18962/README.md new file mode 100644 index 0000000..cbf825e --- /dev/null +++ b/sentry-javascript/18962/README.md @@ -0,0 +1,83 @@ +# Reproduction for sentry-javascript#18962 + +**Issue:** [Sentry breaking OpenAI STREAMING](https://github.com/getsentry/sentry-javascript/issues/18962) + +## Description + +This reproduction demonstrates that Sentry's instrumentation breaks OpenAI streaming capabilities. When Sentry is initialized (via `--import ./instrument.mjs`), OpenAI streaming returns only the last full response instead of incremental chunks, breaking the real-time streaming behavior. + +The issue occurs even when: +- The OpenAI integration is explicitly filtered out +- `process.env.OTEL_NODE_DISABLED_INSTRUMENTATIONS = 'openai'` is set +- Various other workarounds are attempted + +The only reliable workaround is setting `defaultIntegrations: false`, but this requires manually re-enabling all needed integrations. + +## Prerequisites + +You need an OpenAI API key to run this reproduction: + +```bash +export OPENAI_API_KEY=sk-your-key-here +``` + +## Steps to Reproduce + +1. Install dependencies: + ```bash + npm install + ``` + +2. First, test **WITHOUT** Sentry (baseline - this should work correctly): + ```bash + npm run start:without-sentry + ``` + + **Expected behavior:** You should see incremental streaming with multiple chunks being received in real-time as the model generates the response. + +3. Now test **WITH** Sentry enabled: + ```bash + npm run start:with-sentry + ``` + +## Expected Behavior + +OpenAI streaming should work the same way with or without Sentry: +- Multiple chunks should be received incrementally +- Content should stream in real-time as it's generated +- The chunk count should be relatively high (10+ chunks for a typical response) + +## Actual Behavior (Bug) + +When Sentry is enabled via `--import ./instrument.mjs`: +- Streaming appears broken +- Only 1-2 chunks are received (typically just the final complete response) +- No real-time incremental updates +- The response arrives all at once instead of streaming + +## Environment + +- Node.js: v20+ (ESM modules) +- @sentry/node: ^8.0.0 +- openai: ^4.77.0 + +## Notes + +- The reproduction uses a simplified Node.js setup instead of full NestJS to isolate the issue +- The core problem is the same: Sentry's auto-instrumentation interferes with stream handling +- Even filtering out the OpenAI integration doesn't resolve the issue +- This suggests the problem may be in Sentry's underlying HTTP/fetch instrumentation affecting stream processing + +## Known Workaround + +Setting `defaultIntegrations: false` in Sentry initialization prevents the issue, but requires manually enabling all needed integrations: + +```javascript +Sentry.init({ + dsn: '...', + defaultIntegrations: false, + integrations: [ + // Manually add only the integrations you need + ], +}); +``` diff --git a/sentry-javascript/18962/index.js b/sentry-javascript/18962/index.js new file mode 100644 index 0000000..7ce1e2e --- /dev/null +++ b/sentry-javascript/18962/index.js @@ -0,0 +1,72 @@ +import OpenAI from 'openai'; + +// Initialize OpenAI client +// You need to set OPENAI_API_KEY environment variable +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY || 'dummy-key-for-demo', +}); + +async function testStreaming() { + console.log('\nšŸš€ Starting OpenAI streaming test...\n'); + + if (!process.env.OPENAI_API_KEY) { + console.error('āŒ Error: OPENAI_API_KEY environment variable is not set'); + console.log(' Please set it with: export OPENAI_API_KEY=sk-your-key-here'); + process.exit(1); + } + + try { + const stream = await openai.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [ + { + role: 'user', + content: 'Count from 1 to 10, with each number on a new line.', + }, + ], + stream: true, + }); + + console.log('šŸ“ Streaming response:'); + console.log('---'); + + let chunkCount = 0; + let fullResponse = ''; + + for await (const chunk of stream) { + chunkCount++; + const content = chunk.choices[0]?.delta?.content || ''; + if (content) { + process.stdout.write(content); + fullResponse += content; + } + } + + console.log('\n---'); + console.log(`\nāœ… Streaming completed successfully!`); + console.log(` Total chunks received: ${chunkCount}`); + console.log(` Full response length: ${fullResponse.length} characters`); + + // With Sentry enabled, you might see: + // - Very few chunks (maybe just 1-2) + // - Only the final complete message instead of incremental updates + // - No real-time streaming behavior + + if (chunkCount < 5) { + console.log('\nāš ļø WARNING: Low chunk count detected!'); + console.log(' This suggests streaming may not be working properly.'); + console.log(' Expected: Multiple chunks as the response is generated'); + console.log(' Actual: Only final response chunk(s)'); + } + + } catch (error) { + console.error('āŒ Error during streaming:', error.message); + throw error; + } +} + +// Run the test +testStreaming().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); +}); diff --git a/sentry-javascript/18962/instrument.mjs b/sentry-javascript/18962/instrument.mjs new file mode 100644 index 0000000..9d77819 --- /dev/null +++ b/sentry-javascript/18962/instrument.mjs @@ -0,0 +1,26 @@ +import * as Sentry from '@sentry/node'; + +// Only initialize if ENABLE_SENTRY is set +if (process.env.ENABLE_SENTRY === 'true') { + console.log('šŸ” Initializing Sentry...'); + + Sentry.init({ + dsn: process.env.SENTRY_DSN || '', // Empty DSN for testing + environment: 'local', + tracesSampleRate: 1.0, + + // Even with OpenAI integration filtered out, streaming still breaks + integrations: (integrations) => { + const filtered = integrations.filter((integration) => { + const name = integration?.name ?? ''; + const normalized = name.toLowerCase(); + // Try to filter out OpenAI integration (as mentioned in the issue) + return !normalized.includes('openai'); + }); + console.log('āœ… Sentry initialized with integrations:', filtered.map(i => i.name).join(', ')); + return filtered; + }, + }); +} else { + console.log('ā­ļø Sentry disabled - streaming should work normally'); +} diff --git a/sentry-javascript/18962/package.json b/sentry-javascript/18962/package.json new file mode 100644 index 0000000..116dbc2 --- /dev/null +++ b/sentry-javascript/18962/package.json @@ -0,0 +1,19 @@ +{ + "name": "sentry-openai-streaming-repro", + "version": "1.0.0", + "description": "Reproduction for sentry-javascript#18962 - Sentry breaking OpenAI streaming", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node index.js", + "start:with-sentry": "ENABLE_SENTRY=true node --import ./instrument.mjs index.js", + "start:without-sentry": "node index.js" + }, + "dependencies": { + "@sentry/node": "^8.0.0", + "openai": "^4.77.0" + }, + "devDependencies": { + "@types/node": "^22.0.0" + } +} From 34a6cab00d115f1344271e42b4d2623ac74b78f4 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 27 Jan 2026 10:57:12 +0100 Subject: [PATCH 2/3] proper reproduction --- sentry-javascript/18962/.gitignore | 1 + sentry-javascript/18962/README.md | 115 +++++---- sentry-javascript/18962/index.js | 72 ------ sentry-javascript/18962/instrument.mjs | 26 -- sentry-javascript/18962/nest-cli.json | 5 + sentry-javascript/18962/package.json | 30 ++- sentry-javascript/18962/src/app.module.ts | 9 + sentry-javascript/18962/src/instrument.ts | 23 ++ sentry-javascript/18962/src/main.ts | 15 ++ .../18962/src/stream.controller.ts | 243 ++++++++++++++++++ sentry-javascript/18962/tsconfig.json | 21 ++ 11 files changed, 408 insertions(+), 152 deletions(-) delete mode 100644 sentry-javascript/18962/index.js delete mode 100644 sentry-javascript/18962/instrument.mjs create mode 100644 sentry-javascript/18962/nest-cli.json create mode 100644 sentry-javascript/18962/src/app.module.ts create mode 100644 sentry-javascript/18962/src/instrument.ts create mode 100644 sentry-javascript/18962/src/main.ts create mode 100644 sentry-javascript/18962/src/stream.controller.ts create mode 100644 sentry-javascript/18962/tsconfig.json diff --git a/sentry-javascript/18962/.gitignore b/sentry-javascript/18962/.gitignore index 1945131..e44b27c 100644 --- a/sentry-javascript/18962/.gitignore +++ b/sentry-javascript/18962/.gitignore @@ -1,3 +1,4 @@ node_modules/ +dist/ package-lock.json .env diff --git a/sentry-javascript/18962/README.md b/sentry-javascript/18962/README.md index cbf825e..93a9328 100644 --- a/sentry-javascript/18962/README.md +++ b/sentry-javascript/18962/README.md @@ -1,25 +1,19 @@ # Reproduction for sentry-javascript#18962 -**Issue:** [Sentry breaking OpenAI STREAMING](https://github.com/getsentry/sentry-javascript/issues/18962) +**Issue:** https://github.com/getsentry/sentry-javascript/issues/18962 ## Description -This reproduction demonstrates that Sentry's instrumentation breaks OpenAI streaming capabilities. When Sentry is initialized (via `--import ./instrument.mjs`), OpenAI streaming returns only the last full response instead of incremental chunks, breaking the real-time streaming behavior. +This reproduction attempts to demonstrate the reported issue where OpenAI streaming breaks when Sentry is initialized in a NestJS application with `@sentry/nestjs`. -The issue occurs even when: -- The OpenAI integration is explicitly filtered out -- `process.env.OTEL_NODE_DISABLED_INSTRUMENTATIONS = 'openai'` is set -- Various other workarounds are attempted +**Note:** In our testing, streaming works correctly both with and without Sentry enabled. We need more details from the issue reporter to reproduce the exact issue. -The only reliable workaround is setting `defaultIntegrations: false`, but this requires manually re-enabling all needed integrations. +## Setup -## Prerequisites - -You need an OpenAI API key to run this reproduction: - -```bash -export OPENAI_API_KEY=sk-your-key-here -``` +This is a NestJS application that uses: +- `@sentry/nestjs` v10.x (with OpenAI integration) +- `openai` SDK for direct API calls +- `@langchain/openai` for LangChain integration ## Steps to Reproduce @@ -28,56 +22,89 @@ export OPENAI_API_KEY=sk-your-key-here npm install ``` -2. First, test **WITHOUT** Sentry (baseline - this should work correctly): +2. Build the TypeScript: ```bash - npm run start:without-sentry + npm run build + ``` + +3. Set your OpenAI API key: + ```bash + export OPENAI_API_KEY=sk-your-key-here + ``` + +4. Optionally set Sentry DSN: + ```bash + export SENTRY_DSN=https://your-dsn@sentry.io/project ``` - **Expected behavior:** You should see incremental streaming with multiple chunks being received in real-time as the model generates the response. +5. Test WITHOUT Sentry: + ```bash + npm run start:without-sentry + # In another terminal: + curl http://localhost:3000/stream + ``` -3. Now test **WITH** Sentry enabled: +6. Test WITH Sentry: ```bash npm run start:with-sentry + # In another terminal: + curl http://localhost:3000/stream ``` -## Expected Behavior +## Available Endpoints + +- `GET /` - Status and available endpoints +- `GET /stream` - Test OpenAI streaming with `for await` +- `GET /stream-langchain` - Test OpenAI streaming via LangChain +- `GET /stream-web` - Test OpenAI streaming with `toReadableStream()` +- `GET /stream-sse` - Test OpenAI streaming with Server-Sent Events -OpenAI streaming should work the same way with or without Sentry: -- Multiple chunks should be received incrementally -- Content should stream in real-time as it's generated -- The chunk count should be relatively high (10+ chunks for a typical response) +## Expected Behavior (per issue) -## Actual Behavior (Bug) +According to the issue, with Sentry enabled: +- Streaming should break +- Only the final complete response should come through (1-2 chunks) +- Real-time streaming behavior should not work -When Sentry is enabled via `--import ./instrument.mjs`: -- Streaming appears broken -- Only 1-2 chunks are received (typically just the final complete response) -- No real-time incremental updates -- The response arrives all at once instead of streaming +## Actual Behavior (our testing) + +In our testing with `@sentry/nestjs@10.36.0`: +- Streaming works correctly with Sentry enabled +- 40+ chunks received for all streaming methods +- No difference between Sentry enabled/disabled ## Environment -- Node.js: v20+ (ESM modules) -- @sentry/node: ^8.0.0 -- openai: ^4.77.0 +- Node.js: 22.x +- @sentry/nestjs: 10.36.0 +- @nestjs/core: 10.x +- openai: 4.x +- @langchain/openai: 1.x + +## Questions for Issue Reporter -## Notes +To help reproduce the issue, please provide: -- The reproduction uses a simplified Node.js setup instead of full NestJS to isolate the issue -- The core problem is the same: Sentry's auto-instrumentation interferes with stream handling -- Even filtering out the OpenAI integration doesn't resolve the issue -- This suggests the problem may be in Sentry's underlying HTTP/fetch instrumentation affecting stream processing +1. **Exact SDK version**: What specific version of `@sentry/nestjs` are you using? +2. **Node.js version**: What Node.js version? +3. **Sentry.init config**: Full Sentry initialization config +4. **NestJS setup**: Any interceptors, middleware, or guards that might affect responses? +5. **OpenAI usage**: Exact code showing how you're using OpenAI/LangChain +6. **Network config**: Any proxies or custom HTTP agents? -## Known Workaround +## Workaround (from issue) -Setting `defaultIntegrations: false` in Sentry initialization prevents the issue, but requires manually enabling all needed integrations: +The issue reporter found that `defaultIntegrations: false` helps, suggesting one of the default integrations causes the problem. Try narrowing down which integration: -```javascript +```typescript Sentry.init({ dsn: '...', - defaultIntegrations: false, - integrations: [ - // Manually add only the integrations you need - ], + // Try disabling specific integrations to find the culprit + integrations: (integrations) => { + return integrations.filter((integration) => { + // Try filtering different integrations + return integration.name !== 'OpenAI'; + }); + }, }); ``` diff --git a/sentry-javascript/18962/index.js b/sentry-javascript/18962/index.js deleted file mode 100644 index 7ce1e2e..0000000 --- a/sentry-javascript/18962/index.js +++ /dev/null @@ -1,72 +0,0 @@ -import OpenAI from 'openai'; - -// Initialize OpenAI client -// You need to set OPENAI_API_KEY environment variable -const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY || 'dummy-key-for-demo', -}); - -async function testStreaming() { - console.log('\nšŸš€ Starting OpenAI streaming test...\n'); - - if (!process.env.OPENAI_API_KEY) { - console.error('āŒ Error: OPENAI_API_KEY environment variable is not set'); - console.log(' Please set it with: export OPENAI_API_KEY=sk-your-key-here'); - process.exit(1); - } - - try { - const stream = await openai.chat.completions.create({ - model: 'gpt-4o-mini', - messages: [ - { - role: 'user', - content: 'Count from 1 to 10, with each number on a new line.', - }, - ], - stream: true, - }); - - console.log('šŸ“ Streaming response:'); - console.log('---'); - - let chunkCount = 0; - let fullResponse = ''; - - for await (const chunk of stream) { - chunkCount++; - const content = chunk.choices[0]?.delta?.content || ''; - if (content) { - process.stdout.write(content); - fullResponse += content; - } - } - - console.log('\n---'); - console.log(`\nāœ… Streaming completed successfully!`); - console.log(` Total chunks received: ${chunkCount}`); - console.log(` Full response length: ${fullResponse.length} characters`); - - // With Sentry enabled, you might see: - // - Very few chunks (maybe just 1-2) - // - Only the final complete message instead of incremental updates - // - No real-time streaming behavior - - if (chunkCount < 5) { - console.log('\nāš ļø WARNING: Low chunk count detected!'); - console.log(' This suggests streaming may not be working properly.'); - console.log(' Expected: Multiple chunks as the response is generated'); - console.log(' Actual: Only final response chunk(s)'); - } - - } catch (error) { - console.error('āŒ Error during streaming:', error.message); - throw error; - } -} - -// Run the test -testStreaming().catch((error) => { - console.error('Fatal error:', error); - process.exit(1); -}); diff --git a/sentry-javascript/18962/instrument.mjs b/sentry-javascript/18962/instrument.mjs deleted file mode 100644 index 9d77819..0000000 --- a/sentry-javascript/18962/instrument.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import * as Sentry from '@sentry/node'; - -// Only initialize if ENABLE_SENTRY is set -if (process.env.ENABLE_SENTRY === 'true') { - console.log('šŸ” Initializing Sentry...'); - - Sentry.init({ - dsn: process.env.SENTRY_DSN || '', // Empty DSN for testing - environment: 'local', - tracesSampleRate: 1.0, - - // Even with OpenAI integration filtered out, streaming still breaks - integrations: (integrations) => { - const filtered = integrations.filter((integration) => { - const name = integration?.name ?? ''; - const normalized = name.toLowerCase(); - // Try to filter out OpenAI integration (as mentioned in the issue) - return !normalized.includes('openai'); - }); - console.log('āœ… Sentry initialized with integrations:', filtered.map(i => i.name).join(', ')); - return filtered; - }, - }); -} else { - console.log('ā­ļø Sentry disabled - streaming should work normally'); -} diff --git a/sentry-javascript/18962/nest-cli.json b/sentry-javascript/18962/nest-cli.json new file mode 100644 index 0000000..2566481 --- /dev/null +++ b/sentry-javascript/18962/nest-cli.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src" +} diff --git a/sentry-javascript/18962/package.json b/sentry-javascript/18962/package.json index 116dbc2..1b0f16d 100644 --- a/sentry-javascript/18962/package.json +++ b/sentry-javascript/18962/package.json @@ -1,19 +1,29 @@ { - "name": "sentry-openai-streaming-repro", + "name": "sentry-nestjs-openai-streaming-repro", "version": "1.0.0", - "description": "Reproduction for sentry-javascript#18962 - Sentry breaking OpenAI streaming", - "main": "index.js", - "type": "module", + "description": "Reproduction for sentry-javascript#18962 - Sentry breaking OpenAI streaming in NestJS", + "main": "dist/main.js", "scripts": { - "start": "node index.js", - "start:with-sentry": "ENABLE_SENTRY=true node --import ./instrument.mjs index.js", - "start:without-sentry": "node index.js" + "build": "nest build", + "start": "nest start", + "start:dev": "nest start --watch", + "start:with-sentry": "ENABLE_SENTRY=true node --import ./instrument.mjs dist/main.js", + "start:without-sentry": "node dist/main.js" }, "dependencies": { - "@sentry/node": "^8.0.0", - "openai": "^4.77.0" + "@langchain/openai": "^1.2.3", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@sentry/nestjs": "^10.0.0", + "langchain": "^1.2.14", + "openai": "^4.77.0", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.1" }, "devDependencies": { - "@types/node": "^22.0.0" + "@nestjs/cli": "^10.0.0", + "@types/node": "^22.0.0", + "typescript": "^5.0.0" } } diff --git a/sentry-javascript/18962/src/app.module.ts b/sentry-javascript/18962/src/app.module.ts new file mode 100644 index 0000000..a394fdc --- /dev/null +++ b/sentry-javascript/18962/src/app.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { StreamController } from './stream.controller'; + +@Module({ + imports: [], + controllers: [StreamController], + providers: [], +}) +export class AppModule {} diff --git a/sentry-javascript/18962/src/instrument.ts b/sentry-javascript/18962/src/instrument.ts new file mode 100644 index 0000000..9e0b972 --- /dev/null +++ b/sentry-javascript/18962/src/instrument.ts @@ -0,0 +1,23 @@ +// Sentry instrumentation file for NestJS +// This is loaded BEFORE the application starts via --import flag +import * as Sentry from "@sentry/nestjs"; + +if (process.env.ENABLE_SENTRY === "true") { + console.log("šŸ” Initializing Sentry with @sentry/nestjs..."); + + Sentry.init({ + dsn: process.env.SENTRY_DSN || "", + environment: "local", + tracesSampleRate: 1.0, + debug: true, + sendDefaultPii: true, + integrations: [ + Sentry.openAIIntegration({ recordInputs: true, recordOutputs: true }), + Sentry.langChainIntegration({ recordInputs: true, recordOutputs: true }), + ], + }); + + console.log("āœ… Sentry initialized"); +} else { + console.log("ā­ļø Sentry disabled (ENABLE_SENTRY != true)"); +} diff --git a/sentry-javascript/18962/src/main.ts b/sentry-javascript/18962/src/main.ts new file mode 100644 index 0000000..3001baf --- /dev/null +++ b/sentry-javascript/18962/src/main.ts @@ -0,0 +1,15 @@ +// Import this first! +import "./instrument"; + +import { NestFactory } from "@nestjs/core"; +import { AppModule } from "./app.module"; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + const port = process.env.PORT || 3000; + await app.listen(port); + console.log(`\nšŸš€ NestJS server running at http://localhost:${port}`); + console.log(`\nšŸ“ Test streaming: curl http://localhost:${port}/stream`); + console.log(` Or open in browser: http://localhost:${port}/stream\n`); +} +bootstrap(); diff --git a/sentry-javascript/18962/src/stream.controller.ts b/sentry-javascript/18962/src/stream.controller.ts new file mode 100644 index 0000000..931e442 --- /dev/null +++ b/sentry-javascript/18962/src/stream.controller.ts @@ -0,0 +1,243 @@ +import { Controller, Get, Res } from '@nestjs/common'; +import { Response } from 'express'; +import OpenAI from 'openai'; +import { ChatOpenAI } from '@langchain/openai'; +import { HumanMessage } from '@langchain/core/messages'; + +@Controller() +export class StreamController { + private openai: OpenAI | null = null; + private langchainModel: ChatOpenAI | null = null; + + private getOpenAI(): OpenAI { + if (!this.openai) { + if (!process.env.OPENAI_API_KEY) { + throw new Error('OPENAI_API_KEY not set. Export it with: export OPENAI_API_KEY=sk-...'); + } + this.openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, + }); + } + return this.openai; + } + + private getLangchainModel(): ChatOpenAI { + if (!this.langchainModel) { + if (!process.env.OPENAI_API_KEY) { + throw new Error('OPENAI_API_KEY not set. Export it with: export OPENAI_API_KEY=sk-...'); + } + this.langchainModel = new ChatOpenAI({ + model: 'gpt-4o-mini', + temperature: 0, + streaming: true, + }); + } + return this.langchainModel; + } + + @Get('/') + index() { + return { + message: 'Sentry NestJS OpenAI Streaming Reproduction', + issue: 'https://github.com/getsentry/sentry-javascript/issues/18962', + endpoints: { + '/stream': 'Test OpenAI streaming with for-await (GET)', + '/stream-langchain': 'Test OpenAI streaming via Langchain (GET)', + '/stream-web': 'Test OpenAI streaming with toReadableStream() (GET)', + '/stream-sse': 'Test OpenAI streaming with SSE (GET)', + }, + sentryEnabled: process.env.ENABLE_SENTRY === 'true', + openaiKeySet: !!process.env.OPENAI_API_KEY, + }; + } + + @Get('/stream') + async stream(@Res() res: Response) { + try { + const openai = this.getOpenAI(); + + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.setHeader('Transfer-Encoding', 'chunked'); + + console.log('\nšŸ“ [OpenAI for-await] Starting streaming request...'); + const startTime = Date.now(); + let chunkCount = 0; + + const stream = await openai.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [ + { + role: 'user', + content: 'Count from 1 to 20, each number on a new line. Be slow and deliberate.', + }, + ], + stream: true, + }); + + for await (const chunk of stream) { + chunkCount++; + const content = chunk.choices[0]?.delta?.content || ''; + if (content) { + console.log(` Chunk ${chunkCount}: "${content.replace(/\n/g, '\\n')}"`); + res.write(content); + } + } + + const elapsed = Date.now() - startTime; + console.log(`\nāœ… [OpenAI for-await] Completed in ${elapsed}ms, chunks: ${chunkCount}`); + + if (chunkCount < 10) { + console.log('\nāš ļø WARNING: Very few chunks received - streaming may be broken!'); + } + + res.end( + `\n\n--- Stats ---\nChunks: ${chunkCount}\nTime: ${elapsed}ms\nSentry: ${process.env.ENABLE_SENTRY === 'true' ? 'ENABLED' : 'disabled'}\n`, + ); + } catch (error) { + console.error('āŒ Error:', error.message); + res.status(500).json({ error: error.message }); + } + } + + // Using OpenAI's toReadableStream() - Web Streams API + @Get('/stream-web') + async streamWeb(@Res() res: Response) { + try { + const openai = this.getOpenAI(); + + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.setHeader('Transfer-Encoding', 'chunked'); + + console.log('\nšŸ“ [OpenAI toReadableStream] Starting streaming request...'); + const startTime = Date.now(); + let chunkCount = 0; + + const stream = await openai.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [ + { + role: 'user', + content: 'Count from 1 to 20, each number on a new line.', + }, + ], + stream: true, + }); + + // Using the toReadableStream() method from OpenAI SDK + const webStream = stream.toReadableStream(); + const reader = webStream.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + chunkCount++; + const text = decoder.decode(value, { stream: true }); + console.log(` WebStream Chunk ${chunkCount}: "${text.substring(0, 50)}..."`); + res.write(text); + } + + const elapsed = Date.now() - startTime; + console.log(`\nāœ… [OpenAI toReadableStream] Completed in ${elapsed}ms, chunks: ${chunkCount}`); + + if (chunkCount < 10) { + console.log('\nāš ļø WARNING: Very few chunks received - streaming may be broken!'); + } + + res.end( + `\n\n--- Stats ---\nChunks: ${chunkCount}\nTime: ${elapsed}ms\nSentry: ${process.env.ENABLE_SENTRY === 'true' ? 'ENABLED' : 'disabled'}\nMethod: toReadableStream\n`, + ); + } catch (error) { + console.error('āŒ WebStream Error:', error.message); + res.status(500).json({ error: error.message }); + } + } + + @Get('/stream-langchain') + async streamLangchain(@Res() res: Response) { + try { + const model = this.getLangchainModel(); + + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.setHeader('Transfer-Encoding', 'chunked'); + + console.log('\nšŸ“ [Langchain] Starting streaming request...'); + const startTime = Date.now(); + let chunkCount = 0; + + // Using Langchain's streaming with .stream() + const stream = await model.stream([ + new HumanMessage('Count from 1 to 20, each number on a new line. Be slow and deliberate.'), + ]); + + for await (const chunk of stream) { + chunkCount++; + const content = chunk.content; + if (content) { + console.log(` Chunk ${chunkCount}: "${String(content).replace(/\n/g, '\\n')}"`); + res.write(String(content)); + } + } + + const elapsed = Date.now() - startTime; + console.log(`\nāœ… [Langchain] Completed in ${elapsed}ms, chunks: ${chunkCount}`); + + if (chunkCount < 10) { + console.log('\nāš ļø WARNING: Very few chunks received - streaming may be broken!'); + } + + res.end( + `\n\n--- Stats ---\nChunks: ${chunkCount}\nTime: ${elapsed}ms\nSentry: ${process.env.ENABLE_SENTRY === 'true' ? 'ENABLED' : 'disabled'}\nMethod: Langchain\n`, + ); + } catch (error) { + console.error('āŒ Langchain Error:', error.message); + res.status(500).json({ error: error.message }); + } + } + + @Get('/stream-sse') + async streamSSE(@Res() res: Response) { + try { + const openai = this.getOpenAI(); + + // Server-Sent Events format + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + console.log('\nšŸ“ [SSE] Starting streaming request...'); + const startTime = Date.now(); + let chunkCount = 0; + + const stream = await openai.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [ + { + role: 'user', + content: 'Count from 1 to 20, each number on a new line.', + }, + ], + stream: true, + }); + + for await (const chunk of stream) { + chunkCount++; + const content = chunk.choices[0]?.delta?.content || ''; + if (content) { + console.log(` SSE Chunk ${chunkCount}: "${content.replace(/\n/g, '\\n')}"`); + res.write(`data: ${JSON.stringify({ content, chunk: chunkCount })}\n\n`); + } + } + + const elapsed = Date.now() - startTime; + console.log(`\nāœ… [SSE] Completed in ${elapsed}ms, chunks: ${chunkCount}`); + + res.write(`data: ${JSON.stringify({ done: true, totalChunks: chunkCount, elapsed })}\n\n`); + res.end(); + } catch (error) { + console.error('āŒ SSE Error:', error.message); + res.status(500).json({ error: error.message }); + } + } +} diff --git a/sentry-javascript/18962/tsconfig.json b/sentry-javascript/18962/tsconfig.json new file mode 100644 index 0000000..95f5641 --- /dev/null +++ b/sentry-javascript/18962/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false + } +} From b618e1a75dfd4a1a1c5b6042fdb48e3b9e193694 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 27 Jan 2026 11:55:29 +0100 Subject: [PATCH 3/3] update repro once more, simplify --- sentry-javascript/18962/README.md | 36 ++- sentry-javascript/18962/package.json | 5 +- sentry-javascript/18962/src/instrument.ts | 4 - .../18962/src/stream.controller.ts | 219 +++++------------- 4 files changed, 76 insertions(+), 188 deletions(-) diff --git a/sentry-javascript/18962/README.md b/sentry-javascript/18962/README.md index 93a9328..89bd227 100644 --- a/sentry-javascript/18962/README.md +++ b/sentry-javascript/18962/README.md @@ -11,8 +11,7 @@ This reproduction attempts to demonstrate the reported issue where OpenAI stream ## Setup This is a NestJS application that uses: -- `@sentry/nestjs` v10.x (with OpenAI integration) -- `openai` SDK for direct API calls +- `@sentry/nestjs` v10.x (with OpenAI and LangChain integrations) - `@langchain/openai` for LangChain integration ## Steps to Reproduce @@ -22,42 +21,36 @@ This is a NestJS application that uses: npm install ``` -2. Build the TypeScript: +2. Create a `.env` file with your configuration: ```bash - npm run build + OPENAI_API_KEY=sk-your-key-here + SENTRY_DSN=https://your-dsn@sentry.io/project + ENABLE_SENTRY=true ``` -3. Set your OpenAI API key: - ```bash - export OPENAI_API_KEY=sk-your-key-here - ``` + Set `ENABLE_SENTRY=true` to enable Sentry, or omit/set to any other value to disable. -4. Optionally set Sentry DSN: +3. Build the TypeScript: ```bash - export SENTRY_DSN=https://your-dsn@sentry.io/project + npm run build ``` -5. Test WITHOUT Sentry: +4. Run the server: ```bash - npm run start:without-sentry - # In another terminal: - curl http://localhost:3000/stream + npm run start ``` -6. Test WITH Sentry: +5. Test streaming: ```bash - npm run start:with-sentry - # In another terminal: - curl http://localhost:3000/stream + curl http://localhost:3000/stream-langchain + curl http://localhost:3000/stream-openai ``` ## Available Endpoints - `GET /` - Status and available endpoints -- `GET /stream` - Test OpenAI streaming with `for await` +- `GET /stream-openai` - Test OpenAI streaming via LangChain's ChatOpenAI - `GET /stream-langchain` - Test OpenAI streaming via LangChain -- `GET /stream-web` - Test OpenAI streaming with `toReadableStream()` -- `GET /stream-sse` - Test OpenAI streaming with Server-Sent Events ## Expected Behavior (per issue) @@ -78,7 +71,6 @@ In our testing with `@sentry/nestjs@10.36.0`: - Node.js: 22.x - @sentry/nestjs: 10.36.0 - @nestjs/core: 10.x -- openai: 4.x - @langchain/openai: 1.x ## Questions for Issue Reporter diff --git a/sentry-javascript/18962/package.json b/sentry-javascript/18962/package.json index 1b0f16d..04a452f 100644 --- a/sentry-javascript/18962/package.json +++ b/sentry-javascript/18962/package.json @@ -6,9 +6,7 @@ "scripts": { "build": "nest build", "start": "nest start", - "start:dev": "nest start --watch", - "start:with-sentry": "ENABLE_SENTRY=true node --import ./instrument.mjs dist/main.js", - "start:without-sentry": "node dist/main.js" + "start:dev": "nest start --watch" }, "dependencies": { "@langchain/openai": "^1.2.3", @@ -17,7 +15,6 @@ "@nestjs/platform-express": "^10.0.0", "@sentry/nestjs": "^10.0.0", "langchain": "^1.2.14", - "openai": "^4.77.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, diff --git a/sentry-javascript/18962/src/instrument.ts b/sentry-javascript/18962/src/instrument.ts index 9e0b972..28864fb 100644 --- a/sentry-javascript/18962/src/instrument.ts +++ b/sentry-javascript/18962/src/instrument.ts @@ -11,10 +11,6 @@ if (process.env.ENABLE_SENTRY === "true") { tracesSampleRate: 1.0, debug: true, sendDefaultPii: true, - integrations: [ - Sentry.openAIIntegration({ recordInputs: true, recordOutputs: true }), - Sentry.langChainIntegration({ recordInputs: true, recordOutputs: true }), - ], }); console.log("āœ… Sentry initialized"); diff --git a/sentry-javascript/18962/src/stream.controller.ts b/sentry-javascript/18962/src/stream.controller.ts index 931e442..0be7355 100644 --- a/sentry-javascript/18962/src/stream.controller.ts +++ b/sentry-javascript/18962/src/stream.controller.ts @@ -1,33 +1,21 @@ -import { Controller, Get, Res } from '@nestjs/common'; -import { Response } from 'express'; -import OpenAI from 'openai'; -import { ChatOpenAI } from '@langchain/openai'; -import { HumanMessage } from '@langchain/core/messages'; +import { Controller, Get, Res } from "@nestjs/common"; +import { Response } from "express"; +import { ChatOpenAI } from "@langchain/openai"; +import { HumanMessage } from "@langchain/core/messages"; @Controller() export class StreamController { - private openai: OpenAI | null = null; private langchainModel: ChatOpenAI | null = null; - private getOpenAI(): OpenAI { - if (!this.openai) { - if (!process.env.OPENAI_API_KEY) { - throw new Error('OPENAI_API_KEY not set. Export it with: export OPENAI_API_KEY=sk-...'); - } - this.openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, - }); - } - return this.openai; - } - private getLangchainModel(): ChatOpenAI { if (!this.langchainModel) { if (!process.env.OPENAI_API_KEY) { - throw new Error('OPENAI_API_KEY not set. Export it with: export OPENAI_API_KEY=sk-...'); + throw new Error( + "OPENAI_API_KEY not set. Export it with: export OPENAI_API_KEY=sk-..." + ); } this.langchainModel = new ChatOpenAI({ - model: 'gpt-4o-mini', + model: "gpt-4o-mini", temperature: 0, streaming: true, }); @@ -35,208 +23,123 @@ export class StreamController { return this.langchainModel; } - @Get('/') + @Get("/") index() { return { - message: 'Sentry NestJS OpenAI Streaming Reproduction', - issue: 'https://github.com/getsentry/sentry-javascript/issues/18962', + message: "Sentry NestJS OpenAI Streaming Reproduction", + issue: "https://github.com/getsentry/sentry-javascript/issues/18962", endpoints: { - '/stream': 'Test OpenAI streaming with for-await (GET)', - '/stream-langchain': 'Test OpenAI streaming via Langchain (GET)', - '/stream-web': 'Test OpenAI streaming with toReadableStream() (GET)', - '/stream-sse': 'Test OpenAI streaming with SSE (GET)', + "/stream-openai": "Test OpenAI streaming with for-await (GET)", + "/stream-langchain": "Test OpenAI streaming via Langchain (GET)", }, - sentryEnabled: process.env.ENABLE_SENTRY === 'true', + sentryEnabled: process.env.ENABLE_SENTRY === "true", openaiKeySet: !!process.env.OPENAI_API_KEY, }; } - @Get('/stream') + @Get("/stream-openai") async stream(@Res() res: Response) { try { - const openai = this.getOpenAI(); + const openai = new ChatOpenAI({ + model: "gpt-4o-mini", + temperature: 0, + maxTokens: undefined, + timeout: undefined, + maxRetries: 2, + }); - res.setHeader('Content-Type', 'text/plain; charset=utf-8'); - res.setHeader('Transfer-Encoding', 'chunked'); + res.setHeader("Content-Type", "text/plain; charset=utf-8"); + res.setHeader("Transfer-Encoding", "chunked"); - console.log('\nšŸ“ [OpenAI for-await] Starting streaming request...'); + console.log("\nšŸ“ [OpenAI for-await] Starting streaming request..."); const startTime = Date.now(); let chunkCount = 0; - const stream = await openai.chat.completions.create({ - model: 'gpt-4o-mini', - messages: [ - { - role: 'user', - content: 'Count from 1 to 20, each number on a new line. Be slow and deliberate.', - }, - ], - stream: true, - }); + const stream = await openai.stream( + "Count from 1 to 20, each number on a new line. Be slow and deliberate." + ); for await (const chunk of stream) { chunkCount++; - const content = chunk.choices[0]?.delta?.content || ''; + const content = chunk.content.toString() || ""; if (content) { - console.log(` Chunk ${chunkCount}: "${content.replace(/\n/g, '\\n')}"`); + console.log( + ` Chunk ${chunkCount}: "${content.replace(/\n/g, "\\n")}"` + ); res.write(content); } } const elapsed = Date.now() - startTime; - console.log(`\nāœ… [OpenAI for-await] Completed in ${elapsed}ms, chunks: ${chunkCount}`); - - if (chunkCount < 10) { - console.log('\nāš ļø WARNING: Very few chunks received - streaming may be broken!'); - } - - res.end( - `\n\n--- Stats ---\nChunks: ${chunkCount}\nTime: ${elapsed}ms\nSentry: ${process.env.ENABLE_SENTRY === 'true' ? 'ENABLED' : 'disabled'}\n`, + console.log( + `\nāœ… [OpenAI for-await] Completed in ${elapsed}ms, chunks: ${chunkCount}` ); - } catch (error) { - console.error('āŒ Error:', error.message); - res.status(500).json({ error: error.message }); - } - } - - // Using OpenAI's toReadableStream() - Web Streams API - @Get('/stream-web') - async streamWeb(@Res() res: Response) { - try { - const openai = this.getOpenAI(); - - res.setHeader('Content-Type', 'text/plain; charset=utf-8'); - res.setHeader('Transfer-Encoding', 'chunked'); - - console.log('\nšŸ“ [OpenAI toReadableStream] Starting streaming request...'); - const startTime = Date.now(); - let chunkCount = 0; - - const stream = await openai.chat.completions.create({ - model: 'gpt-4o-mini', - messages: [ - { - role: 'user', - content: 'Count from 1 to 20, each number on a new line.', - }, - ], - stream: true, - }); - - // Using the toReadableStream() method from OpenAI SDK - const webStream = stream.toReadableStream(); - const reader = webStream.getReader(); - const decoder = new TextDecoder(); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - chunkCount++; - const text = decoder.decode(value, { stream: true }); - console.log(` WebStream Chunk ${chunkCount}: "${text.substring(0, 50)}..."`); - res.write(text); - } - - const elapsed = Date.now() - startTime; - console.log(`\nāœ… [OpenAI toReadableStream] Completed in ${elapsed}ms, chunks: ${chunkCount}`); if (chunkCount < 10) { - console.log('\nāš ļø WARNING: Very few chunks received - streaming may be broken!'); + console.log( + "\nāš ļø WARNING: Very few chunks received - streaming may be broken!" + ); } res.end( - `\n\n--- Stats ---\nChunks: ${chunkCount}\nTime: ${elapsed}ms\nSentry: ${process.env.ENABLE_SENTRY === 'true' ? 'ENABLED' : 'disabled'}\nMethod: toReadableStream\n`, + `\n\n--- Stats ---\nChunks: ${chunkCount}\nTime: ${elapsed}ms\nSentry: ${ + process.env.ENABLE_SENTRY === "true" ? "ENABLED" : "disabled" + }\n` ); } catch (error) { - console.error('āŒ WebStream Error:', error.message); + console.error("āŒ Error:", error.message); res.status(500).json({ error: error.message }); } } - @Get('/stream-langchain') + @Get("/stream-langchain") async streamLangchain(@Res() res: Response) { try { const model = this.getLangchainModel(); - res.setHeader('Content-Type', 'text/plain; charset=utf-8'); - res.setHeader('Transfer-Encoding', 'chunked'); + res.setHeader("Content-Type", "text/plain; charset=utf-8"); + res.setHeader("Transfer-Encoding", "chunked"); - console.log('\nšŸ“ [Langchain] Starting streaming request...'); + console.log("\nšŸ“ [Langchain] Starting streaming request..."); const startTime = Date.now(); let chunkCount = 0; // Using Langchain's streaming with .stream() const stream = await model.stream([ - new HumanMessage('Count from 1 to 20, each number on a new line. Be slow and deliberate.'), + new HumanMessage( + "Count from 1 to 20, each number on a new line. Be slow and deliberate." + ), ]); for await (const chunk of stream) { chunkCount++; const content = chunk.content; if (content) { - console.log(` Chunk ${chunkCount}: "${String(content).replace(/\n/g, '\\n')}"`); + console.log( + ` Chunk ${chunkCount}: "${String(content).replace(/\n/g, "\\n")}"` + ); res.write(String(content)); } } const elapsed = Date.now() - startTime; - console.log(`\nāœ… [Langchain] Completed in ${elapsed}ms, chunks: ${chunkCount}`); + console.log( + `\nāœ… [Langchain] Completed in ${elapsed}ms, chunks: ${chunkCount}` + ); if (chunkCount < 10) { - console.log('\nāš ļø WARNING: Very few chunks received - streaming may be broken!'); + console.log( + "\nāš ļø WARNING: Very few chunks received - streaming may be broken!" + ); } res.end( - `\n\n--- Stats ---\nChunks: ${chunkCount}\nTime: ${elapsed}ms\nSentry: ${process.env.ENABLE_SENTRY === 'true' ? 'ENABLED' : 'disabled'}\nMethod: Langchain\n`, + `\n\n--- Stats ---\nChunks: ${chunkCount}\nTime: ${elapsed}ms\nSentry: ${ + process.env.ENABLE_SENTRY === "true" ? "ENABLED" : "disabled" + }\nMethod: Langchain\n` ); } catch (error) { - console.error('āŒ Langchain Error:', error.message); - res.status(500).json({ error: error.message }); - } - } - - @Get('/stream-sse') - async streamSSE(@Res() res: Response) { - try { - const openai = this.getOpenAI(); - - // Server-Sent Events format - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Connection', 'keep-alive'); - - console.log('\nšŸ“ [SSE] Starting streaming request...'); - const startTime = Date.now(); - let chunkCount = 0; - - const stream = await openai.chat.completions.create({ - model: 'gpt-4o-mini', - messages: [ - { - role: 'user', - content: 'Count from 1 to 20, each number on a new line.', - }, - ], - stream: true, - }); - - for await (const chunk of stream) { - chunkCount++; - const content = chunk.choices[0]?.delta?.content || ''; - if (content) { - console.log(` SSE Chunk ${chunkCount}: "${content.replace(/\n/g, '\\n')}"`); - res.write(`data: ${JSON.stringify({ content, chunk: chunkCount })}\n\n`); - } - } - - const elapsed = Date.now() - startTime; - console.log(`\nāœ… [SSE] Completed in ${elapsed}ms, chunks: ${chunkCount}`); - - res.write(`data: ${JSON.stringify({ done: true, totalChunks: chunkCount, elapsed })}\n\n`); - res.end(); - } catch (error) { - console.error('āŒ SSE Error:', error.message); + console.error("āŒ Langchain Error:", error.message); res.status(500).json({ error: error.message }); } }