@@ -5,11 +5,14 @@ import {
55 SPAN_STATUS_ERROR ,
66 startInactiveSpan ,
77 startSpan ,
8+ stringify ,
89 withActiveSpan ,
910} from '@sentry/core' ;
1011import {
1112 GEN_AI_AGENT_NAME ,
1213 GEN_AI_CONVERSATION_ID ,
14+ GEN_AI_INPUT_MESSAGES ,
15+ GEN_AI_OUTPUT_MESSAGES ,
1316 GEN_AI_COST_CACHE_CREATION_INPUT_TOKENS ,
1417 GEN_AI_COST_CACHE_READ_INPUT_TOKENS ,
1518 GEN_AI_COST_INPUT_TOKENS ,
@@ -21,6 +24,10 @@ import {
2124 GEN_AI_RESPONSE_FINISH_REASONS ,
2225 GEN_AI_RESPONSE_ID ,
2326 GEN_AI_RESPONSE_MODEL ,
27+ GEN_AI_SYSTEM_INSTRUCTIONS ,
28+ GEN_AI_TOOL_CALL_ARGUMENTS ,
29+ GEN_AI_TOOL_CALL_RESULT ,
30+ GEN_AI_TOOL_DEFINITIONS ,
2431 GEN_AI_TOOL_NAME ,
2532 GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS ,
2633 GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS ,
@@ -29,12 +36,15 @@ import {
2936 GEN_AI_USAGE_TOTAL_TOKENS ,
3037} from '@sentry/conventions/attributes' ;
3138import { ANTHROPIC_AI_INTEGRATION_NAME } from '../anthropic-ai/constants' ;
32- import { getGenAiSpanOp } from '../core/utils' ;
39+ import type { GenAiOptions } from '../core/utils' ;
40+ import { getGenAiSpanOp , resolveAIRecordingOptions } from '../core/utils' ;
3341import { GOOGLE_GENAI_INTEGRATION_NAME } from '../google-genai/constants' ;
3442import { OPENAI_INTEGRATION_NAME } from '../openai/constants' ;
3543import { FLUE_INSTRUMENTATION_KEY , FLUE_ORIGIN , SPANNED_OPERATION_TYPE } from './constants' ;
3644import type { FlueInstrumentation , FlueObservation , FlueUsage } from './types' ;
3745
46+ export type FlueOptions = GenAiOptions ;
47+
3848const SKIPPED_PROVIDERS = [ OPENAI_INTEGRATION_NAME , ANTHROPIC_AI_INTEGRATION_NAME , GOOGLE_GENAI_INTEGRATION_NAME ] ;
3949
4050/**
@@ -47,17 +57,18 @@ const SKIPPED_PROVIDERS = [OPENAI_INTEGRATION_NAME, ANTHROPIC_AI_INTEGRATION_NAM
4757 * - `observe` opens and closes the turn span, because Flue's `turn_start`/`turn` events are the
4858 * only one-to-one signal for a model call and `turn` is what carries usage and cost.
4959 *
50- * Takes no options yet: no message content is recorded, so there is nothing for
51- * `recordInputs`/`recordOutputs` to gate .
60+ * Message content, tool arguments and tool results are gated on `recordInputs`/`recordOutputs`,
61+ * which fall back to the client's `dataCollection.genAI` settings .
5262 */
53- export function createFlueInstrumentation ( ) : FlueInstrumentation {
63+ export function createFlueInstrumentation ( options : FlueOptions = { } ) : FlueInstrumentation {
5464 // Flue drives the providers through `@earendil-works/pi-ai`, which bundles the `openai`,
5565 // `@anthropic-ai/sdk` and `@google/genai` clients. Left alone they instrument the same call this
5666 // reports as a turn, emitting a second `gen_ai.chat` beside ours. Done here rather than in
5767 // `flueIntegration` so registering by hand — the only option on Cloudflare, where agents run in
5868 // per-Durable-Object isolates — gets it too.
5969 _INTERNAL_skipAiProviderWrapping ( SKIPPED_PROVIDERS ) ;
6070
71+ const { recordInputs, recordOutputs } = resolveAIRecordingOptions ( options ) ;
6172 const turnSpans = new Map < string , Span > ( ) ;
6273 const toolSpans = new Map < string , Span > ( ) ;
6374 let agentSpan : Span | undefined ;
@@ -113,14 +124,19 @@ export function createFlueInstrumentation(): FlueInstrumentation {
113124 case 'turn_start' :
114125 startTurnSpan ( observation , turnSpans , agentSpan ) ;
115126 return ;
127+ case 'turn_request' :
128+ if ( recordInputs ) {
129+ recordRequestContent ( observation , turnSpans ) ;
130+ }
131+ return ;
116132 case 'turn' :
117- endTurnSpan ( observation , turnSpans ) ;
133+ endTurnSpan ( observation , turnSpans , recordOutputs ) ;
118134 return ;
119135 case 'tool_start' :
120- startToolSpan ( observation , toolSpans , agentSpan ) ;
136+ startToolSpan ( observation , toolSpans , agentSpan , recordInputs ) ;
121137 return ;
122138 case 'tool' :
123- endToolSpan ( observation , toolSpans ) ;
139+ endToolSpan ( observation , toolSpans , recordOutputs ) ;
124140 return ;
125141 default :
126142 return ;
@@ -159,7 +175,7 @@ function startTurnSpan(observation: FlueObservation, turnSpans: Map<string, Span
159175 turnSpans . set ( turnId , agentSpan ? withActiveSpan ( agentSpan , open ) : open ( ) ) ;
160176}
161177
162- function endTurnSpan ( observation : FlueObservation , turnSpans : Map < string , Span > ) : void {
178+ function endTurnSpan ( observation : FlueObservation , turnSpans : Map < string , Span > , recordOutputs : boolean ) : void {
163179 const { turnId } = observation ;
164180 const span = turnId ? turnSpans . get ( turnId ) : undefined ;
165181 if ( ! span || ! turnId ) {
@@ -193,6 +209,11 @@ function endTurnSpan(observation: FlueObservation, turnSpans: Map<string, Span>)
193209 span . setAttribute ( GEN_AI_RESPONSE_FINISH_REASONS , [ finishReason ] ) ;
194210 }
195211
212+ const output = observation . response ?. output ;
213+ if ( recordOutputs && output !== undefined ) {
214+ span . setAttribute ( GEN_AI_OUTPUT_MESSAGES , stringify ( output ) ) ;
215+ }
216+
196217 setUsageAttributes ( span , observation . response ?. usage , observation . isError ) ;
197218
198219 if ( observation . isError ) {
@@ -239,7 +260,12 @@ function setUsageAttributes(span: Span, usage: FlueUsage | undefined, isError?:
239260 * OpenTelemetry adapter projects them: siblings of `chat`, correlated to model output by tool call
240261 * id. Keyed by `toolCallId` so concurrent tool calls in one turn cannot cross-attribute.
241262 */
242- function startToolSpan ( observation : FlueObservation , toolSpans : Map < string , Span > , agentSpan : Span | undefined ) : void {
263+ function startToolSpan (
264+ observation : FlueObservation ,
265+ toolSpans : Map < string , Span > ,
266+ agentSpan : Span | undefined ,
267+ recordInputs : boolean ,
268+ ) : void {
243269 const { toolCallId, toolName } = observation ;
244270 if ( ! toolCallId || toolSpans . has ( toolCallId ) ) {
245271 return ;
@@ -254,22 +280,52 @@ function startToolSpan(observation: FlueObservation, toolSpans: Map<string, Span
254280 [ GEN_AI_OPERATION_NAME ] : 'execute_tool' ,
255281 ...( toolName ? { [ GEN_AI_TOOL_NAME ] : toolName } : { } ) ,
256282 ...( observation . conversationId ? { [ GEN_AI_CONVERSATION_ID ] : observation . conversationId } : { } ) ,
283+ ...( recordInputs && observation . args !== undefined
284+ ? { [ GEN_AI_TOOL_CALL_ARGUMENTS ] : stringify ( observation . args ) }
285+ : { } ) ,
257286 } ,
258287 } ) ;
259288
260289 toolSpans . set ( toolCallId , agentSpan ? withActiveSpan ( agentSpan , open ) : open ( ) ) ;
261290}
262291
263- function endToolSpan ( observation : FlueObservation , toolSpans : Map < string , Span > ) : void {
292+ function endToolSpan ( observation : FlueObservation , toolSpans : Map < string , Span > , recordOutputs : boolean ) : void {
264293 const { toolCallId } = observation ;
265294 const span = toolCallId ? toolSpans . get ( toolCallId ) : undefined ;
266295 if ( ! span || ! toolCallId ) {
267296 return ;
268297 }
269298 toolSpans . delete ( toolCallId ) ;
270299
300+ if ( recordOutputs && observation . result !== undefined ) {
301+ span . setAttribute ( GEN_AI_TOOL_CALL_RESULT , stringify ( observation . result ) ) ;
302+ }
303+
271304 if ( observation . isError ) {
272305 span . setStatus ( { code : SPAN_STATUS_ERROR , message : 'internal_error' } ) ;
273306 }
274307 span . end ( ) ;
275308}
309+
310+ /**
311+ * `turn_request` is the only event carrying the request's content — the settled `turn` reports
312+ * metadata alone — so input messages, system prompt and tool definitions are read from it.
313+ */
314+ function recordRequestContent ( observation : FlueObservation , turnSpans : Map < string , Span > ) : void {
315+ const { turnId } = observation ;
316+ const span = turnId ? turnSpans . get ( turnId ) : undefined ;
317+ const input = observation . request ?. input ;
318+ if ( ! span || ! input ) {
319+ return ;
320+ }
321+
322+ if ( input . systemPrompt ) {
323+ span . setAttribute ( GEN_AI_SYSTEM_INSTRUCTIONS , input . systemPrompt ) ;
324+ }
325+ if ( input . messages ) {
326+ span . setAttribute ( GEN_AI_INPUT_MESSAGES , stringify ( input . messages ) ) ;
327+ }
328+ if ( input . tools ?. length ) {
329+ span . setAttribute ( GEN_AI_TOOL_DEFINITIONS , stringify ( input . tools ) ) ;
330+ }
331+ }
0 commit comments