Skip to content

Commit 34a5be2

Browse files
committed
added associations
1 parent a0b2f9d commit 34a5be2

File tree

11 files changed

+557
-64
lines changed

11 files changed

+557
-64
lines changed

converted-old.json

Lines changed: 0 additions & 34 deletions
This file was deleted.

converted.json

Lines changed: 0 additions & 18 deletions
This file was deleted.

packages/sample-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"run:gemini": "npm run build && node dist/src/vertexai/gemini.js",
1616
"run:palm2": "npm run build && node dist/src/vertexai/palm2.js",
1717
"run:decorators": "npm run build && node dist/src/sample_decorators.js",
18+
"run:associations": "npm run build && node dist/src/sample_associations.js",
1819
"run:with": "npm run build && node dist/src/sample_with.js",
1920
"run:prompt_mgmt": "npm run build && node dist/src/sample_prompt_mgmt.js",
2021
"run:vercel": "npm run build && node dist/src/sample_vercel_ai.js",
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import * as traceloop from "@traceloop/node-server-sdk";
2+
import OpenAI from "openai";
3+
4+
// Initialize Traceloop
5+
traceloop.initialize({
6+
appName: "associations_demo",
7+
apiKey: process.env.TRACELOOP_API_KEY,
8+
disableBatch: true,
9+
});
10+
11+
const openai = new OpenAI();
12+
13+
/**
14+
* Sample chatbot that demonstrates the Associations API.
15+
* This example shows how to track conversations, users, and sessions
16+
* across multiple LLM interactions.
17+
*/
18+
class ChatbotWithAssociations {
19+
constructor(
20+
private conversationId: string,
21+
private userId: string,
22+
private sessionId: string,
23+
) {}
24+
25+
/**
26+
* Process a multi-turn conversation with associations
27+
*/
28+
@traceloop.workflow({ name: "chatbot_conversation" })
29+
async handleConversation() {
30+
console.log("\n=== Starting Chatbot Conversation ===");
31+
console.log(`Conversation ID: ${this.conversationId}`);
32+
console.log(`User ID: ${this.userId}`);
33+
console.log(`Session ID: ${this.sessionId}\n`);
34+
35+
// Set associations at the beginning of the conversation
36+
// These will be automatically attached to all spans within this context
37+
traceloop.Associations.set([
38+
[traceloop.AssociationProperty.CONVERSATION_ID, this.conversationId],
39+
[traceloop.AssociationProperty.USER_ID, this.userId],
40+
[traceloop.AssociationProperty.SESSION_ID, this.sessionId],
41+
]);
42+
43+
// First message
44+
const greeting = await this.sendMessage(
45+
"Hello! What's the weather like today?",
46+
);
47+
console.log(`Bot: ${greeting}\n`);
48+
49+
// Second message in the same conversation
50+
const followup = await this.sendMessage(
51+
"What should I wear for that weather?",
52+
);
53+
console.log(`Bot: ${followup}\n`);
54+
55+
// Third message
56+
const final = await this.sendMessage("Thanks for the advice!");
57+
console.log(`Bot: ${final}\n`);
58+
59+
return {
60+
greeting,
61+
followup,
62+
final,
63+
};
64+
}
65+
66+
/**
67+
* Send a single message - this is a task within the workflow
68+
*/
69+
@traceloop.task({ name: "send_message" })
70+
private async sendMessage(userMessage: string): Promise<string> {
71+
console.log(`User: ${userMessage}`);
72+
73+
const completion = await openai.chat.completions.create({
74+
messages: [{ role: "user", content: userMessage }],
75+
model: "gpt-3.5-turbo",
76+
});
77+
78+
return completion.choices[0].message.content || "No response";
79+
}
80+
}
81+
82+
/**
83+
* Simulate a customer service scenario with multiple customers
84+
*/
85+
async function customerServiceDemo() {
86+
return traceloop.withWorkflow(
87+
{ name: "customer_service_scenario" },
88+
async () => {
89+
console.log("\n=== Customer Service Scenario ===\n");
90+
91+
// Customer 1
92+
traceloop.Associations.set([
93+
[traceloop.AssociationProperty.CUSTOMER_ID, "cust-001"],
94+
[traceloop.AssociationProperty.USER_ID, "agent-alice"],
95+
]);
96+
97+
const customer1Response = await openai.chat.completions.create({
98+
messages: [
99+
{
100+
role: "user",
101+
content: "I need help with my order #12345",
102+
},
103+
],
104+
model: "gpt-3.5-turbo",
105+
});
106+
107+
console.log("Customer 1 (cust-001):");
108+
console.log(`Response: ${customer1Response.choices[0].message.content}\n`);
109+
110+
// Customer 2 - Update associations for new customer
111+
traceloop.Associations.set([
112+
[traceloop.AssociationProperty.CUSTOMER_ID, "cust-002"],
113+
[traceloop.AssociationProperty.USER_ID, "agent-bob"],
114+
]);
115+
116+
const customer2Response = await openai.chat.completions.create({
117+
messages: [
118+
{
119+
role: "user",
120+
content: "How do I return an item?",
121+
},
122+
],
123+
model: "gpt-3.5-turbo",
124+
});
125+
126+
console.log("Customer 2 (cust-002):");
127+
console.log(`Response: ${customer2Response.choices[0].message.content}\n`);
128+
},
129+
);
130+
}
131+
132+
/**
133+
* Main execution
134+
*/
135+
async function main() {
136+
console.log("============================================");
137+
console.log("Traceloop Associations API Demo");
138+
console.log("============================================");
139+
140+
try {
141+
// Example 1: Multi-turn chatbot conversation
142+
const chatbot = new ChatbotWithAssociations(
143+
"conv-abc-123", // conversation_id
144+
"user-alice-456", // user_id
145+
"session-xyz-789", // session_id
146+
);
147+
148+
await chatbot.handleConversation();
149+
150+
// Example 2: Customer service with multiple customers
151+
await customerServiceDemo();
152+
153+
console.log("\n=== Demo Complete ===");
154+
console.log(
155+
"Check your Traceloop dashboard to see the associations attached to traces!",
156+
);
157+
console.log(
158+
"You can filter and search by conversation_id, user_id, session_id, or customer_id.",
159+
);
160+
} catch (error) {
161+
console.error("Error running demo:", error);
162+
process.exit(1);
163+
}
164+
}
165+
166+
// Run the demo
167+
main();

packages/traceloop-sdk/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"dependencies": {
5858
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
5959
"@opentelemetry/api": "^1.9.0",
60+
"@opentelemetry/context-async-hooks": "^2.0.0",
6061
"@opentelemetry/core": "^2.0.1",
6162
"@opentelemetry/exporter-trace-otlp-proto": "^0.203.0",
6263
"@opentelemetry/instrumentation": "^0.203.0",

packages/traceloop-sdk/src/lib/node-server-sdk.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export { getTraceloopTracer } from "./tracing/tracing";
6464
export * from "./tracing/decorators";
6565
export * from "./tracing/manual";
6666
export * from "./tracing/association";
67+
export * from "./tracing/associations";
6768
export * from "./tracing/custom-metric";
6869
export * from "./tracing/span-processor";
6970
export * from "./prompts";

packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -601,10 +601,8 @@ const transformTelemetryMetadata = (
601601
const stringValue = typeof value === "string" ? value : String(value);
602602
metadataAttributes[metadataKey] = stringValue;
603603

604-
// Also set as traceloop association property attribute
605-
attributes[
606-
`${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${metadataKey}`
607-
] = stringValue;
604+
// Also set as association property attribute
605+
attributes[metadataKey] = stringValue;
608606
}
609607
}
610608
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { trace, context as otelContext } from "@opentelemetry/api";
2+
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
3+
import { ASSOCATION_PROPERTIES_KEY } from "./tracing";
4+
5+
/**
6+
* Standard association properties for tracing.
7+
*/
8+
export enum AssociationProperty {
9+
CONVERSATION_ID = "conversation_id",
10+
CUSTOMER_ID = "customer_id",
11+
USER_ID = "user_id",
12+
SESSION_ID = "session_id",
13+
}
14+
15+
/**
16+
* Type alias for a single association
17+
*/
18+
export type Association = [AssociationProperty, string];
19+
20+
/**
21+
* Class for managing trace associations.
22+
*/
23+
export class Associations {
24+
/**
25+
* Set associations that will be added directly to all spans in the current context.
26+
*
27+
* @param associations - An array of [property, value] tuples
28+
*
29+
* @example
30+
* // Single association
31+
* Associations.set([[AssociationProperty.CONVERSATION_ID, "conv-123"]]);
32+
*
33+
* // Multiple associations
34+
* Associations.set([
35+
* [AssociationProperty.USER_ID, "user-456"],
36+
* [AssociationProperty.SESSION_ID, "session-789"]
37+
* ]);
38+
*/
39+
static set(associations: Association[]): void {
40+
// Get current associations from context or create empty object
41+
const existingAssociations = otelContext
42+
.active()
43+
.getValue(ASSOCATION_PROPERTIES_KEY) as Record<string, string> | undefined;
44+
const currentAssociations: Record<string, string> = existingAssociations
45+
? { ...existingAssociations }
46+
: {};
47+
48+
// Update associations with new values
49+
for (const [prop, value] of associations) {
50+
currentAssociations[prop] = value;
51+
}
52+
53+
// Store associations in context
54+
const newContext = otelContext
55+
.active()
56+
.setValue(ASSOCATION_PROPERTIES_KEY, currentAssociations);
57+
58+
// Set the new context as active using the context manager
59+
// This is the equivalent of Python's attach(set_value(...))
60+
const contextManager = (otelContext as any)['_getContextManager']();
61+
if (
62+
contextManager &&
63+
contextManager instanceof AsyncLocalStorageContextManager
64+
) {
65+
// For AsyncLocalStorageContextManager, we need to use the internal _asyncLocalStorage
66+
const storage = (contextManager as any)._asyncLocalStorage;
67+
if (storage) {
68+
storage.enterWith(newContext);
69+
}
70+
}
71+
72+
// Also set directly on the current span
73+
const span = trace.getSpan(otelContext.active());
74+
if (span && span.isRecording()) {
75+
for (const [prop, value] of associations) {
76+
span.setAttribute(prop, value);
77+
}
78+
}
79+
}
80+
}

packages/traceloop-sdk/src/lib/tracing/span-processor.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,7 @@ const onSpanStart = (span: Span): void => {
197197
.getValue(ASSOCATION_PROPERTIES_KEY);
198198
if (associationProperties) {
199199
for (const [key, value] of Object.entries(associationProperties)) {
200-
span.setAttribute(
201-
`${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${key}`,
202-
value,
203-
);
200+
span.setAttribute(key, value);
204201
}
205202
}
206203

0 commit comments

Comments
 (0)