Track multi-turn conversations by grouping traces with session IDs. Use withSpan directly from @arizeai/openinference-core - no wrappers or custom utilities needed.
Session Pattern:
- Generate a unique
session.idonce at application startup - Export SESSION_ID, import
withSpanwhere needed - Use
withSpanto create a parent CHAIN span withsession.idfor each interaction - All child spans (LLM, TOOL, AGENT, etc.) automatically group under the parent
- Query traces by
session.idin Phoenix to see all interactions
import { register } from "@arizeai/phoenix-otel";
import { randomUUID } from "node:crypto";
// Initialize Phoenix
register({
projectName: "your-app",
url: process.env.PHOENIX_COLLECTOR_ENDPOINT || "http://localhost:6006",
apiKey: process.env.PHOENIX_API_KEY,
batch: true,
});
// Generate and export session ID
export const SESSION_ID = randomUUID();import { withSpan } from "@arizeai/openinference-core";
import { SESSION_ID } from "./instrumentation";
// Use withSpan directly - no wrapper needed
const handleInteraction = withSpan(
async () => {
const result = await agent.generate({ prompt: userInput });
return result;
},
{
name: "cli.interaction",
kind: "CHAIN",
attributes: { "session.id": SESSION_ID },
}
);
// Call it
const result = await handleInteraction();const processQuery = withSpan(
async (query: string) => {
return await agent.generate({ prompt: query });
},
{
name: "process.query",
kind: "CHAIN",
attributes: { "session.id": SESSION_ID },
}
);
await processQuery("What is 2+2?");- CLI/Desktop Apps: Generate once at process startup
- Web Servers: Generate per-user session (e.g., on login, store in session storage)
- Stateless APIs: Accept session.id as a parameter from client
cli.interaction (CHAIN) ← session.id here
├── ai.generateText (AGENT)
│ ├── ai.generateText.doGenerate (LLM)
│ └── ai.toolCall (TOOL)
└── ai.generateText.doGenerate (LLM)
The session.id is only set on the root span. Child spans are automatically grouped by the trace hierarchy.
# Get all traces for a session
npx @arizeai/phoenix-cli traces \
--endpoint http://localhost:6006 \
--project your-app \
--format raw \
--no-progress | \
jq '.[] | select(.spans[0].attributes["session.id"] == "YOUR-SESSION-ID")'{
"dependencies": {
"@arizeai/openinference-core": "^2.0.5",
"@arizeai/phoenix-otel": "^0.4.1"
}
}Note: @opentelemetry/api is NOT needed - it's only for manual span management.
- Simple: Just export SESSION_ID, use withSpan directly - no wrappers
- Built-in:
withSpanfrom@arizeai/openinference-corehandles everything - Type-safe: Preserves function signatures and type information
- Automatic lifecycle: Handles span creation, error tracking, and cleanup
- Framework-agnostic: Works with any LLM framework (AI SDK, LangChain, etc.)
- No extra deps: Don't need
@opentelemetry/apior custom utilities
import { withSpan } from "@arizeai/openinference-core";
import { SESSION_ID } from "./instrumentation";
const handleWithContext = withSpan(
async (userInput: string) => {
return await agent.generate({ prompt: userInput });
},
{
name: "cli.interaction",
kind: "CHAIN",
attributes: {
"session.id": SESSION_ID,
"user.id": userId, // Track user
"metadata.environment": "prod", // Custom metadata
},
}
);❌ Don't do this:
// Unnecessary wrapper
export function withSessionTracking(fn) {
return withSpan(fn, { attributes: { "session.id": SESSION_ID } });
}✅ Do this instead:
// Use withSpan directly
import { withSpan } from "@arizeai/openinference-core";
import { SESSION_ID } from "./instrumentation";
const handler = withSpan(fn, {
attributes: { "session.id": SESSION_ID }
});For web servers or complex async flows where you need to propagate session IDs through middleware, you can use the Context API:
import { context } from "@opentelemetry/api";
import { setSession } from "@arizeai/openinference-core";
await context.with(
setSession(context.active(), { sessionId: "user_123_conv_456" }),
async () => {
const response = await llm.invoke(prompt);
}
);Use Context API when:
- Building web servers with middleware chains
- Session ID needs to flow through many async boundaries
- You don't control the call stack (e.g., framework-provided handlers)
Use withSpan when:
- Building CLI apps or scripts
- You control the function call points
- Simpler, more explicit code is preferred
fundamentals-universal-attributes.md- Other universal attributes (user.id, metadata)span-chain.md- CHAIN span specificationsessions-python.md- Python session tracking patterns