diff --git a/packages/sample-app/package.json b/packages/sample-app/package.json index 00eb5743..526df3ad 100644 --- a/packages/sample-app/package.json +++ b/packages/sample-app/package.json @@ -15,6 +15,7 @@ "run:gemini": "npm run build && node dist/src/vertexai/gemini.js", "run:palm2": "npm run build && node dist/src/vertexai/palm2.js", "run:decorators": "npm run build && node dist/src/sample_decorators.js", + "run:associations": "npm run build && node dist/src/sample_associations.js", "run:with": "npm run build && node dist/src/sample_with.js", "run:prompt_mgmt": "npm run build && node dist/src/sample_prompt_mgmt.js", "run:vercel": "npm run build && node dist/src/sample_vercel_ai.js", @@ -43,6 +44,7 @@ "run:mcp": "npm run build && node dist/src/sample_mcp.js", "run:mcp:real": "npm run build && node dist/src/sample_mcp_real.js", "run:mcp:working": "npm run build && node dist/src/sample_mcp_working.js", + "run:chatbot": "npm run build && node dist/src/sample_chatbot_interactive.js", "dev:image_generation": "pnpm --filter @traceloop/instrumentation-openai build && pnpm --filter @traceloop/node-server-sdk build && npm run build && node dist/src/sample_openai_image_generation.js", "lint": "eslint .", "lint:fix": "eslint . --fix" diff --git a/packages/sample-app/src/sample_associations.ts b/packages/sample-app/src/sample_associations.ts new file mode 100644 index 00000000..1e6b3cc9 --- /dev/null +++ b/packages/sample-app/src/sample_associations.ts @@ -0,0 +1,174 @@ +import * as traceloop from "@traceloop/node-server-sdk"; +import OpenAI from "openai"; + +// Initialize Traceloop +traceloop.initialize({ + appName: "associations_demo", + apiKey: process.env.TRACELOOP_API_KEY, + disableBatch: true, +}); + +const openai = new OpenAI(); + +/** + * Sample chatbot that demonstrates the Associations API. + * This example shows how to track users and sessions + * across multiple LLM interactions. + */ +class ChatbotWithAssociations { + constructor( + private userId: string, + private sessionId: string, + ) {} + + /** + * Process a multi-turn conversation with associations + */ + @traceloop.workflow({ name: "chatbot_conversation" }) + async handleConversation() { + console.log("\n=== Starting Chatbot Conversation ==="); + console.log(`User ID: ${this.userId}`); + console.log(`Session ID: ${this.sessionId}\n`); + + // Set standard associations at the beginning of the conversation + // These will be automatically attached to all spans within this context + traceloop.Associations.set([ + [traceloop.AssociationProperty.USER_ID, this.userId], + [traceloop.AssociationProperty.SESSION_ID, this.sessionId], + ]); + + // Use withAssociationProperties to add custom properties + // Custom properties (like chat_subject) will be prefixed with traceloop.association.properties + return traceloop.withAssociationProperties( + { chat_subject: "general" }, + async () => { + // First message + const greeting = await this.sendMessage( + "Hello! What's the weather like today?", + ); + console.log(`Bot: ${greeting}\n`); + + // Second message in the same conversation + const followup = await this.sendMessage( + "What should I wear for that weather?", + ); + console.log(`Bot: ${followup}\n`); + + // Third message + const final = await this.sendMessage("Thanks for the advice!"); + console.log(`Bot: ${final}\n`); + + return { + greeting, + followup, + final, + }; + }, + ); + } + + /** + * Send a single message - this is a task within the workflow + */ + @traceloop.task({ name: "send_message" }) + private async sendMessage(userMessage: string): Promise { + console.log(`User: ${userMessage}`); + + const completion = await openai.chat.completions.create({ + messages: [{ role: "user", content: userMessage }], + model: "gpt-3.5-turbo", + }); + + return completion.choices[0].message.content || "No response"; + } +} + +/** + * Simulate a customer service scenario with multiple customers + */ +async function customerServiceDemo() { + return traceloop.withWorkflow( + { name: "customer_service_scenario" }, + async () => { + console.log("\n=== Customer Service Scenario ===\n"); + + // Customer 1 + traceloop.Associations.set([ + [traceloop.AssociationProperty.CUSTOMER_ID, "cust-001"], + [traceloop.AssociationProperty.USER_ID, "agent-alice"], + ]); + + const customer1Response = await openai.chat.completions.create({ + messages: [ + { + role: "user", + content: "I need help with my order #12345", + }, + ], + model: "gpt-3.5-turbo", + }); + + console.log("Customer 1 (cust-001):"); + console.log( + `Response: ${customer1Response.choices[0].message.content}\n`, + ); + + // Customer 2 - Update associations for new customer + traceloop.Associations.set([ + [traceloop.AssociationProperty.CUSTOMER_ID, "cust-002"], + [traceloop.AssociationProperty.USER_ID, "agent-bob"], + ]); + + const customer2Response = await openai.chat.completions.create({ + messages: [ + { + role: "user", + content: "How do I return an item?", + }, + ], + model: "gpt-3.5-turbo", + }); + + console.log("Customer 2 (cust-002):"); + console.log( + `Response: ${customer2Response.choices[0].message.content}\n`, + ); + }, + ); +} + +/** + * Main execution + */ +async function main() { + console.log("============================================"); + console.log("Traceloop Associations API Demo"); + console.log("============================================"); + + try { + // Example 1: Multi-turn chatbot conversation with custom properties + const chatbot = new ChatbotWithAssociations( + "user-alice-456", // user_id + "session-xyz-789", // session_id + ); + + await chatbot.handleConversation(); + + // Example 2: Customer service with multiple customers + await customerServiceDemo(); + + console.log("\n=== Demo Complete ==="); + console.log( + "Check your Traceloop dashboard to see the associations attached to traces!", + ); + console.log( + "You can filter and search by user_id, session_id, customer_id, or custom properties like chat_subject.", + ); + } catch (error) { + console.error("Error running demo:", error); + process.exit(1); + } +} + +// Run the demo +main(); diff --git a/packages/sample-app/src/sample_chatbot_interactive.ts b/packages/sample-app/src/sample_chatbot_interactive.ts new file mode 100644 index 00000000..d5c8f30d --- /dev/null +++ b/packages/sample-app/src/sample_chatbot_interactive.ts @@ -0,0 +1,284 @@ +import * as traceloop from "@traceloop/node-server-sdk"; +import { openai } from "@ai-sdk/openai"; +import { streamText, CoreMessage, tool } from "ai"; +import * as readline from "readline"; +import { z } from "zod"; + +import "dotenv/config"; + +traceloop.initialize({ + appName: "sample_chatbot_interactive", + disableBatch: true, +}); + +// ANSI color codes for terminal output +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + dim: "\x1b[2m", + cyan: "\x1b[36m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", +}; + +class InteractiveChatbot { + private conversationHistory: CoreMessage[] = []; + private rl: readline.Interface; + private sessionId: string; + private userId: string; + + constructor() { + this.rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: `${colors.cyan}${colors.bright}You: ${colors.reset}`, + }); + // Generate unique IDs for this session + this.sessionId = `session-${Date.now()}`; + this.userId = `user-${Math.random().toString(36).substring(7)}`; + } + + @traceloop.task({ name: "summarize_interaction" }) + async generateSummary( + userMessage: string, + assistantResponse: string, + ): Promise { + console.log( + `\n${colors.yellow}▼ SUMMARY${colors.reset} ${colors.dim}TASK${colors.reset}`, + ); + + const summaryResult = await streamText({ + model: openai("gpt-4o-mini"), + messages: [ + { + role: "system", + content: + "Create a very brief title (3-6 words) that summarizes this conversation exchange. Only return the title, nothing else.", + }, + { + role: "user", + content: `User: ${userMessage}\n\nAssistant: ${assistantResponse}`, + }, + ], + experimental_telemetry: { isEnabled: true }, + }); + + let summary = ""; + for await (const chunk of summaryResult.textStream) { + summary += chunk; + } + + const cleanSummary = summary.trim().replace(/^["']|["']$/g, ""); + console.log(`${colors.dim}${cleanSummary}${colors.reset}`); + + return cleanSummary; + } + + @traceloop.workflow({ name: "chat_interaction" }) + async processMessage(userMessage: string): Promise { + // Set associations for tracing + traceloop.Associations.set([ + [traceloop.AssociationProperty.SESSION_ID, this.sessionId], + [traceloop.AssociationProperty.USER_ID, this.userId], + ]); + + // Add user message to history + this.conversationHistory.push({ + role: "user", + content: userMessage, + }); + + console.log(`\n${colors.green}${colors.bright}Assistant: ${colors.reset}`); + + // Stream the response + const result = await streamText({ + model: openai("gpt-4o"), + messages: [ + { + role: "system", + content: + "You are a helpful AI assistant with access to tools. Use the available tools when appropriate to provide accurate information. Provide clear, concise, and friendly responses.", + }, + ...this.conversationHistory, + ], + tools: { + calculator: tool({ + description: + "Perform mathematical calculations. Supports basic arithmetic operations.", + parameters: z.object({ + expression: z + .string() + .describe( + "The mathematical expression to evaluate (e.g., '2 + 2' or '10 * 5')", + ), + }), + execute: async ({ expression }) => { + try { + // Simple safe eval for basic math (only allow numbers and operators) + const sanitized = expression.replace(/[^0-9+\-*/().\s]/g, ""); + const result = eval(sanitized); + console.log( + `\n${colors.yellow}🔧 Calculator: ${expression} = ${result}${colors.reset}`, + ); + return { result, expression }; + } catch (error) { + return { error: "Invalid mathematical expression" }; + } + }, + }), + getCurrentWeather: tool({ + description: + "Get the current weather for a location. Use this when users ask about weather conditions.", + parameters: z.object({ + location: z + .string() + .describe("The city and country, e.g., 'London, UK'"), + }), + execute: async ({ location }) => { + console.log( + `\n${colors.yellow}🔧 Weather: Checking weather for ${location}${colors.reset}`, + ); + // Simulated weather data + const weatherConditions = [ + "sunny", + "cloudy", + "rainy", + "partly cloudy", + ]; + const condition = + weatherConditions[ + Math.floor(Math.random() * weatherConditions.length) + ]; + const temperature = Math.floor(Math.random() * 30) + 10; // 10-40°C + return { + location, + temperature: `${temperature}°C`, + condition, + humidity: `${Math.floor(Math.random() * 40) + 40}%`, + }; + }, + }), + getTime: tool({ + description: + "Get the current date and time. Use this when users ask about the current time or date.", + parameters: z.object({ + timezone: z + .string() + .optional() + .describe("Optional timezone (e.g., 'America/New_York')"), + }), + execute: async ({ timezone }) => { + const now = new Date(); + const options: Intl.DateTimeFormatOptions = { + timeZone: timezone, + dateStyle: "full", + timeStyle: "long", + }; + const formatted = now.toLocaleString("en-US", options); + console.log( + `\n${colors.yellow}🔧 Time: ${formatted}${colors.reset}`, + ); + return { + datetime: formatted, + timestamp: now.toISOString(), + timezone: timezone || "local", + }; + }, + }), + }, + maxSteps: 5, + experimental_telemetry: { isEnabled: true }, + }); + + let fullResponse = ""; + for await (const chunk of result.textStream) { + process.stdout.write(chunk); + fullResponse += chunk; + } + + console.log("\n"); + + // Wait for the full response to complete to get all messages including tool calls + const finalResult = await result.response; + + // Add all response messages (including tool calls and results) to history + // This ensures the conversation history includes the complete interaction + for (const message of finalResult.messages) { + this.conversationHistory.push(message); + } + + // Generate summary for this interaction + await this.generateSummary(userMessage, fullResponse); + + return fullResponse; + } + + clearHistory(): void { + this.conversationHistory = []; + console.log( + `\n${colors.magenta}✓ Conversation history cleared${colors.reset}\n`, + ); + } + + async start(): Promise { + console.log( + `${colors.bright}${colors.blue}╔════════════════════════════════════════════════════════════╗`, + ); + console.log( + `║ Interactive AI Chatbot with Traceloop ║`, + ); + console.log( + `╚════════════════════════════════════════════════════════════╝${colors.reset}\n`, + ); + console.log( + `${colors.dim}Commands: /exit (quit) | /clear (clear history)${colors.reset}\n`, + ); + + this.rl.prompt(); + + this.rl.on("line", async (input: string) => { + const trimmedInput = input.trim(); + + if (!trimmedInput) { + this.rl.prompt(); + return; + } + + if (trimmedInput === "/exit") { + console.log(`\n${colors.magenta}Goodbye! 👋${colors.reset}\n`); + this.rl.close(); + process.exit(0); + } + + if (trimmedInput === "/clear") { + this.clearHistory(); + this.rl.prompt(); + return; + } + + try { + await this.processMessage(trimmedInput); + } catch (error) { + console.error( + `\n${colors.bright}Error:${colors.reset} ${error instanceof Error ? error.message : String(error)}\n`, + ); + } + + this.rl.prompt(); + }); + + this.rl.on("close", () => { + console.log(`\n${colors.magenta}Goodbye! 👋${colors.reset}\n`); + process.exit(0); + }); + } +} + +async function main() { + const chatbot = new InteractiveChatbot(); + await chatbot.start(); +} + +main().catch(console.error); diff --git a/packages/traceloop-sdk/package.json b/packages/traceloop-sdk/package.json index d64d5c50..354715ae 100644 --- a/packages/traceloop-sdk/package.json +++ b/packages/traceloop-sdk/package.json @@ -57,6 +57,7 @@ "dependencies": { "@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0", "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.0.0", "@opentelemetry/core": "^2.0.1", "@opentelemetry/exporter-trace-otlp-proto": "^0.203.0", "@opentelemetry/instrumentation": "^0.203.0", diff --git a/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-allow-updating-associations-mid-workflow_3438992191/recording.har b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-allow-updating-associations-mid-workflow_3438992191/recording.har new file mode 100644 index 00000000..1f1160a3 --- /dev/null +++ b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-allow-updating-associations-mid-workflow_3438992191/recording.har @@ -0,0 +1,490 @@ +{ + "log": { + "_recordingName": "Test Associations API/should allow updating associations mid-workflow", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "b8201bbe07e5b4424a2de4e8d0641034", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 118, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "118" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v22.19.0" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 475, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"First message\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 567, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 567, + "text": "[\"H4sIAAAAAAAAAwAAAP//\",\"jFLBThsxEL3vV0x9TlDSFEhzQYgLSAiq9tBDhVZee3bj4vVY9jhthPLvlb2QXQqVetnDvHlv33uepwpAGC02INRWsuq9nV/5td9h1zf33+6/f212XxZ3a9fd+svUXiUxywxqfqLiF9aJot5bZENugFVAyZhVl+dnZ6vFp/PVsgA9abSZ1nmer05O55xCQ/PF8uPpM3NLRmEUG/hRAQA8lW/26DT+FhtYzF4mPcYoOxSb4xKACGTzRMgYTWTpWMxGUJFjdMX2NVpLH+CafoGSDm5gIMCeEjBpub+YEgO2Kcps3CVrJ4B0jljm4MXywzNyOJq01PlATfyLKlrjTNzWAWUklw1FJi8KeqgAHkoZ6VU+4QP1nmumRyy/+zyoibH9txgTSzuOl+vZO1q1RpbGxkmVQkm1RT0yx95l0oYmQDVJ/NbLe9pDauO6/5EfAaXQM+raB9RGvc47rgXMp/mvtWPDxbCIGHZGYc0GQ34Fja1MdjgaEfeRsa9b4zoMPphyOfkVq0P1BwAA//8DAN8UoaI4AwAA\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2025-12-21T08:42:12.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "drQBKJgNGAaQD9fx_Sh4yvCm1TL4Lnf7ea0tIbVnM.s-1766304732-1.0.1.1-t_a9BGMEbSboYtJot14QiUPavmOUtifHD6kZbgfGrP38t.kKEjvoSmX57xhbmfbFwS6tmmio91N.sScmmVaHaFoQB4aNu7FelgZnz1I8qJQ" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "VDjvnYY9_JyEWLpcxFbtOpeLdOgST3AsJ0UZj0xEc0w-1766304732304-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Sun, 21 Dec 2025 08:12:12 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-expose-headers", + "value": "X-Request-ID" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "683" + }, + { + "name": "openai-project", + "value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "702" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "10000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "50000000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "9999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "49999993" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "6ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "0s" + }, + { + "name": "x-request-id", + "value": "req_70b4720225c4462ab219ae7a16e4d6e6" + }, + { + "name": "x-openai-proxy-wasm", + "value": "v0.1" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=drQBKJgNGAaQD9fx_Sh4yvCm1TL4Lnf7ea0tIbVnM.s-1766304732-1.0.1.1-t_a9BGMEbSboYtJot14QiUPavmOUtifHD6kZbgfGrP38t.kKEjvoSmX57xhbmfbFwS6tmmio91N.sScmmVaHaFoQB4aNu7FelgZnz1I8qJQ; path=/; expires=Sun, 21-Dec-25 08:42:12 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=VDjvnYY9_JyEWLpcxFbtOpeLdOgST3AsJ0UZj0xEc0w-1766304732304-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "9b15e9bb78af3120-TLV" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1321, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-12-21T08:12:11.301Z", + "time": 873, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 873 + } + }, + { + "_id": "e3af0e6e5b79fe802c15b8d10a0a6ce7", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 119, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "119" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v22.19.0" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 475, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Second message\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 575, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 575, + "text": "[\"H4sIAAAAAAAAAwAAAP//\",\"jFLBbtswDL37KzidkyJplnbNZRhWoN2wWy8DhsJQJNrWJouKRK8zivz7IDmJnbUFdtGBj49675HPBYAwWmxAqEayar2df/Yf/NO3uNt9f+hvq4fVum+2X+8arpqw+yRmiUHbn6j4yLpQ1HqLbMgNsAooGdPU5fXV1Wrx/np1mYGWNNpEqz3PVxfrOXdhS/PF8nJ9YDZkFEaxgR8FAMBzfpNGp/GP2MBidqy0GKOsUWxOTQAikE0VIWM0kaVjMRtBRY7RZdn3aC2BrKVx7+CenkBJB19goEFPHTBp2X+c0gNWXZRJvuusnQDSOWKZ7Gfhjwdkf5JqqfaBtvEfqqiMM7EpA8pILsmKTF5kdF8APOZIujOXwgdqPZdMvzB/dzNME+MORmx5SEswsbST+pF0NqzUyNLYOElUKKka1CNzjF922tAEKCaWX4p5bfZg27j6f8aPgFLoGXXpA2qjzg2PbQHThb7Vdoo4CxYRw2+jsGSDIa1BYyU7O9yOiH1kbMvKuBqDDyYfUFpjsS/+AgAA//8DAPJCx24/AwAA\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2025-12-21T08:42:12.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "l85y3z7MNW9PxBfw4Kwk0qYEEb_Vqmnb10Z8Cd9A98g-1766304732-1.0.1.1-tlMQjujSxy08v8lBYRobWkK9NJtQVHKJ87a.vRTlbi4dFP4O9QDzCW.EsnH03L_pgIpA8Q0YhxqkFZmHr4sYaQfu0_2XVgAOeu4P3YAZfIA" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "sSypzEkvJTwON3Coh94tDZkUPtzkoQiIELhKYCEg1lQ-1766304732796-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Sun, 21 Dec 2025 08:12:12 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-expose-headers", + "value": "X-Request-ID" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "287" + }, + { + "name": "openai-project", + "value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "308" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "10000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "50000000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "9999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "49999994" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "6ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "0s" + }, + { + "name": "x-request-id", + "value": "req_80ce64d903724fd2802c8abc3924c0c9" + }, + { + "name": "x-openai-proxy-wasm", + "value": "v0.1" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=l85y3z7MNW9PxBfw4Kwk0qYEEb_Vqmnb10Z8Cd9A98g-1766304732-1.0.1.1-tlMQjujSxy08v8lBYRobWkK9NJtQVHKJ87a.vRTlbi4dFP4O9QDzCW.EsnH03L_pgIpA8Q0YhxqkFZmHr4sYaQfu0_2XVgAOeu4P3YAZfIA; path=/; expires=Sun, 21-Dec-25 08:42:12 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=sSypzEkvJTwON3Coh94tDZkUPtzkoQiIELhKYCEg1lQ-1766304732796-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "9b15e9c0fe2d3120-TLV" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1321, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-12-21T08:12:12.179Z", + "time": 492, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 492 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-merge-associations-from-Associations-set-and-withAssociationProperties_2272111199/recording.har b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-merge-associations-from-Associations-set-and-withAssociationProperties_2272111199/recording.har new file mode 100644 index 00000000..8354fa84 --- /dev/null +++ b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-merge-associations-from-Associations-set-and-withAssociationProperties_2272111199/recording.har @@ -0,0 +1,252 @@ +{ + "log": { + "_recordingName": "Test Associations API/should merge associations from Associations.set() and withAssociationProperties()", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "589055754a2a0ad0a24f03993fd04563", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 115, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "115" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v22.19.0" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 475, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Test merge\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 594, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 594, + "text": "[\"H4sIAAAAAAAAAwAAAP//\",\"jFJNb9swDL37V3A6J0U+lqbNZRgyYOmpl2GXoTAUibHVyaIg0W29Iv99kJzE7tYBu+jAx/f0HsnXAkAYLTYgVC1ZNd5Ot/7G/5rfVJ25/cK43t4xqs9P949fO/n9m5gkBu0fUfGZdaWo8RbZkOthFVAyJtX5+vp6Ofu4Xq4y0JBGm2iV5+nyajXlNuxpOpsvVidmTUZhFBv4UQAAvOY3eXQaX8QGZpNzpcEYZYVic2kCEIFsqggZo4ksHYvJACpyjC7b3qG19AF29AxKOriDngAdtcCkZQfPhmvgGqHBUCEwRv40Fgt4aKNMYVxr7QiQzhHLNIwc4+GEHC/GLVU+0D7+QRUH40ysy4AykksmI5MXGT0WAA95QO2bzMIHajyXTD8xf3fbq4lhIwM2X55AJpZ2qC8Wk3fESo0sjY2j+QolVY16YA7LkK02NAKKUeS/zbyn3cc2rvof+QFQCj2jLn1AbdTbwENbwHSv/2q7jDgbFhHDk1FYssGQ1qDxIFvbX5KIXWRsyoNxFQYfTD6ntMbiWPwGAAD//w==\",\"AwD98dtiTQMAAA==\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2025-12-21T08:42:15.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "KJUjBzo7Dsrcvl_dw4Q7swXJF0np7yCFp1zjKkmrUhA-1766304735-1.0.1.1-8vnqK4zxhftvMjQA9a6mjZ.SHUUudovr5.oiwis5RWgiV2IsuOhnYmlHqnNURmePzT.NyyeKtAez7INaY9Kqq4W2Lll2qcTqg3BCLV0.5FM" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "_kEzHtvd4Vo2ruw2lqo_P_HFcY2i_.i7npO7g2S2rvU-1766304735907-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Sun, 21 Dec 2025 08:12:15 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-expose-headers", + "value": "X-Request-ID" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "719" + }, + { + "name": "openai-project", + "value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "738" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "10000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "50000000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "9999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "49999995" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "6ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "0s" + }, + { + "name": "x-request-id", + "value": "req_428744323ddb46bda6f34756c1ed5767" + }, + { + "name": "x-openai-proxy-wasm", + "value": "v0.1" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=KJUjBzo7Dsrcvl_dw4Q7swXJF0np7yCFp1zjKkmrUhA-1766304735-1.0.1.1-8vnqK4zxhftvMjQA9a6mjZ.SHUUudovr5.oiwis5RWgiV2IsuOhnYmlHqnNURmePzT.NyyeKtAez7INaY9Kqq4W2Lll2qcTqg3BCLV0.5FM; path=/; expires=Sun, 21-Dec-25 08:42:15 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=_kEzHtvd4Vo2ruw2lqo_P_HFcY2i_.i7npO7g2S2rvU-1766304735907-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "9b15e9d1cd883120-TLV" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1321, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-12-21T08:12:14.863Z", + "time": 915, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 915 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-propagate-associations-to-all-child-spans_3781439892/recording.har b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-propagate-associations-to-all-child-spans_3781439892/recording.har new file mode 100644 index 00000000..200adf55 --- /dev/null +++ b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-propagate-associations-to-all-child-spans_3781439892/recording.har @@ -0,0 +1,252 @@ +{ + "log": { + "_recordingName": "Test Associations API/should propagate associations to all child spans", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "63d8627010f393d41028514b6da29bb3", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 123, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "123" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v22.19.0" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 475, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Child task message\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 579, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 579, + "text": "[\"H4sIAAAAAAAAAwAAAP//\",\"jFLLbtswELzrK7Y824EV51H4EqBGkRRpv6AIBJpcWawpLsNd1TUC/3tB+SG5DyAXHnZ2ljOz+1YAKGfVApRptJg2+ukyfoy78nVTPn3lx9fPtX9+/GK/beP2k1k+q0lm0OoHGjmxrgy10aM4CgfYJNSCeWp5f3c3n93cz296oCWLPtPWUabzq9updGlF01l5fXtkNuQMslrA9wIA4K1/s8Zg8ZdawGxyqrTIrNeoFucmAJXI54rSzI5FB1GTATQUBEMv+wm9pw+w1AF21EH0qBmhQR+hRdg6aUAaxyCaNw/jGQnrjnX2EDrvR4AOgUTnDHr1L0dkf9braR0TrfgPqqpdcNxUCTVTyNpYKKoe3RcAL30u3YVVFRO1USqhDfbflcdY1LCJEVgeQSHRfqhfn+oX0yqLop3nUa7KaNOgHZjDEnRnHY2AYuT5bzH/mn3w7cL6PeMHwBiMgraKCa0zl4aHtoT5Tv/Xds64F6wY009nsBKHKe/BYq07f7ggxTsWbKvahTWmmFx/RnmPxb74DQAA//8DAMn4bMZFAwAA\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2025-12-21T08:42:14.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "tdhwk6A4993pgei3vC4TMyw..OHxv5LlEVoncpvJiCw-1766304734-1.0.1.1-0Q6dipDEB.6k3wF6ZVE_WXQv0eAuwI56rC.PaK5tNpkdzE0bHTLCkVv1ImN4WERZjB.bl3Zjw_cr1.WOloR6qy6moqPfO5Veo6Ws242Dluo" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "2hMhu8GIIzuEDZgdnwraFL7CHWtgrCBOTsg8QebSapE-1766304734979-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Sun, 21 Dec 2025 08:12:14 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-expose-headers", + "value": "X-Request-ID" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "314" + }, + { + "name": "openai-project", + "value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "340" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "10000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "50000000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "9999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "49999993" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "6ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "0s" + }, + { + "name": "x-request-id", + "value": "req_847ecbd28c8d473986aef4100921c5a8" + }, + { + "name": "x-openai-proxy-wasm", + "value": "v0.1" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=tdhwk6A4993pgei3vC4TMyw..OHxv5LlEVoncpvJiCw-1766304734-1.0.1.1-0Q6dipDEB.6k3wF6ZVE_WXQv0eAuwI56rC.PaK5tNpkdzE0bHTLCkVv1ImN4WERZjB.bl3Zjw_cr1.WOloR6qy6moqPfO5Veo6Ws242Dluo; path=/; expires=Sun, 21-Dec-25 08:42:14 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=2hMhu8GIIzuEDZgdnwraFL7CHWtgrCBOTsg8QebSapE-1766304734979-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "9b15e9ce7ae73120-TLV" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1321, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-12-21T08:12:14.334Z", + "time": 521, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 521 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-set-multiple-associations-on-spans_3842784822/recording.har b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-set-multiple-associations-on-spans_3842784822/recording.har new file mode 100644 index 00000000..5c8a6293 --- /dev/null +++ b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-set-multiple-associations-on-spans_3842784822/recording.har @@ -0,0 +1,252 @@ +{ + "log": { + "_recordingName": "Test Associations API/should set multiple associations on spans", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "aad89c290c4c49a352550af1f3ee34d3", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 119, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "119" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v22.19.0" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 475, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Tell me a fact\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 686, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 686, + "text": "[\"H4sIAAAAAAAAAwAAAP//\",\"jFNNb9swDL3nVxA6J0HTLO2W2zBgxdDLMHQDhrUwZJm2ucqiINLpmiL/fbCSxck+gF184ON7Jt+jXiYAhiqzBuNaq66LfvYuvo7959s73HS3z+/d18vtzZeSPm4/3DRRzHRgcPkdnf5izR130aMShz3sElrFQXVxfXW1vHh1vbzIQMcV+oHWRJ0t56uZ9qnk2cXicnVgtkwOxazh2wQA4CV/hxlDhT/MGrJOrnQoYhs062MTgEnsh4qxIiRqg5rpCDoOiiGPfdciROt6qEnaKXCtGCBhjSlhBcpgBbRFuDel9R5cr4rp3hzaSeAx8FOAmhOQCkhrUwRF1BZsqKC1JSlwDSUphQa4rrOcoig5jzJgnfUI8kRdh0mAQu5429ktB/hEG0zz0+ET1r3YwbzQe38C2BBY7WB+tu3hgOyORnluYuJSfqOamgJJWyS0wmEwRZSjyehuAvCQA+nPPDYxcRe1UH7E/LvFYi9nxhMYweWbA6is1o/11SHEc7WiQrXk5SRQ46xrsRqZY/q2r4hPgMnJzn8O8zft/d4Umv+RHwHnMCpWRUxYkTtfeGxLODyQf7UdPc4DG8G0IYeFEqYhhwpr2/v96Rp5FsWuqCk0mGKifL9DjpPd5CcAAAD//w==\",\"AwDQ8+nOvgMAAA==\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2025-12-21T08:42:11.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "B1BMLT_q7zMT0idIrO2eGScOPXTM52s8Wf0fr4JaDuA-1766304731-1.0.1.1-MsQDbQVW2CgDOl9coYCKtKVztnCWqB7UHPtMVsFrms4gffsMMmdZn.3Ep4a9tboSJgizgb7DUYCezESnxFoag5_1M4Vzyxhiz0tmRIieMJc" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "SzvXQOANAG_KoWCMpsxs27nZ0V8VshPzKTnjF_MSYCU-1766304731424-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Sun, 21 Dec 2025 08:12:11 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-expose-headers", + "value": "X-Request-ID" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "770" + }, + { + "name": "openai-project", + "value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "788" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "10000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "50000000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "9999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "49999993" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "6ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "0s" + }, + { + "name": "x-request-id", + "value": "req_f613030df3ae44389bebfcf6fe36da91" + }, + { + "name": "x-openai-proxy-wasm", + "value": "v0.1" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=B1BMLT_q7zMT0idIrO2eGScOPXTM52s8Wf0fr4JaDuA-1766304731-1.0.1.1-MsQDbQVW2CgDOl9coYCKtKVztnCWqB7UHPtMVsFrms4gffsMMmdZn.3Ep4a9tboSJgizgb7DUYCezESnxFoag5_1M4Vzyxhiz0tmRIieMJc; path=/; expires=Sun, 21-Dec-25 08:42:11 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=SzvXQOANAG_KoWCMpsxs27nZ0V8VshPzKTnjF_MSYCU-1766304731424-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "9b15e9b55aad3120-TLV" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1321, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-12-21T08:12:10.318Z", + "time": 977, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 977 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-set-single-association-on-spans_3887215347/recording.har b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-set-single-association-on-spans_3887215347/recording.har new file mode 100644 index 00000000..53b24cb5 --- /dev/null +++ b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-set-single-association-on-spans_3887215347/recording.har @@ -0,0 +1,252 @@ +{ + "log": { + "_recordingName": "Test Associations API/should set single association on spans", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "fd40a5569f36530000ae54cd2be33f61", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 119, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "119" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v22.19.0" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 475, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Tell me a joke\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 610, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 610, + "text": "[\"H4sIAAAAAAAAAwAAAP//\",\"jFLBbhMxEL3vVww+J1XS0LTKBQlu0AsHBAhVq4k92R3qtS173LSq8u/ITshuoUhcfJg37/m9mXluABQbtQGlexQ9BDv/EG5C5s9xteBP+cvtevv4XS++PWTd3X+8VbPC8NufpOU360L7IVgS9u4I60goVFSX1+v1avH2erWowOAN2ULrgsxXF1dzyXHr54vl5dWJ2XvWlNQGfjQAAM/1LR6doUe1gapTKwOlhB2pzbkJQEVvS0VhSpwEnajZCGrvhFy1/bV/AsMGpCdIGiPp6PewZwfoAPcYzTt4TxpzIugJ9pjAZymKhl0H7KDnBDsma95Mv4i0ywlLRJetnQDonBcsI6rh7k7I4RzH+i5Ev01/UNWOHae+jYTJu2I9iQ+qoocG4K6OLb+YhArRD0Fa8fdUv1suj3JqXNQEvD6B4gXtWL+8mb2i1hoSZJsmY1cadU9mZI47wmzYT4BmkvlvM69pH3Oz6/5HfgS0piBk2hDJsH4ZeGyLVM74X23nGVfDKlF8YE2tMMWyB0M7zPZ4YCo9JaGh3bHrKIbI9crKHptD8wsAAP//\",\"AwBD1GbaZAMAAA==\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2025-12-21T08:42:10.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "ZzRza1aEuHO9cVzRkHd6mpdGea7.3G8.d4IH2o6KZrs-1766304730-1.0.1.1-DbIp.Oy_SLvokBhLUif.WRPuTZYxCu2V_jJDbJeNVFWb9FfS9j.ljPMdHQBdHkrX6TkweOLdUIwiLRrtJkHioP7VIRRBIHkWQjaDEz.HYug" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "XPfe5DDAEfiuv6VfH4DKHkroWn1jOONENTK.UTDSWoY-1766304730436-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Sun, 21 Dec 2025 08:12:10 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-expose-headers", + "value": "X-Request-ID" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "248" + }, + { + "name": "openai-project", + "value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "268" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "10000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "50000000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "9999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "49999993" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "6ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "0s" + }, + { + "name": "x-request-id", + "value": "req_d335f511512c4eeb96259b5aa46677df" + }, + { + "name": "x-openai-proxy-wasm", + "value": "v0.1" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=ZzRza1aEuHO9cVzRkHd6mpdGea7.3G8.d4IH2o6KZrs-1766304730-1.0.1.1-DbIp.Oy_SLvokBhLUif.WRPuTZYxCu2V_jJDbJeNVFWb9FfS9j.ljPMdHQBdHkrX6TkweOLdUIwiLRrtJkHioP7VIRRBIHkWQjaDEz.HYug; path=/; expires=Sun, 21-Dec-25 08:42:10 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=XPfe5DDAEfiuv6VfH4DKHkroWn1jOONENTK.UTDSWoY-1766304730436-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "9b15e9b288503120-TLV" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1321, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-12-21T08:12:09.832Z", + "time": 477, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 477 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-work-with-all-AssociationProperty-types_111443069/recording.har b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-work-with-all-AssociationProperty-types_111443069/recording.har new file mode 100644 index 00000000..68bc023c --- /dev/null +++ b/packages/traceloop-sdk/recordings/Test-Associations-API_2277442331/should-work-with-all-AssociationProperty-types_111443069/recording.har @@ -0,0 +1,252 @@ +{ + "log": { + "_recordingName": "Test Associations API/should work with all AssociationProperty types", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "86b47881e1313cfde7039fb11eebc151", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 124, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "content-length", + "value": "124" + }, + { + "_fromType": "array", + "name": "accept", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "OpenAI/JS 4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-lang", + "value": "js" + }, + { + "_fromType": "array", + "name": "x-stainless-package-version", + "value": "4.38.3" + }, + { + "_fromType": "array", + "name": "x-stainless-os", + "value": "MacOS" + }, + { + "_fromType": "array", + "name": "x-stainless-arch", + "value": "arm64" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime", + "value": "node" + }, + { + "_fromType": "array", + "name": "x-stainless-runtime-version", + "value": "v22.19.0" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "name": "host", + "value": "api.openai.com" + } + ], + "headersSize": 475, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Test all properties\"\n }\n ],\n \"model\": \"gpt-3.5-turbo\"\n}" + }, + "queryString": [], + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "bodySize": 1202, + "content": { + "encoding": "base64", + "mimeType": "application/json", + "size": 1202, + "text": "[\"H4sIAAAAAAAAAwAAAP//\",\"nFVNj9w2DL3PryB89g72M2nmuihatGibAgF6aIIBLdNjtrKoiPTuToL974XkmbG36SLZXgZjPT3xURQfP68AKm6rDVSuR3ND9Ge38bt4n34mefP7w8P++ofbPwLvPv163fFPb11VZ4Y0f5GzI2vtZIiejCVMsEuERvnUi9evXl2dX7++uizAIC35TNtFO7ta35zZmBo5O7+4vDkwe2FHWm3gzxUAwOfymzWGlh6qDZzXx5WBVHFH1ea0CaBK4vNKhaqshsGqegadBKNQZHMA6wnuJfn2fXgf3vWUCDARNOw9S1CQDmKSSMmYFJaEGjC0wAaswEMUVW48gQkYqeV9A6D3a/hR7umOUg0qA4GTYZCwPNR6NHAYoKFCpRY4OD+2tMmiLtbwtt8rO/QL1gbe9aR03AlolrgZjRR0dD2gghMvOSp/ohq0x0g1GD3YmGjSfk+86219OGmh6CCGUNnvj5qaPUijlO4wlxgkwUCoY6KBgq2z0ss13PY0vFxpInTGd2z7GjqPw4AN+/KFjtvpT2hBxY8T8Lzmg1jrk4y7HtxRzxSiVDQBBvR7ZS2qr9bwC7kew8t1qyUKO+tr6DG1gVRrII9q7E6iLevI0DdrHmY1GeGwg4Gsl3YObBSUPZ1wSScFx7WS23UJmoaXP53QjqeS0EPEoCzhUIZIjjt20BPaV7Nq9qBjMYosNHfPgEaJc3YCLXcdJQoGRkOkhPl1lvA8VSsHnN5ZprPlx6JRglJJ8GYN33tylv5PeyxzTJSt4vCRgzqM6NgwOPpqjqNmcTQLOdaFPo4cT/3x2x0l9L6G+z4Xb7KOIAZfmAd6/7ztzJaTsrctHQcSebrLiwu2dIDzpUvK9dCSWKZSyD1cbvbjiLm5piKPbIcuhE5SwTkbZ1vypfXSUhN1o2K29DB6vwAwBLHiF8XMPxyQx5N9e9nFJI3+i1p1HFj7bSJUCdmq1SRWBX1cAXwoY2J84vxVTDJE25r8TSXcxWFKVPNgmsHL68sDamLoF8DNEXhy3rYlQ/a6GDSVQ9dTO1PnqYRjy7IAVousv5TzX2dPmXPYfcvxM+AcRaN2GxO17J6mPG9LlPvxuW2nWy6Cq2z57GhrTClXoqUORz+N1Er3ajRsOw47SjFxmau5kqvH1T8AAAD//w==\",\"AwD3pwbhVggAAA==\"]" + }, + "cookies": [ + { + "domain": ".api.openai.com", + "expires": "2025-12-21T08:42:14.000Z", + "httpOnly": true, + "name": "__cf_bm", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "LXl7YIkHSftn90NVEMYr9u6WFrNiB2unH2IfC0lAnuU-1766304734-1.0.1.1-L8ZO_bqTrLcFXCTsY2cEM2QUT_ibEzq3v1WOwJqF8SExpgHfC9f87tgPHFF2sZ5GFqwPeFIqZoLAR8zx_rsl1pEosKCQBP4q7leynQ5AuOE" + }, + { + "domain": ".api.openai.com", + "httpOnly": true, + "name": "_cfuvid", + "path": "/", + "sameSite": "None", + "secure": true, + "value": "rteD6Fb73g85ujX.BasqlUzcnlFu6Wcm76q2AAEuPd4-1766304734441-0.0.1.1-604800000" + } + ], + "headers": [ + { + "name": "date", + "value": "Sun, 21 Dec 2025 08:12:14 GMT" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "transfer-encoding", + "value": "chunked" + }, + { + "name": "connection", + "value": "keep-alive" + }, + { + "name": "access-control-expose-headers", + "value": "X-Request-ID" + }, + { + "name": "openai-organization", + "value": "traceloop" + }, + { + "name": "openai-processing-ms", + "value": "1434" + }, + { + "name": "openai-project", + "value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa" + }, + { + "name": "openai-version", + "value": "2020-10-01" + }, + { + "name": "x-envoy-upstream-service-time", + "value": "1453" + }, + { + "name": "x-ratelimit-limit-requests", + "value": "10000" + }, + { + "name": "x-ratelimit-limit-tokens", + "value": "50000000" + }, + { + "name": "x-ratelimit-remaining-requests", + "value": "9999" + }, + { + "name": "x-ratelimit-remaining-tokens", + "value": "49999992" + }, + { + "name": "x-ratelimit-reset-requests", + "value": "6ms" + }, + { + "name": "x-ratelimit-reset-tokens", + "value": "0s" + }, + { + "name": "x-request-id", + "value": "req_9c41fddbe63f4a14a3729d050b59ef8c" + }, + { + "name": "x-openai-proxy-wasm", + "value": "v0.1" + }, + { + "name": "cf-cache-status", + "value": "DYNAMIC" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "__cf_bm=LXl7YIkHSftn90NVEMYr9u6WFrNiB2unH2IfC0lAnuU-1766304734-1.0.1.1-L8ZO_bqTrLcFXCTsY2cEM2QUT_ibEzq3v1WOwJqF8SExpgHfC9f87tgPHFF2sZ5GFqwPeFIqZoLAR8zx_rsl1pEosKCQBP4q7leynQ5AuOE; path=/; expires=Sun, 21-Dec-25 08:42:14 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "_cfuvid=rteD6Fb73g85ujX.BasqlUzcnlFu6Wcm76q2AAEuPd4-1766304734441-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000; includeSubDomains; preload" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "server", + "value": "cloudflare" + }, + { + "name": "cf-ray", + "value": "9b15e9c419a23120-TLV" + }, + { + "name": "content-encoding", + "value": "gzip" + }, + { + "name": "alt-svc", + "value": "h3=\":443\"; ma=86400" + } + ], + "headersSize": 1323, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-12-21T08:12:12.680Z", + "time": 1636, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 1636 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/packages/traceloop-sdk/src/lib/node-server-sdk.ts b/packages/traceloop-sdk/src/lib/node-server-sdk.ts index 4236a04c..997f760d 100644 --- a/packages/traceloop-sdk/src/lib/node-server-sdk.ts +++ b/packages/traceloop-sdk/src/lib/node-server-sdk.ts @@ -64,6 +64,7 @@ export { getTraceloopTracer } from "./tracing/tracing"; export * from "./tracing/decorators"; export * from "./tracing/manual"; export * from "./tracing/association"; +export * from "./tracing/associations"; export * from "./tracing/custom-metric"; export * from "./tracing/span-processor"; export * from "./prompts"; diff --git a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts index 2dd604c5..66a73f42 100644 --- a/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts +++ b/packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts @@ -6,7 +6,6 @@ import { import { ATTR_GEN_AI_AGENT_NAME, ATTR_GEN_AI_COMPLETION, - ATTR_GEN_AI_CONVERSATION_ID, ATTR_GEN_AI_INPUT_MESSAGES, ATTR_GEN_AI_OPERATION_NAME, ATTR_GEN_AI_OUTPUT_MESSAGES, @@ -565,17 +564,6 @@ const transformToolCallAttributes = (attributes: Record): void => { } }; -const transformConversationId = (attributes: Record): void => { - const conversationId = attributes["ai.telemetry.metadata.conversationId"]; - const sessionId = attributes["ai.telemetry.metadata.sessionId"]; - - if (conversationId) { - attributes[ATTR_GEN_AI_CONVERSATION_ID] = conversationId; - } else if (sessionId) { - attributes[ATTR_GEN_AI_CONVERSATION_ID] = sessionId; - } -}; - const transformResponseMetadata = (attributes: Record): void => { const AI_RESPONSE_MODEL = "ai.response.model"; const AI_RESPONSE_ID = "ai.response.id"; @@ -686,7 +674,6 @@ export const transformLLMSpans = ( transformResponseMetadata(attributes); calculateTotalTokens(attributes); transformVendor(attributes); // Also sets GEN_AI_PROVIDER_NAME - transformConversationId(attributes); transformToolCallAttributes(attributes); transformTelemetryMetadata(attributes, spanName); }; diff --git a/packages/traceloop-sdk/src/lib/tracing/association.ts b/packages/traceloop-sdk/src/lib/tracing/association.ts index 07f6c850..5cb5e803 100644 --- a/packages/traceloop-sdk/src/lib/tracing/association.ts +++ b/packages/traceloop-sdk/src/lib/tracing/association.ts @@ -1,5 +1,5 @@ import { context } from "@opentelemetry/api"; -import { ASSOCATION_PROPERTIES_KEY } from "./tracing"; +import { ASSOCIATION_PROPERTIES_KEY } from "./tracing"; export function withAssociationProperties< A extends unknown[], @@ -14,8 +14,16 @@ export function withAssociationProperties< return fn.apply(thisArg, args); } + // Get existing associations from context and merge with new properties + const existingAssociations = context + .active() + .getValue(ASSOCIATION_PROPERTIES_KEY) as Record | undefined; + const mergedAssociations = existingAssociations + ? { ...existingAssociations, ...properties } + : properties; + const newContext = context .active() - .setValue(ASSOCATION_PROPERTIES_KEY, properties); + .setValue(ASSOCIATION_PROPERTIES_KEY, mergedAssociations); return context.with(newContext, fn, thisArg, ...args); } diff --git a/packages/traceloop-sdk/src/lib/tracing/associations.ts b/packages/traceloop-sdk/src/lib/tracing/associations.ts new file mode 100644 index 00000000..ecae7c48 --- /dev/null +++ b/packages/traceloop-sdk/src/lib/tracing/associations.ts @@ -0,0 +1,89 @@ +import { trace, context as otelContext } from "@opentelemetry/api"; +import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks"; +import { ASSOCIATION_PROPERTIES_KEY } from "./tracing"; + +/** + * Standard association properties for tracing. + */ +export enum AssociationProperty { + CUSTOMER_ID = "customer_id", + USER_ID = "user_id", + SESSION_ID = "session_id", +} + +/** + * Set of standard association property keys (without prefix). + * Use this to check if a property should be set directly or with the TRACELOOP_ASSOCIATION_PROPERTIES prefix. + */ +export const STANDARD_ASSOCIATION_PROPERTIES = new Set( + Object.values(AssociationProperty), +); + +/** + * Type alias for a single association + */ +export type Association = [AssociationProperty, string]; + +/** + * Class for managing trace associations. + */ +export class Associations { + /** + * Set associations that will be added directly to all spans in the current context. + * + * @param associations - An array of [property, value] tuples + * + * @example + * // Single association + * Associations.set([[AssociationProperty.SESSION_ID, "session-123"]]); + * + * // Multiple associations + * Associations.set([ + * [AssociationProperty.USER_ID, "user-456"], + * [AssociationProperty.SESSION_ID, "session-789"] + * ]); + */ + static set(associations: Association[]): void { + // Get current associations from context or create empty object + const existingAssociations = otelContext + .active() + .getValue(ASSOCIATION_PROPERTIES_KEY) as + | Record + | undefined; + const currentAssociations: Record = existingAssociations + ? { ...existingAssociations } + : {}; + + // Update associations with new values + for (const [prop, value] of associations) { + currentAssociations[prop] = value; + } + + // Store associations in context + const newContext = otelContext + .active() + .setValue(ASSOCIATION_PROPERTIES_KEY, currentAssociations); + + // Set the new context as active using the context manager + // This is the equivalent of Python's attach(set_value(...)) + const contextManager = (otelContext as any)["_getContextManager"](); + if ( + contextManager && + contextManager instanceof AsyncLocalStorageContextManager + ) { + // For AsyncLocalStorageContextManager, we need to use the internal _asyncLocalStorage + const storage = (contextManager as any)._asyncLocalStorage; + if (storage) { + storage.enterWith(newContext); + } + } + + // Also set directly on the current span (use newContext after enterWith) + const span = trace.getSpan(newContext); + if (span && span.isRecording()) { + for (const [prop, value] of associations) { + span.setAttribute(prop, value); + } + } + } +} diff --git a/packages/traceloop-sdk/src/lib/tracing/decorators.ts b/packages/traceloop-sdk/src/lib/tracing/decorators.ts index aaf0f02b..3eeddcb5 100644 --- a/packages/traceloop-sdk/src/lib/tracing/decorators.ts +++ b/packages/traceloop-sdk/src/lib/tracing/decorators.ts @@ -2,7 +2,7 @@ import { Span, context } from "@opentelemetry/api"; import { suppressTracing } from "@opentelemetry/core"; import { AGENT_NAME_KEY, - ASSOCATION_PROPERTIES_KEY, + ASSOCIATION_PROPERTIES_KEY, ENTITY_NAME_KEY, getEntityPath, getTracer, @@ -71,7 +71,7 @@ function withEntity< } if (associationProperties) { entityContext = entityContext.setValue( - ASSOCATION_PROPERTIES_KEY, + ASSOCIATION_PROPERTIES_KEY, associationProperties, ); } diff --git a/packages/traceloop-sdk/src/lib/tracing/span-processor.ts b/packages/traceloop-sdk/src/lib/tracing/span-processor.ts index 252b86ff..ba48af76 100644 --- a/packages/traceloop-sdk/src/lib/tracing/span-processor.ts +++ b/packages/traceloop-sdk/src/lib/tracing/span-processor.ts @@ -9,12 +9,13 @@ import { context } from "@opentelemetry/api"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; import { SpanExporter } from "@opentelemetry/sdk-trace-base"; import { - ASSOCATION_PROPERTIES_KEY, + ASSOCIATION_PROPERTIES_KEY, ENTITY_NAME_KEY, WORKFLOW_NAME_KEY, AGENT_NAME_KEY, } from "./tracing"; import { SpanAttributes } from "@traceloop/ai-semantic-conventions"; +import { STANDARD_ASSOCIATION_PROPERTIES } from "./associations"; import { ATTR_GEN_AI_AGENT_NAME } from "@opentelemetry/semantic-conventions/incubating"; import { transformAiSdkSpanAttributes, @@ -197,13 +198,19 @@ const onSpanStart = (span: Span): void => { const associationProperties = context .active() - .getValue(ASSOCATION_PROPERTIES_KEY); + .getValue(ASSOCIATION_PROPERTIES_KEY); if (associationProperties) { for (const [key, value] of Object.entries(associationProperties)) { - span.setAttribute( - `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${key}`, - value, - ); + // Standard association properties are set without prefix on all spans + if (STANDARD_ASSOCIATION_PROPERTIES.has(key)) { + span.setAttribute(key, value); + } else { + // Custom properties use the TRACELOOP_ASSOCIATION_PROPERTIES prefix + span.setAttribute( + `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${key}`, + value, + ); + } } } diff --git a/packages/traceloop-sdk/src/lib/tracing/tracing.ts b/packages/traceloop-sdk/src/lib/tracing/tracing.ts index 5bb79951..8bbcc61b 100644 --- a/packages/traceloop-sdk/src/lib/tracing/tracing.ts +++ b/packages/traceloop-sdk/src/lib/tracing/tracing.ts @@ -4,7 +4,7 @@ const TRACER_NAME = "@traceloop/node-server-sdk"; export const WORKFLOW_NAME_KEY = createContextKey("workflow_name"); export const ENTITY_NAME_KEY = createContextKey("entity_name"); export const AGENT_NAME_KEY = createContextKey("agent_name"); -export const ASSOCATION_PROPERTIES_KEY = createContextKey( +export const ASSOCIATION_PROPERTIES_KEY = createContextKey( "association_properties", ); diff --git a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts index 178c0de4..d288859b 100644 --- a/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts +++ b/packages/traceloop-sdk/test/ai-sdk-transformations.test.ts @@ -4,7 +4,6 @@ import { SpanAttributes } from "@traceloop/ai-semantic-conventions"; import { ATTR_GEN_AI_AGENT_NAME, ATTR_GEN_AI_COMPLETION, - ATTR_GEN_AI_CONVERSATION_ID, ATTR_GEN_AI_INPUT_MESSAGES, ATTR_GEN_AI_OPERATION_NAME, ATTR_GEN_AI_OUTPUT_MESSAGES, @@ -2185,52 +2184,6 @@ describe("AI SDK Transformations", () => { }); }); - describe("transformLLMSpans - conversation id", () => { - it("should transform conversationId from metadata", () => { - const attributes = { - "ai.telemetry.metadata.conversationId": "conv_123", - }; - - transformLLMSpans(attributes); - - assert.strictEqual(attributes[ATTR_GEN_AI_CONVERSATION_ID], "conv_123"); - }); - - it("should use sessionId as fallback for conversation id", () => { - const attributes = { - "ai.telemetry.metadata.sessionId": "session_456", - }; - - transformLLMSpans(attributes); - - assert.strictEqual( - attributes[ATTR_GEN_AI_CONVERSATION_ID], - "session_456", - ); - }); - - it("should prefer conversationId over sessionId", () => { - const attributes = { - "ai.telemetry.metadata.conversationId": "conv_123", - "ai.telemetry.metadata.sessionId": "session_456", - }; - - transformLLMSpans(attributes); - - assert.strictEqual(attributes[ATTR_GEN_AI_CONVERSATION_ID], "conv_123"); - }); - - it("should not set conversation id when neither is present", () => { - const attributes = { - "ai.telemetry.metadata.userId": "user_789", - }; - - transformLLMSpans(attributes); - - assert.strictEqual(attributes[ATTR_GEN_AI_CONVERSATION_ID], undefined); - }); - }); - describe("transformLLMSpans - response metadata", () => { it("should transform ai.response.model to gen_ai.response.model", () => { const attributes = { @@ -2303,7 +2256,6 @@ describe("AI SDK Transformations", () => { ]), "ai.usage.promptTokens": 10, "ai.usage.completionTokens": 15, - "ai.telemetry.metadata.conversationId": "conv_456", "ai.telemetry.metadata.userId": "user_789", }; @@ -2332,9 +2284,6 @@ describe("AI SDK Transformations", () => { "chatcmpl-abc123", ); - // Check conversation ID - assert.strictEqual(attributes[ATTR_GEN_AI_CONVERSATION_ID], "conv_456"); - // Check that original AI SDK attributes are removed assert.strictEqual(attributes["ai.model.id"], undefined); assert.strictEqual(attributes["ai.model.provider"], undefined); diff --git a/packages/traceloop-sdk/test/associations.test.ts b/packages/traceloop-sdk/test/associations.test.ts new file mode 100644 index 00000000..ed2a0db0 --- /dev/null +++ b/packages/traceloop-sdk/test/associations.test.ts @@ -0,0 +1,360 @@ +/* + * Copyright Traceloop + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from "assert"; + +import type * as OpenAIModule from "openai"; + +import * as traceloop from "../src"; + +import { Polly, setupMocha as setupPolly } from "@pollyjs/core"; +import NodeHttpAdapter from "@pollyjs/adapter-node-http"; +import FetchAdapter from "@pollyjs/adapter-fetch"; +import FSPersister from "@pollyjs/persister-fs"; +import { SpanAttributes } from "@traceloop/ai-semantic-conventions"; +import { ATTR_GEN_AI_PROMPT } from "@opentelemetry/semantic-conventions/incubating"; +import { initializeSharedTraceloop, getSharedExporter } from "./test-setup"; + +const memoryExporter = getSharedExporter(); + +Polly.register(NodeHttpAdapter); +Polly.register(FetchAdapter); +Polly.register(FSPersister); + +describe("Test Associations API", () => { + let openai: OpenAIModule.OpenAI; + + setupPolly({ + adapters: ["node-http", "fetch"], + persister: "fs", + recordIfMissing: process.env.RECORD_MODE === "NEW", + recordFailedRequests: true, + mode: process.env.RECORD_MODE === "NEW" ? "record" : "replay", + matchRequestsBy: { + headers: false, + url: { + protocol: true, + hostname: true, + pathname: true, + query: false, + }, + }, + logging: true, + }); + + before(async function () { + if (process.env.RECORD_MODE !== "NEW") { + process.env.OPENAI_API_KEY = "test"; + } + + // Use shared initialization to avoid conflicts with other test suites + initializeSharedTraceloop(); + + // Initialize OpenAI after Polly is set up + const openAIModule: typeof OpenAIModule = await import("openai"); + openai = new openAIModule.OpenAI(); + }); + + beforeEach(function () { + const { server } = this.polly as Polly; + server.any().on("beforePersist", (_req, recording) => { + recording.request.headers = recording.request.headers.filter( + ({ name }: { name: string }) => name !== "authorization", + ); + }); + }); + + afterEach(async () => { + memoryExporter.reset(); + }); + + it("should set single association on spans", async () => { + const result = await traceloop.withWorkflow( + { name: "test_single_association" }, + async () => { + // Set a single association + traceloop.Associations.set([ + [traceloop.AssociationProperty.SESSION_ID, "session-123"], + ]); + + const chatCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "Tell me a joke" }], + model: "gpt-3.5-turbo", + }); + + return chatCompletion.choices[0].message.content; + }, + ); + + await traceloop.forceFlush(); + const spans = memoryExporter.getFinishedSpans(); + + const workflowSpan = spans.find( + (span) => span.name === "test_single_association.workflow", + ); + const chatSpan = spans.find((span) => span.name === "openai.chat"); + + assert.ok(result); + assert.ok(workflowSpan); + assert.ok(chatSpan); + + // Check that the association is set on both workflow and LLM spans (standard properties without prefix) + assert.strictEqual(workflowSpan.attributes["session_id"], "session-123"); + assert.strictEqual(chatSpan.attributes["session_id"], "session-123"); + }); + + it("should set multiple associations on spans", async () => { + const result = await traceloop.withWorkflow( + { name: "test_multiple_associations" }, + async () => { + // Set multiple associations + traceloop.Associations.set([ + [traceloop.AssociationProperty.USER_ID, "user-456"], + [traceloop.AssociationProperty.SESSION_ID, "session-789"], + ]); + + const chatCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "Tell me a fact" }], + model: "gpt-3.5-turbo", + }); + + return chatCompletion.choices[0].message.content; + }, + ); + + await traceloop.forceFlush(); + const spans = memoryExporter.getFinishedSpans(); + + const workflowSpan = spans.find( + (span) => span.name === "test_multiple_associations.workflow", + ); + const chatSpan = spans.find((span) => span.name === "openai.chat"); + + assert.ok(result); + assert.ok(workflowSpan); + assert.ok(chatSpan); + + // Check that both associations are set (standard properties without prefix) + assert.strictEqual(workflowSpan.attributes["user_id"], "user-456"); + assert.strictEqual(workflowSpan.attributes["session_id"], "session-789"); + assert.strictEqual(chatSpan.attributes["user_id"], "user-456"); + assert.strictEqual(chatSpan.attributes["session_id"], "session-789"); + }); + + it("should allow updating associations mid-workflow", async () => { + const result = await traceloop.withWorkflow( + { name: "test_update_associations" }, + async () => { + // Set initial association + traceloop.Associations.set([ + [traceloop.AssociationProperty.SESSION_ID, "session-initial"], + ]); + + const firstCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "First message" }], + model: "gpt-3.5-turbo", + }); + + // Update association + traceloop.Associations.set([ + [traceloop.AssociationProperty.SESSION_ID, "session-updated"], + ]); + + const secondCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "Second message" }], + model: "gpt-3.5-turbo", + }); + + return { + first: firstCompletion.choices[0].message.content, + second: secondCompletion.choices[0].message.content, + }; + }, + ); + + await traceloop.forceFlush(); + const spans = memoryExporter.getFinishedSpans(); + + const firstChatSpan = spans.find( + (span) => + span.name === "openai.chat" && + span.attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] === "First message", + ); + const secondChatSpan = spans.find( + (span) => + span.name === "openai.chat" && + span.attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] === "Second message", + ); + + assert.ok(result); + assert.ok(firstChatSpan); + assert.ok(secondChatSpan); + + // First span should have initial value (standard properties without prefix) + assert.strictEqual( + firstChatSpan.attributes["session_id"], + "session-initial", + ); + + // Second span should have updated value (standard properties without prefix) + assert.strictEqual( + secondChatSpan.attributes["session_id"], + "session-updated", + ); + }); + + it("should work with all AssociationProperty types", async () => { + const result = await traceloop.withWorkflow( + { name: "test_all_property_types" }, + async () => { + // Set all association property types + traceloop.Associations.set([ + [traceloop.AssociationProperty.CUSTOMER_ID, "customer-def"], + [traceloop.AssociationProperty.USER_ID, "user-ghi"], + [traceloop.AssociationProperty.SESSION_ID, "session-jkl"], + ]); + + const chatCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "Test all properties" }], + model: "gpt-3.5-turbo", + }); + + return chatCompletion.choices[0].message.content; + }, + ); + + await traceloop.forceFlush(); + const spans = memoryExporter.getFinishedSpans(); + + const chatSpan = spans.find((span) => span.name === "openai.chat"); + + assert.ok(result); + assert.ok(chatSpan); + + // Check all property types are set (standard properties without prefix) + assert.strictEqual(chatSpan.attributes["customer_id"], "customer-def"); + assert.strictEqual(chatSpan.attributes["user_id"], "user-ghi"); + assert.strictEqual(chatSpan.attributes["session_id"], "session-jkl"); + }); + + it("should propagate associations to all child spans", async () => { + const result = await traceloop.withWorkflow( + { name: "test_child_propagation" }, + async () => { + // Set associations at the workflow level + traceloop.Associations.set([ + [traceloop.AssociationProperty.SESSION_ID, "session-propagate"], + [traceloop.AssociationProperty.USER_ID, "user-propagate"], + ]); + + // Call a child task + const taskResult = await traceloop.withTask( + { name: "subtask" }, + async () => { + const chatCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "Child task message" }], + model: "gpt-3.5-turbo", + }); + return chatCompletion.choices[0].message.content; + }, + ); + + return taskResult; + }, + ); + + await traceloop.forceFlush(); + const spans = memoryExporter.getFinishedSpans(); + + const workflowSpan = spans.find( + (span) => span.name === "test_child_propagation.workflow", + ); + const taskSpan = spans.find((span) => span.name === "subtask.task"); + const chatSpan = spans.find( + (span) => + span.name === "openai.chat" && + span.attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] === + "Child task message", + ); + + assert.ok(result); + assert.ok(workflowSpan); + assert.ok(taskSpan); + assert.ok(chatSpan); + + // All spans should have the associations (standard properties without prefix) + assert.strictEqual( + workflowSpan.attributes["session_id"], + "session-propagate", + ); + assert.strictEqual(workflowSpan.attributes["user_id"], "user-propagate"); + + assert.strictEqual(taskSpan.attributes["session_id"], "session-propagate"); + assert.strictEqual(taskSpan.attributes["user_id"], "user-propagate"); + + assert.strictEqual(chatSpan.attributes["session_id"], "session-propagate"); + assert.strictEqual(chatSpan.attributes["user_id"], "user-propagate"); + }); + + it("should merge associations from Associations.set() and withAssociationProperties()", async () => { + const result = await traceloop.withWorkflow( + { name: "test_merge_associations" }, + async () => { + // Set standard associations + traceloop.Associations.set([ + [traceloop.AssociationProperty.SESSION_ID, "session-merge"], + [traceloop.AssociationProperty.USER_ID, "user-merge"], + ]); + + // Add custom properties via withAssociationProperties + return await traceloop.withAssociationProperties( + { custom_field: "custom-value" }, + async () => { + const chatCompletion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "Test merge" }], + model: "gpt-3.5-turbo", + }); + return chatCompletion.choices[0].message.content; + }, + ); + }, + ); + + await traceloop.forceFlush(); + const spans = memoryExporter.getFinishedSpans(); + + const chatSpan = spans.find( + (span) => + span.name === "openai.chat" && + span.attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] === "Test merge", + ); + + assert.ok(result); + assert.ok(chatSpan); + + // Standard properties should be without prefix + assert.strictEqual(chatSpan.attributes["session_id"], "session-merge"); + assert.strictEqual(chatSpan.attributes["user_id"], "user-merge"); + + // Custom property should have prefix + assert.strictEqual( + chatSpan.attributes[ + `${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.custom_field` + ], + "custom-value", + ); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c521e3a..a4e41dbe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -822,6 +822,9 @@ importers: '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0 + '@opentelemetry/context-async-hooks': + specifier: ^2.0.0 + version: 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/core': specifier: ^2.0.1 version: 2.0.1(@opentelemetry/api@1.9.0) @@ -14375,7 +14378,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.3 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.13.2(debug@4.4.3)) + retry-axios: 2.6.0(axios@1.13.2) tough-cookie: 4.1.4 transitivePeerDependencies: - supports-color @@ -16317,7 +16320,7 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - retry-axios@2.6.0(axios@1.13.2(debug@4.4.3)): + retry-axios@2.6.0(axios@1.13.2): dependencies: axios: 1.13.2(debug@4.4.3)