Skip to content

Commit f546c9a

Browse files
jerryliang64claude
andcommitted
fix(agent-tracing): separate traceId and sessionId in createSession
Previously createSession(sessionId) used the same value for both trace_id and thread_id in metadata. This caused trace_id to equal the Claude SDK thread_id instead of the server-side call chain traceId. Now createSession accepts { traceId, sessionId } options: - traceId: server-side trace ID for call chain linking (defaults to UUID) - sessionId: Claude SDK session ID, recorded as thread_id in metadata Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d169cab commit f546c9a

3 files changed

Lines changed: 77 additions & 12 deletions

File tree

core/agent-tracing/src/ClaudeAgentTracer.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type ClaudeContentBlock,
1313
type ClaudeTokenUsage,
1414
type IRunCost,
15+
type CreateSessionOptions,
1516
RunStatus,
1617
type TracerConfig,
1718
applyTracerConfig,
@@ -23,16 +24,18 @@ import {
2324
*/
2425
export class TraceSession {
2526
private traceId: string;
27+
private sessionId?: string;
2628
private rootRun: Run | null = null;
2729
private rootRunId: string;
2830
private startTime: number;
2931
private executionOrder = 2; // Start at 2, root is 1
3032
private pendingToolUses = new Map<string, Run>();
3133
private tracer: ClaudeAgentTracer;
3234

33-
constructor(tracer: ClaudeAgentTracer, sessionId?: string) {
35+
constructor(tracer: ClaudeAgentTracer, options?: CreateSessionOptions) {
3436
this.tracer = tracer;
35-
this.traceId = sessionId || randomUUID();
37+
this.traceId = options?.traceId || randomUUID();
38+
this.sessionId = options?.sessionId;
3639
this.rootRunId = randomUUID();
3740
this.startTime = Date.now();
3841
}
@@ -61,8 +64,11 @@ export class TraceSession {
6164
}
6265

6366
private handleInit(message: ClaudeMessage): void {
64-
this.traceId = message.session_id || this.traceId;
65-
this.rootRun = this.tracer.createRootRunInternal(message, this.startTime, this.traceId, this.rootRunId);
67+
// sessionId: prefer constructor option, fallback to init message's session_id
68+
if (!this.sessionId) {
69+
this.sessionId = message.session_id;
70+
}
71+
this.rootRun = this.tracer.createRootRunInternal(message, this.startTime, this.traceId, this.rootRunId, this.sessionId);
6672
this.tracer.logTrace(this.rootRun, RunStatus.START);
6773
}
6874

@@ -213,14 +219,17 @@ export class ClaudeAgentTracer {
213219
* Create a new trace session for streaming message processing.
214220
* Use this for real-time tracing where messages arrive one-by-one.
215221
*
222+
* @param options.traceId - Server-side trace ID for call chain linking. Defaults to a random UUID.
223+
* @param options.sessionId - Claude SDK session ID (thread_id), recorded in metadata.
224+
*
216225
* @example
217-
* const session = claudeTracer.createSession();
226+
* const session = claudeTracer.createSession({ traceId: ctx.tracer.traceId, sessionId: threadId });
218227
* for await (const message of agent.run('task')) {
219228
* await session.processMessage(message);
220229
* }
221230
*/
222-
public createSession(sessionId?: string): TraceSession {
223-
return new TraceSession(this, sessionId);
231+
public createSession(options?: CreateSessionOptions): TraceSession {
232+
return new TraceSession(this, options);
224233
}
225234

226235
/**
@@ -315,8 +324,9 @@ export class ClaudeAgentTracer {
315324
* @internal
316325
* Create root run from init message (used by TraceSession)
317326
*/
318-
createRootRunInternal(initMsg: ClaudeMessage, startTime: number, traceId: string, rootRunId?: string): Run {
327+
createRootRunInternal(initMsg: ClaudeMessage, startTime: number, traceId: string, rootRunId?: string, sessionId?: string): Run {
319328
const runId = rootRunId || initMsg.uuid || randomUUID();
329+
const threadId = sessionId || initMsg.session_id;
320330

321331
return {
322332
id: runId,
@@ -325,7 +335,7 @@ export class ClaudeAgentTracer {
325335
inputs: {
326336
tools: initMsg.tools || [],
327337
model: initMsg.model,
328-
session_id: initMsg.session_id,
338+
session_id: threadId,
329339
mcp_servers: initMsg.mcp_servers,
330340
agents: initMsg.agents,
331341
slash_commands: initMsg.slash_commands,
@@ -342,7 +352,7 @@ export class ClaudeAgentTracer {
342352
tags: [],
343353
extra: {
344354
metadata: {
345-
thread_id: initMsg.session_id,
355+
thread_id: threadId,
346356
},
347357
apiKeySource: initMsg.apiKeySource,
348358
claude_code_version: initMsg.claude_code_version,

core/agent-tracing/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ export const RunStatus = {
132132
} as const;
133133
export type RunStatus = (typeof RunStatus)[keyof typeof RunStatus];
134134

135+
/** Options for creating a new trace session. */
136+
export interface CreateSessionOptions {
137+
/** Server-side trace ID for linking to the request call chain. Defaults to a random UUID. */
138+
traceId?: string;
139+
/** Claude SDK session ID (thread_id), recorded in metadata. */
140+
sessionId?: string;
141+
}
142+
135143
/** User-facing config passed to tracer.configure() */
136144
export interface TracerConfig {
137145
agentName?: string;

core/agent-tracing/test/ClaudeAgentTracer.test.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,10 @@ describe('test/ClaudeAgentTracer.test.ts', () => {
212212
const toolEnd = toolRuns.find(e => e.status === RunStatus.END);
213213
assert(toolEnd, 'Should have tool end');
214214

215-
// All runs share the same trace_id = session_id
215+
// All runs share the same trace_id (auto-generated UUID, NOT session_id)
216216
const traceIds = new Set(capturedRuns.map(e => e.run.trace_id));
217217
assert.strictEqual(traceIds.size, 1, `All runs should share one trace_id, got ${traceIds.size}`);
218-
assert.strictEqual([ ...traceIds ][0], 'test-session-001', 'trace_id should match session_id');
218+
assert.notStrictEqual([ ...traceIds ][0], 'test-session-001', 'trace_id should NOT equal session_id');
219219

220220
// Root run should carry session_id as thread_id in extra.metadata
221221
const rootExtra = rootStart.run.extra as Record<string, any>;
@@ -240,6 +240,53 @@ describe('test/ClaudeAgentTracer.test.ts', () => {
240240
});
241241
});
242242

243+
describe('Separate traceId and sessionId', () => {
244+
it('should use provided traceId and record sessionId as thread_id in metadata', async () => {
245+
const { claudeTracer, capturedRuns } = createTestEnv();
246+
const session = claudeTracer.createSession({
247+
traceId: 'server-trace-abc',
248+
sessionId: 'my-thread-id',
249+
});
250+
251+
const messages: SDKMessage[] = [
252+
createMockInit(),
253+
createMockAssistantTextOnly(),
254+
createMockResult(),
255+
];
256+
257+
for (const msg of messages) {
258+
await session.processMessage(msg);
259+
}
260+
261+
// All runs should use the provided traceId
262+
const traceIds = new Set(capturedRuns.map(e => e.run.trace_id));
263+
assert.strictEqual(traceIds.size, 1);
264+
assert.strictEqual([ ...traceIds ][0], 'server-trace-abc', 'trace_id should be the server-side traceId');
265+
266+
// thread_id in metadata should be the sessionId, not the traceId
267+
const rootStart = capturedRuns.find(e => !e.run.parent_run_id && e.status === RunStatus.START);
268+
assert(rootStart);
269+
const rootExtra = rootStart.run.extra as Record<string, any>;
270+
assert.strictEqual(rootExtra?.metadata?.thread_id, 'my-thread-id', 'thread_id should be the sessionId');
271+
});
272+
273+
it('should fallback sessionId to init message session_id when not provided', async () => {
274+
const { claudeTracer, capturedRuns } = createTestEnv();
275+
const session = claudeTracer.createSession({ traceId: 'server-trace-xyz' });
276+
277+
await session.processMessage(createMockInit());
278+
await session.processMessage(createMockResult());
279+
280+
const rootStart = capturedRuns.find(e => !e.run.parent_run_id && e.status === RunStatus.START);
281+
assert(rootStart);
282+
const rootExtra = rootStart.run.extra as Record<string, any>;
283+
assert.strictEqual(rootExtra?.metadata?.thread_id, 'test-session-001', 'thread_id should fallback to init session_id');
284+
285+
const traceIds = new Set(capturedRuns.map(e => e.run.trace_id));
286+
assert.strictEqual([ ...traceIds ][0], 'server-trace-xyz');
287+
});
288+
});
289+
243290
describe('Batch mode + text-only', () => {
244291
it('should trace a text-only response via processMessages', async () => {
245292
const { claudeTracer, capturedRuns } = createTestEnv();

0 commit comments

Comments
 (0)