Skip to content

Commit 5d59cce

Browse files
committed
wip(ai): Support scope-level conversation ID for AI tracing
1 parent 9115e19 commit 5d59cce

6 files changed

Lines changed: 83 additions & 0 deletions

File tree

packages/core/src/exports.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,24 @@ export function setUser(user: User | null): void {
111111
getIsolationScope().setUser(user);
112112
}
113113

114+
/**
115+
* Sets the conversation ID for the current isolation scope.
116+
*
117+
* @param conversationId The conversation ID to set. Pass `null` or `undefined` to unset the conversation ID.
118+
*/
119+
export function setConversationId(conversationId: string | null | undefined): void {
120+
getIsolationScope().setConversationId(conversationId);
121+
}
122+
123+
/**
124+
* Gets the conversation ID from the current isolation scope.
125+
*
126+
* @returns The conversation ID, or `undefined` if not set.
127+
*/
128+
export function getConversationId(): string | undefined {
129+
return getIsolationScope().getConversationId();
130+
}
131+
114132
/**
115133
* The last error event id of the isolation scope.
116134
*

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export {
2525
setTag,
2626
setTags,
2727
setUser,
28+
setConversationId,
29+
getConversationId,
2830
isInitialized,
2931
isEnabled,
3032
startSession,

packages/core/src/scope.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ export class Scope {
153153
/** Contains the last event id of a captured event. */
154154
protected _lastEventId?: string;
155155

156+
/** Conversation ID */
157+
protected _conversationId?: string;
158+
156159
// NOTE: Any field which gets added here should get added not only to the constructor but also to the `clone` method.
157160

158161
public constructor() {
@@ -202,6 +205,7 @@ export class Scope {
202205
newScope._propagationContext = { ...this._propagationContext };
203206
newScope._client = this._client;
204207
newScope._lastEventId = this._lastEventId;
208+
newScope._conversationId = this._conversationId;
205209

206210
_setSpanForScope(newScope, _getSpanForScope(this));
207211

@@ -284,6 +288,23 @@ export class Scope {
284288
return this._user;
285289
}
286290

291+
/**
292+
* Set the conversation ID for this scope.
293+
* Set to `null` to unset the conversation ID.
294+
*/
295+
public setConversationId(conversationId: string | null | undefined): this {
296+
this._conversationId = conversationId || undefined;
297+
this._notifyScopeListeners();
298+
return this;
299+
}
300+
301+
/**
302+
* Get the conversation ID from this scope.
303+
*/
304+
public getConversationId(): string | undefined {
305+
return this._conversationId;
306+
}
307+
287308
/**
288309
* Set an object that will be merged into existing tags on the scope,
289310
* and will be sent as tags data with the event.

packages/core/src/tracing/sentrySpan.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
TRACE_FLAG_SAMPLED,
3939
} from '../utils/spanUtils';
4040
import { timestampInSeconds } from '../utils/time';
41+
import { GEN_AI_CONVERSATION_ID_ATTRIBUTE } from './ai/gen-ai-attributes';
4142
import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
4243
import { logSpanEnd } from './logSpans';
4344
import { timedEventsToMeasurements } from './measurement';
@@ -221,6 +222,22 @@ export class SentrySpan implements Span {
221222
* use `spanToJSON(span)` instead.
222223
*/
223224
public getSpanJSON(): SpanJSON {
225+
// Automatically inject conversation ID from scope if not already set
226+
if (!this._attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]) {
227+
const capturedScopes = getCapturedScopesOnSpan(this);
228+
// Try isolation scope first (where setConversationId sets it)
229+
let conversationId = capturedScopes.isolationScope?.getConversationId();
230+
231+
// Fallback to regular scope
232+
if (!conversationId) {
233+
conversationId = capturedScopes.scope?.getConversationId();
234+
}
235+
236+
if (conversationId) {
237+
this._attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE] = conversationId;
238+
}
239+
}
240+
224241
return {
225242
data: this._attributes,
226243
description: this._name,

packages/core/src/utils/spanUtils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
88
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
99
} from '../semanticAttributes';
10+
import { GEN_AI_CONVERSATION_ID_ATTRIBUTE } from '../tracing/ai/gen-ai-attributes';
1011
import type { SentrySpan } from '../tracing/sentrySpan';
1112
import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus';
1213
import { getCapturedScopesOnSpan } from '../tracing/utils';
@@ -149,6 +150,28 @@ export function spanToJSON(span: Span): SpanJSON {
149150
// Handle a span from @opentelemetry/sdk-base-trace's `Span` class
150151
if (spanIsOpenTelemetrySdkTraceBaseSpan(span)) {
151152
const { attributes, startTime, name, endTime, status, links } = span;
153+
// Automatically inject conversation ID from scope if not already set
154+
if (!attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]) {
155+
// First try captured scopes (scopes at span creation time)
156+
const capturedScopes = getCapturedScopesOnSpan(span);
157+
// Try captured isolation scope first (where setConversationId sets it)
158+
let conversationId = capturedScopes.isolationScope?.getConversationId();
159+
160+
// Fallback to regular scope
161+
if (!conversationId) {
162+
conversationId = capturedScopes.scope?.getConversationId();
163+
}
164+
165+
// If not found in captured scopes, try current scopes (from AsyncLocalStorage)
166+
if (!conversationId) {
167+
const currentScope = getCurrentScope();
168+
conversationId = currentScope?.getConversationId();
169+
}
170+
171+
if (conversationId) {
172+
attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE] = conversationId;
173+
}
174+
}
152175

153176
// In preparation for the next major of OpenTelemetry, we want to support
154177
// looking up the parent span id according to the new API

packages/node/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ export {
8383
setTag,
8484
setTags,
8585
setUser,
86+
setConversationId,
87+
getConversationId,
8688
SEMANTIC_ATTRIBUTE_SENTRY_OP,
8789
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
8890
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,

0 commit comments

Comments
 (0)