Merged
14 changes: 7 additions & 7 deletions .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,21 +96,21 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackIntegration'),
gzip: true,
limit: '42 KB',
limit: '43 KB',
},
{
name: '@sentry/browser (incl. sendFeedback)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'sendFeedback'),
gzip: true,
limit: '30 KB',
limit: '31 KB',
},
{
name: '@sentry/browser (incl. FeedbackAsync)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackAsyncIntegration'),
gzip: true,
limit: '35 KB',
limit: '36 KB',
},
{
name: '@sentry/browser (incl. Metrics)',
Expand DownExpand Up@@ -140,7 +140,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary'),
ignore: ['react/jsx-runtime'],
gzip: true,
limit: '27 KB',
limit: '28 KB',
},
{
name: '@sentry/react (incl. Tracing)',
Expand DownExpand Up@@ -208,7 +208,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)',
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
gzip: true,
limit: '86 KB',
limit: '87 KB',
},
// browser CDN bundles (non-gzipped)
{
Expand All@@ -223,7 +223,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
limit: '127 KB',
limit: '128 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed',
Expand DownExpand Up@@ -278,7 +278,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '52 KB',
limit: '53 KB',
},
// Node SDK (ESM)
{
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,23 @@

Work in this release was contributed by @sebws and @harshit078. Thank you for your contributions!

- **feat(core): Introduces a new `Sentry.setConversationId()` API to track multi turn AI conversations across API calls. ([#18909](https://github.com/getsentry/sentry-javascript/pull/18909))**

You can now set a conversation ID that will be automatically applied to spans within that scope. This allows you to link traces from the same conversation together.

```javascript
import * as Sentry from '@sentry/node';

// Set conversation ID for all subsequent spans
Sentry.setConversationId('conv_abc123');

// All AI spans will now include the gen_ai.conversation.id attribute
await openai.chat.completions.create({...});
```

This is particularly useful for tracking multiple AI API calls that are part of the same conversation, allowing you to analyze entire conversation flows in Sentry.
The conversation ID is stored on the isolation scope and automatically applied to spans via the new `conversationIdIntegration`.

- **feat(tanstackstart-react): Auto-instrument global middleware in `sentryTanstackStart` Vite plugin ([#18884](https://github.com/getsentry/sentry-javascript/pull/18844))**

The `sentryTanstackStart` Vite plugin now automatically instruments `requestMiddleware` and `functionMiddleware` arrays in `createStart()`. This captures performance data without requiring manual wrapping.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ sentryTest('logs debug messages correctly', async ({ getLocalTestUrl, page }) =>
? [
'Sentry Logger [log]: Integration installed: InboundFilters',
'Sentry Logger [log]: Integration installed: FunctionToString',
'Sentry Logger [log]: Integration installed: ConversationId',
'Sentry Logger [log]: Integration installed: BrowserApiErrors',
'Sentry Logger [log]: Integration installed: Breadcrumbs',
'Sentry Logger [log]: Global Handler attached: onerror',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();

// Test: Multiple chat completions in the same conversation with manual conversation ID
await Sentry.startSpan({ op: 'function', name: 'chat-with-manual-conversation-id' }, async () => {
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Set conversation ID manually using Sentry API
Sentry.setConversationId('user_chat_session_abc123');

// First message in the conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is the capital of France?' }],
});

// Second message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Tell me more about it' }],
});

// Third message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is its population?' }],
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// First request/conversation scope
await Sentry.withScope(async scope => {
// Set conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user1_session_abc');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-1' }, async () => {
// First message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 1' }],
});

// Second message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 1' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Second request/conversation scope (completely separate)
await Sentry.withScope(async scope => {
// Set different conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user2_session_xyz');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-2' }, async () => {
// First message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 2' }],
});

// Second message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 2' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
14 changes: 7 additions & 7 deletions .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,21 +96,21 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackIntegration'),
gzip: true,
limit: '42 KB',
limit: '43 KB',
},
{
name: '@sentry/browser (incl. sendFeedback)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'sendFeedback'),
gzip: true,
limit: '30 KB',
limit: '31 KB',
},
{
name: '@sentry/browser (incl. FeedbackAsync)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackAsyncIntegration'),
gzip: true,
limit: '35 KB',
limit: '36 KB',
},
{
name: '@sentry/browser (incl. Metrics)',
Expand DownExpand Up@@ -140,7 +140,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary'),
ignore: ['react/jsx-runtime'],
gzip: true,
limit: '27 KB',
limit: '28 KB',
},
{
name: '@sentry/react (incl. Tracing)',
Expand DownExpand Up@@ -208,7 +208,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)',
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
gzip: true,
limit: '86 KB',
limit: '87 KB',
},
// browser CDN bundles (non-gzipped)
{
Expand All@@ -223,7 +223,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
limit: '127 KB',
limit: '128 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed',
Expand DownExpand Up@@ -278,7 +278,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '52 KB',
limit: '53 KB',
},
// Node SDK (ESM)
{
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,23 @@

Work in this release was contributed by @sebws and @harshit078. Thank you for your contributions!

- **feat(core): Introduces a new `Sentry.setConversationId()` API to track multi turn AI conversations across API calls. ([#18909](https://github.com/getsentry/sentry-javascript/pull/18909))**

You can now set a conversation ID that will be automatically applied to spans within that scope. This allows you to link traces from the same conversation together.

```javascript
import * as Sentry from '@sentry/node';

// Set conversation ID for all subsequent spans
Sentry.setConversationId('conv_abc123');

// All AI spans will now include the gen_ai.conversation.id attribute
await openai.chat.completions.create({...});
```

This is particularly useful for tracking multiple AI API calls that are part of the same conversation, allowing you to analyze entire conversation flows in Sentry.
The conversation ID is stored on the isolation scope and automatically applied to spans via the new `conversationIdIntegration`.

- **feat(tanstackstart-react): Auto-instrument global middleware in `sentryTanstackStart` Vite plugin ([#18884](https://github.com/getsentry/sentry-javascript/pull/18844))**

The `sentryTanstackStart` Vite plugin now automatically instruments `requestMiddleware` and `functionMiddleware` arrays in `createStart()`. This captures performance data without requiring manual wrapping.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ sentryTest('logs debug messages correctly', async ({ getLocalTestUrl, page }) =>
? [
'Sentry Logger [log]: Integration installed: InboundFilters',
'Sentry Logger [log]: Integration installed: FunctionToString',
'Sentry Logger [log]: Integration installed: ConversationId',
'Sentry Logger [log]: Integration installed: BrowserApiErrors',
'Sentry Logger [log]: Integration installed: Breadcrumbs',
'Sentry Logger [log]: Global Handler attached: onerror',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();

// Test: Multiple chat completions in the same conversation with manual conversation ID
await Sentry.startSpan({ op: 'function', name: 'chat-with-manual-conversation-id' }, async () => {
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Set conversation ID manually using Sentry API
Sentry.setConversationId('user_chat_session_abc123');

// First message in the conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is the capital of France?' }],
});

// Second message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Tell me more about it' }],
});

// Third message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is its population?' }],
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// First request/conversation scope
await Sentry.withScope(async scope => {
// Set conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user1_session_abc');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-1' }, async () => {
// First message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 1' }],
});

// Second message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 1' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Second request/conversation scope (completely separate)
await Sentry.withScope(async scope => {
// Set different conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user2_session_xyz');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-2' }, async () => {
// First message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 2' }],
});

// Second message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 2' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
14 changes: 7 additions & 7 deletions .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,21 +96,21 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackIntegration'),
gzip: true,
limit: '42 KB',
limit: '43 KB',
},
{
name: '@sentry/browser (incl. sendFeedback)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'sendFeedback'),
gzip: true,
limit: '30 KB',
limit: '31 KB',
},
{
name: '@sentry/browser (incl. FeedbackAsync)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackAsyncIntegration'),
gzip: true,
limit: '35 KB',
limit: '36 KB',
},
{
name: '@sentry/browser (incl. Metrics)',
Expand DownExpand Up@@ -140,7 +140,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary'),
ignore: ['react/jsx-runtime'],
gzip: true,
limit: '27 KB',
limit: '28 KB',
},
{
name: '@sentry/react (incl. Tracing)',
Expand DownExpand Up@@ -208,7 +208,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)',
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
gzip: true,
limit: '86 KB',
limit: '87 KB',
},
// browser CDN bundles (non-gzipped)
{
Expand All@@ -223,7 +223,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
limit: '127 KB',
limit: '128 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed',
Expand DownExpand Up@@ -278,7 +278,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '52 KB',
limit: '53 KB',
},
// Node SDK (ESM)
{
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,23 @@

Work in this release was contributed by @sebws and @harshit078. Thank you for your contributions!

- **feat(core): Introduces a new `Sentry.setConversationId()` API to track multi turn AI conversations across API calls. ([#18909](https://github.com/getsentry/sentry-javascript/pull/18909))**

You can now set a conversation ID that will be automatically applied to spans within that scope. This allows you to link traces from the same conversation together.

```javascript
import * as Sentry from '@sentry/node';

// Set conversation ID for all subsequent spans
Sentry.setConversationId('conv_abc123');

// All AI spans will now include the gen_ai.conversation.id attribute
await openai.chat.completions.create({...});
```

This is particularly useful for tracking multiple AI API calls that are part of the same conversation, allowing you to analyze entire conversation flows in Sentry.
The conversation ID is stored on the isolation scope and automatically applied to spans via the new `conversationIdIntegration`.

- **feat(tanstackstart-react): Auto-instrument global middleware in `sentryTanstackStart` Vite plugin ([#18884](https://github.com/getsentry/sentry-javascript/pull/18844))**

The `sentryTanstackStart` Vite plugin now automatically instruments `requestMiddleware` and `functionMiddleware` arrays in `createStart()`. This captures performance data without requiring manual wrapping.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ sentryTest('logs debug messages correctly', async ({ getLocalTestUrl, page }) =>
? [
'Sentry Logger [log]: Integration installed: InboundFilters',
'Sentry Logger [log]: Integration installed: FunctionToString',
'Sentry Logger [log]: Integration installed: ConversationId',
'Sentry Logger [log]: Integration installed: BrowserApiErrors',
'Sentry Logger [log]: Integration installed: Breadcrumbs',
'Sentry Logger [log]: Global Handler attached: onerror',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();

// Test: Multiple chat completions in the same conversation with manual conversation ID
await Sentry.startSpan({ op: 'function', name: 'chat-with-manual-conversation-id' }, async () => {
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Set conversation ID manually using Sentry API
Sentry.setConversationId('user_chat_session_abc123');

// First message in the conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is the capital of France?' }],
});

// Second message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Tell me more about it' }],
});

// Third message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is its population?' }],
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// First request/conversation scope
await Sentry.withScope(async scope => {
// Set conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user1_session_abc');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-1' }, async () => {
// First message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 1' }],
});

// Second message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 1' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Second request/conversation scope (completely separate)
await Sentry.withScope(async scope => {
// Set different conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user2_session_xyz');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-2' }, async () => {
// First message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 2' }],
});

// Second message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 2' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
14 changes: 7 additions & 7 deletions .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,21 +96,21 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackIntegration'),
gzip: true,
limit: '42 KB',
limit: '43 KB',
},
{
name: '@sentry/browser (incl. sendFeedback)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'sendFeedback'),
gzip: true,
limit: '30 KB',
limit: '31 KB',
},
{
name: '@sentry/browser (incl. FeedbackAsync)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackAsyncIntegration'),
gzip: true,
limit: '35 KB',
limit: '36 KB',
},
{
name: '@sentry/browser (incl. Metrics)',
Expand DownExpand Up@@ -140,7 +140,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary'),
ignore: ['react/jsx-runtime'],
gzip: true,
limit: '27 KB',
limit: '28 KB',
},
{
name: '@sentry/react (incl. Tracing)',
Expand DownExpand Up@@ -208,7 +208,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)',
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
gzip: true,
limit: '86 KB',
limit: '87 KB',
},
// browser CDN bundles (non-gzipped)
{
Expand All@@ -223,7 +223,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
limit: '127 KB',
limit: '128 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed',
Expand DownExpand Up@@ -278,7 +278,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '52 KB',
limit: '53 KB',
},
// Node SDK (ESM)
{
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,23 @@

Work in this release was contributed by @sebws and @harshit078. Thank you for your contributions!

- **feat(core): Introduces a new `Sentry.setConversationId()` API to track multi turn AI conversations across API calls. ([#18909](https://github.com/getsentry/sentry-javascript/pull/18909))**

You can now set a conversation ID that will be automatically applied to spans within that scope. This allows you to link traces from the same conversation together.

```javascript
import * as Sentry from '@sentry/node';

// Set conversation ID for all subsequent spans
Sentry.setConversationId('conv_abc123');

// All AI spans will now include the gen_ai.conversation.id attribute
await openai.chat.completions.create({...});
```

This is particularly useful for tracking multiple AI API calls that are part of the same conversation, allowing you to analyze entire conversation flows in Sentry.
The conversation ID is stored on the isolation scope and automatically applied to spans via the new `conversationIdIntegration`.

- **feat(tanstackstart-react): Auto-instrument global middleware in `sentryTanstackStart` Vite plugin ([#18884](https://github.com/getsentry/sentry-javascript/pull/18844))**

The `sentryTanstackStart` Vite plugin now automatically instruments `requestMiddleware` and `functionMiddleware` arrays in `createStart()`. This captures performance data without requiring manual wrapping.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ sentryTest('logs debug messages correctly', async ({ getLocalTestUrl, page }) =>
? [
'Sentry Logger [log]: Integration installed: InboundFilters',
'Sentry Logger [log]: Integration installed: FunctionToString',
'Sentry Logger [log]: Integration installed: ConversationId',
'Sentry Logger [log]: Integration installed: BrowserApiErrors',
'Sentry Logger [log]: Integration installed: Breadcrumbs',
'Sentry Logger [log]: Global Handler attached: onerror',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();

// Test: Multiple chat completions in the same conversation with manual conversation ID
await Sentry.startSpan({ op: 'function', name: 'chat-with-manual-conversation-id' }, async () => {
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Set conversation ID manually using Sentry API
Sentry.setConversationId('user_chat_session_abc123');

// First message in the conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is the capital of France?' }],
});

// Second message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Tell me more about it' }],
});

// Third message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is its population?' }],
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// First request/conversation scope
await Sentry.withScope(async scope => {
// Set conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user1_session_abc');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-1' }, async () => {
// First message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 1' }],
});

// Second message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 1' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Second request/conversation scope (completely separate)
await Sentry.withScope(async scope => {
// Set different conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user2_session_xyz');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-2' }, async () => {
// First message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 2' }],
});

// Second message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 2' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
14 changes: 7 additions & 7 deletions .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,21 +96,21 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackIntegration'),
gzip: true,
limit: '42 KB',
limit: '43 KB',
},
{
name: '@sentry/browser (incl. sendFeedback)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'sendFeedback'),
gzip: true,
limit: '30 KB',
limit: '31 KB',
},
{
name: '@sentry/browser (incl. FeedbackAsync)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackAsyncIntegration'),
gzip: true,
limit: '35 KB',
limit: '36 KB',
},
{
name: '@sentry/browser (incl. Metrics)',
Expand DownExpand Up@@ -140,7 +140,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary'),
ignore: ['react/jsx-runtime'],
gzip: true,
limit: '27 KB',
limit: '28 KB',
},
{
name: '@sentry/react (incl. Tracing)',
Expand DownExpand Up@@ -208,7 +208,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)',
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
gzip: true,
limit: '86 KB',
limit: '87 KB',
},
// browser CDN bundles (non-gzipped)
{
Expand All@@ -223,7 +223,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
limit: '127 KB',
limit: '128 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed',
Expand DownExpand Up@@ -278,7 +278,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '52 KB',
limit: '53 KB',
},
// Node SDK (ESM)
{
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,23 @@

Work in this release was contributed by @sebws and @harshit078. Thank you for your contributions!

- **feat(core): Introduces a new `Sentry.setConversationId()` API to track multi turn AI conversations across API calls. ([#18909](https://github.com/getsentry/sentry-javascript/pull/18909))**

You can now set a conversation ID that will be automatically applied to spans within that scope. This allows you to link traces from the same conversation together.

```javascript
import * as Sentry from '@sentry/node';

// Set conversation ID for all subsequent spans
Sentry.setConversationId('conv_abc123');

// All AI spans will now include the gen_ai.conversation.id attribute
await openai.chat.completions.create({...});
```

This is particularly useful for tracking multiple AI API calls that are part of the same conversation, allowing you to analyze entire conversation flows in Sentry.
The conversation ID is stored on the isolation scope and automatically applied to spans via the new `conversationIdIntegration`.

- **feat(tanstackstart-react): Auto-instrument global middleware in `sentryTanstackStart` Vite plugin ([#18884](https://github.com/getsentry/sentry-javascript/pull/18844))**

The `sentryTanstackStart` Vite plugin now automatically instruments `requestMiddleware` and `functionMiddleware` arrays in `createStart()`. This captures performance data without requiring manual wrapping.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ sentryTest('logs debug messages correctly', async ({ getLocalTestUrl, page }) =>
? [
'Sentry Logger [log]: Integration installed: InboundFilters',
'Sentry Logger [log]: Integration installed: FunctionToString',
'Sentry Logger [log]: Integration installed: ConversationId',
'Sentry Logger [log]: Integration installed: BrowserApiErrors',
'Sentry Logger [log]: Integration installed: Breadcrumbs',
'Sentry Logger [log]: Global Handler attached: onerror',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();

// Test: Multiple chat completions in the same conversation with manual conversation ID
await Sentry.startSpan({ op: 'function', name: 'chat-with-manual-conversation-id' }, async () => {
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Set conversation ID manually using Sentry API
Sentry.setConversationId('user_chat_session_abc123');

// First message in the conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is the capital of France?' }],
});

// Second message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Tell me more about it' }],
});

// Third message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is its population?' }],
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// First request/conversation scope
await Sentry.withScope(async scope => {
// Set conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user1_session_abc');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-1' }, async () => {
// First message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 1' }],
});

// Second message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 1' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Second request/conversation scope (completely separate)
await Sentry.withScope(async scope => {
// Set different conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user2_session_xyz');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-2' }, async () => {
// First message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 2' }],
});

// Second message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 2' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
14 changes: 7 additions & 7 deletions .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,21 +96,21 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackIntegration'),
gzip: true,
limit: '42 KB',
limit: '43 KB',
},
{
name: '@sentry/browser (incl. sendFeedback)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'sendFeedback'),
gzip: true,
limit: '30 KB',
limit: '31 KB',
},
{
name: '@sentry/browser (incl. FeedbackAsync)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackAsyncIntegration'),
gzip: true,
limit: '35 KB',
limit: '36 KB',
},
{
name: '@sentry/browser (incl. Metrics)',
Expand DownExpand Up@@ -140,7 +140,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary'),
ignore: ['react/jsx-runtime'],
gzip: true,
limit: '27 KB',
limit: '28 KB',
},
{
name: '@sentry/react (incl. Tracing)',
Expand DownExpand Up@@ -208,7 +208,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)',
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
gzip: true,
limit: '86 KB',
limit: '87 KB',
},
// browser CDN bundles (non-gzipped)
{
Expand All@@ -223,7 +223,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
limit: '127 KB',
limit: '128 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed',
Expand DownExpand Up@@ -278,7 +278,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '52 KB',
limit: '53 KB',
},
// Node SDK (ESM)
{
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,23 @@

Work in this release was contributed by @sebws and @harshit078. Thank you for your contributions!

- **feat(core): Introduces a new `Sentry.setConversationId()` API to track multi turn AI conversations across API calls. ([#18909](https://github.com/getsentry/sentry-javascript/pull/18909))**

You can now set a conversation ID that will be automatically applied to spans within that scope. This allows you to link traces from the same conversation together.

```javascript
import * as Sentry from '@sentry/node';

// Set conversation ID for all subsequent spans
Sentry.setConversationId('conv_abc123');

// All AI spans will now include the gen_ai.conversation.id attribute
await openai.chat.completions.create({...});
```

This is particularly useful for tracking multiple AI API calls that are part of the same conversation, allowing you to analyze entire conversation flows in Sentry.
The conversation ID is stored on the isolation scope and automatically applied to spans via the new `conversationIdIntegration`.

- **feat(tanstackstart-react): Auto-instrument global middleware in `sentryTanstackStart` Vite plugin ([#18884](https://github.com/getsentry/sentry-javascript/pull/18844))**

The `sentryTanstackStart` Vite plugin now automatically instruments `requestMiddleware` and `functionMiddleware` arrays in `createStart()`. This captures performance data without requiring manual wrapping.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ sentryTest('logs debug messages correctly', async ({ getLocalTestUrl, page }) =>
? [
'Sentry Logger [log]: Integration installed: InboundFilters',
'Sentry Logger [log]: Integration installed: FunctionToString',
'Sentry Logger [log]: Integration installed: ConversationId',
'Sentry Logger [log]: Integration installed: BrowserApiErrors',
'Sentry Logger [log]: Integration installed: Breadcrumbs',
'Sentry Logger [log]: Global Handler attached: onerror',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();

// Test: Multiple chat completions in the same conversation with manual conversation ID
await Sentry.startSpan({ op: 'function', name: 'chat-with-manual-conversation-id' }, async () => {
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Set conversation ID manually using Sentry API
Sentry.setConversationId('user_chat_session_abc123');

// First message in the conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is the capital of France?' }],
});

// Second message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Tell me more about it' }],
});

// Third message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is its population?' }],
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// First request/conversation scope
await Sentry.withScope(async scope => {
// Set conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user1_session_abc');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-1' }, async () => {
// First message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 1' }],
});

// Second message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 1' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Second request/conversation scope (completely separate)
await Sentry.withScope(async scope => {
// Set different conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user2_session_xyz');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-2' }, async () => {
// First message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 2' }],
});

// Second message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 2' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
14 changes: 7 additions & 7 deletions .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,21 +96,21 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackIntegration'),
gzip: true,
limit: '42 KB',
limit: '43 KB',
},
{
name: '@sentry/browser (incl. sendFeedback)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'sendFeedback'),
gzip: true,
limit: '30 KB',
limit: '31 KB',
},
{
name: '@sentry/browser (incl. FeedbackAsync)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackAsyncIntegration'),
gzip: true,
limit: '35 KB',
limit: '36 KB',
},
{
name: '@sentry/browser (incl. Metrics)',
Expand DownExpand Up@@ -140,7 +140,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary'),
ignore: ['react/jsx-runtime'],
gzip: true,
limit: '27 KB',
limit: '28 KB',
},
{
name: '@sentry/react (incl. Tracing)',
Expand DownExpand Up@@ -208,7 +208,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)',
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
gzip: true,
limit: '86 KB',
limit: '87 KB',
},
// browser CDN bundles (non-gzipped)
{
Expand All@@ -223,7 +223,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
limit: '127 KB',
limit: '128 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed',
Expand DownExpand Up@@ -278,7 +278,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '52 KB',
limit: '53 KB',
},
// Node SDK (ESM)
{
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,23 @@

Work in this release was contributed by @sebws and @harshit078. Thank you for your contributions!

- **feat(core): Introduces a new `Sentry.setConversationId()` API to track multi turn AI conversations across API calls. ([#18909](https://github.com/getsentry/sentry-javascript/pull/18909))**

You can now set a conversation ID that will be automatically applied to spans within that scope. This allows you to link traces from the same conversation together.

```javascript
import * as Sentry from '@sentry/node';

// Set conversation ID for all subsequent spans
Sentry.setConversationId('conv_abc123');

// All AI spans will now include the gen_ai.conversation.id attribute
await openai.chat.completions.create({...});
```

This is particularly useful for tracking multiple AI API calls that are part of the same conversation, allowing you to analyze entire conversation flows in Sentry.
The conversation ID is stored on the isolation scope and automatically applied to spans via the new `conversationIdIntegration`.

- **feat(tanstackstart-react): Auto-instrument global middleware in `sentryTanstackStart` Vite plugin ([#18884](https://github.com/getsentry/sentry-javascript/pull/18844))**

The `sentryTanstackStart` Vite plugin now automatically instruments `requestMiddleware` and `functionMiddleware` arrays in `createStart()`. This captures performance data without requiring manual wrapping.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ sentryTest('logs debug messages correctly', async ({ getLocalTestUrl, page }) =>
? [
'Sentry Logger [log]: Integration installed: InboundFilters',
'Sentry Logger [log]: Integration installed: FunctionToString',
'Sentry Logger [log]: Integration installed: ConversationId',
'Sentry Logger [log]: Integration installed: BrowserApiErrors',
'Sentry Logger [log]: Integration installed: Breadcrumbs',
'Sentry Logger [log]: Global Handler attached: onerror',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();

// Test: Multiple chat completions in the same conversation with manual conversation ID
await Sentry.startSpan({ op: 'function', name: 'chat-with-manual-conversation-id' }, async () => {
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Set conversation ID manually using Sentry API
Sentry.setConversationId('user_chat_session_abc123');

// First message in the conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is the capital of France?' }],
});

// Second message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Tell me more about it' }],
});

// Third message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is its population?' }],
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// First request/conversation scope
await Sentry.withScope(async scope => {
// Set conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user1_session_abc');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-1' }, async () => {
// First message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 1' }],
});

// Second message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 1' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Second request/conversation scope (completely separate)
await Sentry.withScope(async scope => {
// Set different conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user2_session_xyz');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-2' }, async () => {
// First message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 2' }],
});

// Second message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 2' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
14 changes: 7 additions & 7 deletions .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,21 +96,21 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackIntegration'),
gzip: true,
limit: '42 KB',
limit: '43 KB',
},
{
name: '@sentry/browser (incl. sendFeedback)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'sendFeedback'),
gzip: true,
limit: '30 KB',
limit: '31 KB',
},
{
name: '@sentry/browser (incl. FeedbackAsync)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackAsyncIntegration'),
gzip: true,
limit: '35 KB',
limit: '36 KB',
},
{
name: '@sentry/browser (incl. Metrics)',
Expand DownExpand Up@@ -140,7 +140,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary'),
ignore: ['react/jsx-runtime'],
gzip: true,
limit: '27 KB',
limit: '28 KB',
},
{
name: '@sentry/react (incl. Tracing)',
Expand DownExpand Up@@ -208,7 +208,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)',
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
gzip: true,
limit: '86 KB',
limit: '87 KB',
},
// browser CDN bundles (non-gzipped)
{
Expand All@@ -223,7 +223,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
limit: '127 KB',
limit: '128 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed',
Expand DownExpand Up@@ -278,7 +278,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '52 KB',
limit: '53 KB',
},
// Node SDK (ESM)
{
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,23 @@

Work in this release was contributed by @sebws and @harshit078. Thank you for your contributions!

- **feat(core): Introduces a new `Sentry.setConversationId()` API to track multi turn AI conversations across API calls. ([#18909](https://github.com/getsentry/sentry-javascript/pull/18909))**

You can now set a conversation ID that will be automatically applied to spans within that scope. This allows you to link traces from the same conversation together.

```javascript
import * as Sentry from '@sentry/node';

// Set conversation ID for all subsequent spans
Sentry.setConversationId('conv_abc123');

// All AI spans will now include the gen_ai.conversation.id attribute
await openai.chat.completions.create({...});
```

This is particularly useful for tracking multiple AI API calls that are part of the same conversation, allowing you to analyze entire conversation flows in Sentry.
The conversation ID is stored on the isolation scope and automatically applied to spans via the new `conversationIdIntegration`.

- **feat(tanstackstart-react): Auto-instrument global middleware in `sentryTanstackStart` Vite plugin ([#18884](https://github.com/getsentry/sentry-javascript/pull/18844))**

The `sentryTanstackStart` Vite plugin now automatically instruments `requestMiddleware` and `functionMiddleware` arrays in `createStart()`. This captures performance data without requiring manual wrapping.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ sentryTest('logs debug messages correctly', async ({ getLocalTestUrl, page }) =>
? [
'Sentry Logger [log]: Integration installed: InboundFilters',
'Sentry Logger [log]: Integration installed: FunctionToString',
'Sentry Logger [log]: Integration installed: ConversationId',
'Sentry Logger [log]: Integration installed: BrowserApiErrors',
'Sentry Logger [log]: Integration installed: Breadcrumbs',
'Sentry Logger [log]: Global Handler attached: onerror',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();

// Test: Multiple chat completions in the same conversation with manual conversation ID
await Sentry.startSpan({ op: 'function', name: 'chat-with-manual-conversation-id' }, async () => {
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Set conversation ID manually using Sentry API
Sentry.setConversationId('user_chat_session_abc123');

// First message in the conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is the capital of France?' }],
});

// Second message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Tell me more about it' }],
});

// Third message in the same conversation
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'What is its population?' }],
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// First request/conversation scope
await Sentry.withScope(async scope => {
// Set conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user1_session_abc');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-1' }, async () => {
// First message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 1' }],
});

// Second message in conversation 1
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 1' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import express from 'express';
import OpenAI from 'openai';

function startMockServer() {
const app = express();
app.use(express.json());

// Chat completions endpoint
app.post('/openai/chat/completions', (req, res) => {
const { model } = req.body;

res.send({
id: 'chatcmpl-mock123',
object: 'chat.completion',
created: 1677652288,
model: model,
choices: [
{
index: 0,
message: {
role: 'assistant',
content: 'Mock response from OpenAI',
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 15,
total_tokens: 25,
},
});
});

return new Promise(resolve => {
const server = app.listen(0, () => {
resolve(server);
});
});
}

async function run() {
const server = await startMockServer();
const client = new OpenAI({
baseURL: `http://localhost:${server.address().port}/openai`,
apiKey: 'mock-api-key',
});

// Second request/conversation scope (completely separate)
await Sentry.withScope(async scope => {
// Set different conversation ID for this request scope BEFORE starting the span
scope.setConversationId('conv_user2_session_xyz');

await Sentry.startSpan({ op: 'http.server', name: 'GET /chat/conversation-2' }, async () => {
// First message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello from conversation 2' }],
});

// Second message in conversation 2
await client.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Follow-up in conversation 2' }],
});
});
});

server.close();
await Sentry.flush(2000);
}

run();
Loading
Loading