Skip to content

Commit e500300

Browse files
committed
fix(server-utils): Instrument LangGraph stream executions
Fixes getsentry#19626
1 parent 63119eb commit e500300

9 files changed

Lines changed: 433 additions & 103 deletions

File tree

‎dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ export default Sentry.withSentry(
6161
messages: [{ role: 'user', content: 'What is the weather in SF?' }],
6262
});
6363

64+
const stream = await compiled.stream({
65+
messages: [{ role: 'user', content: 'Stream the weather in SF' }],
66+
});
67+
for await (const _chunk of stream) {
68+
// Consuming the iterator is what runs the graph and completes the agent span.
69+
}
70+
6471
return new Response(JSON.stringify({ success: true }));
6572
},
6673
},

‎dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
GEN_AI_OPERATION_NAME,
77
GEN_AI_PIPELINE_NAME,
88
GEN_AI_RESPONSE_MODEL,
9+
GEN_AI_RESPONSE_STREAMING,
910
GEN_AI_USAGE_INPUT_TOKENS,
1011
GEN_AI_USAGE_OUTPUT_TOKENS,
1112
GEN_AI_USAGE_TOTAL_TOKENS,
@@ -17,7 +18,7 @@ import { createRunner } from '../../../runner';
1718
// want to test that the instrumentation does not break in our
1819
// cloudflare SDK.
1920

20-
it('traces langgraph compile and invoke operations', async ({ signal }) => {
21+
it('traces langgraph invoke and stream operations', async ({ signal }) => {
2122
const runner = createRunner(__dirname)
2223
.ignore('event')
2324
.expect(envelope => {
@@ -29,13 +30,14 @@ it('traces langgraph compile and invoke operations', async ({ signal }) => {
2930
const container = envelope[1]?.[1]?.[1] as any;
3031
expect(container).toBeDefined();
3132

32-
expect(container.items).toHaveLength(1);
33+
expect(container.items).toHaveLength(2);
3334
expect(container.items.map((span: SerializedStreamedSpan) => span.name).sort()).toEqual([
3435
'invoke_agent weather_assistant',
36+
'invoke_agent weather_assistant',
3537
]);
3638

3739
const invokeAgentSpan = container.items.find(
38-
(span: SerializedStreamedSpan) => span.name === 'invoke_agent weather_assistant',
40+
(span: SerializedStreamedSpan) => span.attributes[GEN_AI_RESPONSE_STREAMING] === undefined,
3941
);
4042
expect(invokeAgentSpan).toBeDefined();
4143
expect(invokeAgentSpan!.status).toBe('ok');
@@ -73,6 +75,16 @@ it('traces langgraph compile and invoke operations', async ({ signal }) => {
7375
type: 'integer',
7476
value: 30,
7577
});
78+
79+
const streamSpan = container.items.find(
80+
(span: SerializedStreamedSpan) => span.attributes[GEN_AI_RESPONSE_STREAMING]?.value === true,
81+
);
82+
expect(streamSpan).toBeDefined();
83+
expect(streamSpan!.status).toBe('ok');
84+
expect(streamSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toEqual({
85+
type: 'string',
86+
value: '[{"role":"user","content":"Stream the weather in SF"}]',
87+
});
7688
})
7789
.start(signal);
7890
await runner.makeRequest('get', '/');

‎dev-packages/node-integration-tests/suites/tracing/langgraph/scenario.mjs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ async function run() {
4444
{ role: 'user', content: 'Tell me about the weather' },
4545
],
4646
});
47+
48+
const stream = await graph.stream({
49+
messages: [{ role: 'user', content: 'Stream the weather forecast' }],
50+
});
51+
for await (const _chunk of stream) {
52+
// Consuming the iterator is what runs the graph and completes the agent span.
53+
}
4754
});
4855

4956
await Sentry.flush(2000);

‎dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
GEN_AI_OPERATION_NAME,
88
GEN_AI_PIPELINE_NAME,
99
GEN_AI_RESPONSE_MODEL,
10+
GEN_AI_RESPONSE_STREAMING,
1011
GEN_AI_RESPONSE_TEXT,
1112
GEN_AI_RESPONSE_TOOL_CALLS,
1213
GEN_AI_SYSTEM_INSTRUCTIONS,
@@ -31,14 +32,15 @@ describe('LangGraph integration', () => {
3132
.expect({ transaction: { transaction: 'langgraph-test' } })
3233
.expect({
3334
span: container => {
34-
expect(container.items).toHaveLength(2);
35+
expect(container.items).toHaveLength(3);
3536
expect(container.items.map(span => span.name).sort()).toEqual([
3637
'invoke_agent weather_assistant',
3738
'invoke_agent weather_assistant',
39+
'invoke_agent weather_assistant',
3840
]);
3941

4042
const invokeAgentSpans = container.items.filter(span => span.name === 'invoke_agent weather_assistant');
41-
expect(invokeAgentSpans).toHaveLength(2);
43+
expect(invokeAgentSpans).toHaveLength(3);
4244
for (const span of invokeAgentSpans) {
4345
expect(span.status).toBe('ok');
4446
expect(span.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent');
@@ -47,6 +49,11 @@ describe('LangGraph integration', () => {
4749
expect(span.attributes[GEN_AI_AGENT_NAME].value).toBe('weather_assistant');
4850
expect(span.attributes[GEN_AI_PIPELINE_NAME].value).toBe('weather_assistant');
4951
}
52+
53+
const streamSpan = invokeAgentSpans.find(
54+
span => span.attributes[GEN_AI_RESPONSE_STREAMING]?.value === true,
55+
);
56+
expect(streamSpan).toBeDefined();
5057
},
5158
})
5259
.start()
@@ -61,7 +68,7 @@ describe('LangGraph integration', () => {
6168
.expect({ transaction: { transaction: 'langgraph-test' } })
6269
.expect({
6370
span: container => {
64-
expect(container.items).toHaveLength(2);
71+
expect(container.items).toHaveLength(3);
6572

6673
const weatherTodaySpan = container.items.find(span =>
6774
getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES]?.value)?.includes(
@@ -83,6 +90,14 @@ describe('LangGraph integration', () => {
8390
expect(weatherDetailsSpan!.name).toBe('invoke_agent weather_assistant');
8491
expect(weatherDetailsSpan!.status).toBe('ok');
8592
expect(weatherDetailsSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent');
93+
94+
const weatherStreamSpan = container.items.find(span =>
95+
getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES]?.value)?.includes(
96+
'Stream the weather forecast',
97+
),
98+
);
99+
expect(weatherStreamSpan).toBeDefined();
100+
expect(weatherStreamSpan!.attributes[GEN_AI_RESPONSE_STREAMING].value).toBe(true);
86101
},
87102
})
88103
.start()

0 commit comments

Comments
 (0)