Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ module.exports = [
path: createCDNPath('bundle.min.js'),
gzip: false,
brotli: false,
limit: '83 KB',
limit: '83.5 KB',
},
{
name: 'CDN Bundle (incl. Tracing) - uncompressed',
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/integrations/conversationId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { defineIntegration } from '../integration';
import { GEN_AI_CONVERSATION_ID_ATTRIBUTE } from '../semanticAttributes';
import type { IntegrationFn } from '../types-hoist/integration';
import type { Span } from '../types-hoist/span';
import { spanToJSON } from '../utils/spanUtils';

const INTEGRATION_NAME = 'ConversationId';

Expand All @@ -18,6 +19,16 @@ const _conversationIdIntegration = (() => {
const conversationId = scopeData.conversationId || isolationScopeData.conversationId;

if (conversationId) {
const { op, data: attributes, description: name } = spanToJSON(span);

// Only apply conversation ID to gen_ai spans.
// We also check for Vercel AI spans (ai.operationId attribute or ai.* span name)
// because the Vercel AI integration sets the gen_ai.* op in its own spanStart handler
// which fires after this, so the op is not yet available at this point.
if (!op?.startsWith('gen_ai.') && !attributes['ai.operationId'] && !name?.startsWith('ai.')) {
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conversation ID skipped for Vercel AI spans due to hook ordering

High Severity

The spanToJSON(span).op check will always fail for Vercel AI spans because the gen_ai.* op is set by the Vercel AI integration's own spanStart handler (onVercelAiSpanStart), which is registered after the conversationIdIntegration handler. Since conversationIdIntegration uses setup() and is a default integration, while Vercel AI registers its handler in afterAllSetup() (Node) or later in setup() order (Cloudflare), the conversation ID handler always runs first — when op is still undefined. This means conversation_id will never be applied to Vercel AI gen_ai spans, which is a regression from the previous behavior.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 81df583. Configure here.


span.setAttribute(GEN_AI_CONVERSATION_ID_ATTRIBUTE, conversationId);
}
});
Expand Down
24 changes: 17 additions & 7 deletions packages/core/test/lib/integrations/conversationId.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('ConversationId', () => {
it('applies conversation ID from current scope to span', () => {
getCurrentScope().setConversationId('conv_test_123');

startSpan({ name: 'test-span' }, span => {
startSpan({ name: 'test-span', op: 'gen_ai.chat' }, span => {
const spanJSON = spanToJSON(span);
expect(spanJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBe('conv_test_123');
});
Expand All @@ -35,7 +35,7 @@ describe('ConversationId', () => {
it('applies conversation ID from isolation scope when current scope does not have one', () => {
getIsolationScope().setConversationId('conv_isolation_456');

startSpan({ name: 'test-span' }, span => {
startSpan({ name: 'test-span', op: 'gen_ai.chat' }, span => {
const spanJSON = spanToJSON(span);
expect(spanJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBe('conv_isolation_456');
});
Expand All @@ -45,14 +45,14 @@ describe('ConversationId', () => {
getCurrentScope().setConversationId('conv_current_789');
getIsolationScope().setConversationId('conv_isolation_999');

startSpan({ name: 'test-span' }, span => {
startSpan({ name: 'test-span', op: 'gen_ai.chat' }, span => {
const spanJSON = spanToJSON(span);
expect(spanJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBe('conv_current_789');
});
});

it('does not apply conversation ID when not set in scope', () => {
startSpan({ name: 'test-span' }, span => {
startSpan({ name: 'test-span', op: 'gen_ai.chat' }, span => {
const spanJSON = spanToJSON(span);
expect(spanJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBeUndefined();
});
Expand All @@ -62,7 +62,7 @@ describe('ConversationId', () => {
getCurrentScope().setConversationId('conv_test_123');
getCurrentScope().setConversationId(null);

startSpan({ name: 'test-span' }, span => {
startSpan({ name: 'test-span', op: 'gen_ai.chat' }, span => {
const spanJSON = spanToJSON(span);
expect(spanJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBeUndefined();
});
Expand All @@ -71,8 +71,8 @@ describe('ConversationId', () => {
it('applies conversation ID to nested spans', () => {
getCurrentScope().setConversationId('conv_nested_abc');

startSpan({ name: 'parent-span' }, () => {
startSpan({ name: 'child-span' }, childSpan => {
startSpan({ name: 'parent-span', op: 'gen_ai.invoke_agent' }, () => {
startSpan({ name: 'child-span', op: 'gen_ai.chat' }, childSpan => {
const childJSON = spanToJSON(childSpan);
expect(childJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBe('conv_nested_abc');
});
Expand All @@ -85,6 +85,7 @@ describe('ConversationId', () => {
startSpan(
{
name: 'test-span',
op: 'gen_ai.chat',
attributes: {
[GEN_AI_CONVERSATION_ID_ATTRIBUTE]: 'conv_explicit',
},
Expand All @@ -95,4 +96,13 @@ describe('ConversationId', () => {
},
);
});

it('does not apply conversation ID to non-gen_ai spans', () => {
getCurrentScope().setConversationId('conv_test_123');

startSpan({ name: 'db-query', op: 'db.query' }, span => {
const spanJSON = spanToJSON(span);
expect(spanJSON.data[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBeUndefined();
});
});
});
Loading