Skip to content

Commit e73455a

Browse files
feat(event-normalization): Update is_ai_span and infer_ai_operation_type to use gen_ai.operation.name (#5433)
Co-authored-by: David Herberth <[email protected]>
1 parent bb9aa47 commit e73455a

File tree

2 files changed

+155
-8
lines changed

2 files changed

+155
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
**Internal**:
1010

1111
- Revise trace metric and log size limits. ([#5440](https://github.com/getsentry/relay/pull/5440))
12+
- Update `is_ai_span` and `infer_ai_operation_type` to use `gen_ai.operation.name`. ([#5433](https://github.com/getsentry/relay/pull/5433))
1213

1314
## 25.11.1
1415

relay-event-normalization/src/normalize/span/ai.rs

Lines changed: 154 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -244,29 +244,47 @@ pub fn enrich_ai_event_data(
244244

245245
/// Infer AI operation type mapping to a span.
246246
///
247-
/// This function maps span.op values to gen_ai.operation.type based on the provided
248-
/// operation type map configuration.
247+
/// This function sets the gen_ai.operation.type attribute based on the value of either
248+
/// gen_ai.operation.name or span.op based on the provided operation type map configuration.
249249
fn infer_ai_operation_type(span: &mut Span, operation_type_map: &AiOperationTypeMap) {
250250
let data = span.data.get_or_insert_with(SpanData::default);
251+
let op_type = data
252+
.gen_ai_operation_name
253+
.value()
254+
.or(span.op.value())
255+
.and_then(|op| operation_type_map.get_operation_type(op));
251256

252-
if let Some(op) = span.op.value()
253-
&& let Some(operation_type) = operation_type_map.get_operation_type(op)
254-
{
257+
if let Some(operation_type) = op_type {
255258
data.gen_ai_operation_type
256259
.set_value(Some(operation_type.to_owned()));
257260
}
258261
}
259262

260263
/// Returns true if the span is an AI span.
261-
/// AI spans are spans with op starting with "ai." (legacy) or "gen_ai." (new).
264+
/// AI spans are spans with either a gen_ai.operation.name attribute or op starting with "ai."
265+
/// (legacy) or "gen_ai." (new).
262266
fn is_ai_span(span: &Span) -> bool {
263-
span.op
267+
let has_ai_op = span
268+
.data
269+
.value()
270+
.and_then(|data| data.gen_ai_operation_name.value())
271+
.is_some();
272+
273+
let is_ai_span_op = span
274+
.op
264275
.value()
265-
.is_some_and(|op| op.starts_with("ai.") || op.starts_with("gen_ai."))
276+
.is_some_and(|op| op.starts_with("ai.") || op.starts_with("gen_ai."));
277+
278+
has_ai_op || is_ai_span_op
266279
}
267280

268281
#[cfg(test)]
269282
mod tests {
283+
use std::collections::HashMap;
284+
285+
use relay_pattern::Pattern;
286+
use relay_protocol::get_value;
287+
270288
use super::*;
271289

272290
#[test]
@@ -364,4 +382,132 @@ mod tests {
364382
}
365383
");
366384
}
385+
386+
/// Test that the AI operation type is inferred from a gen_ai.operation.name attribute.
387+
#[test]
388+
fn test_infer_ai_operation_type_from_gen_ai_operation_name() {
389+
let operation_types = HashMap::from([
390+
(Pattern::new("*").unwrap(), "ai_client".to_owned()),
391+
(Pattern::new("invoke_agent").unwrap(), "agent".to_owned()),
392+
(
393+
Pattern::new("gen_ai.invoke_agent").unwrap(),
394+
"agent".to_owned(),
395+
),
396+
]);
397+
398+
let operation_type_map = AiOperationTypeMap {
399+
version: 1,
400+
operation_types,
401+
};
402+
403+
let span = r#"{
404+
"data": {
405+
"gen_ai.operation.name": "invoke_agent"
406+
}
407+
}"#;
408+
let mut span = Annotated::from_json(span).unwrap();
409+
infer_ai_operation_type(span.value_mut().as_mut().unwrap(), &operation_type_map);
410+
assert_eq!(
411+
get_value!(span.data.gen_ai_operation_type!).as_str(),
412+
"agent"
413+
);
414+
}
415+
416+
/// Test that the AI operation type is inferred from a span.op attribute.
417+
#[test]
418+
fn test_infer_ai_operation_type_from_span_op() {
419+
let operation_types = HashMap::from([
420+
(Pattern::new("*").unwrap(), "ai_client".to_owned()),
421+
(Pattern::new("invoke_agent").unwrap(), "agent".to_owned()),
422+
(
423+
Pattern::new("gen_ai.invoke_agent").unwrap(),
424+
"agent".to_owned(),
425+
),
426+
]);
427+
let operation_type_map = AiOperationTypeMap {
428+
version: 1,
429+
operation_types,
430+
};
431+
432+
let span = r#"{
433+
"op": "gen_ai.invoke_agent"
434+
}"#;
435+
let mut span = Annotated::from_json(span).unwrap();
436+
infer_ai_operation_type(span.value_mut().as_mut().unwrap(), &operation_type_map);
437+
assert_eq!(
438+
get_value!(span.data.gen_ai_operation_type!).as_str(),
439+
"agent"
440+
);
441+
}
442+
443+
/// Test that the AI operation type is inferred from a fallback.
444+
#[test]
445+
fn test_infer_ai_operation_type_from_fallback() {
446+
let operation_types = HashMap::from([
447+
(Pattern::new("*").unwrap(), "ai_client".to_owned()),
448+
(Pattern::new("invoke_agent").unwrap(), "agent".to_owned()),
449+
(
450+
Pattern::new("gen_ai.invoke_agent").unwrap(),
451+
"agent".to_owned(),
452+
),
453+
]);
454+
455+
let operation_type_map = AiOperationTypeMap {
456+
version: 1,
457+
operation_types,
458+
};
459+
460+
let span = r#"{
461+
"data": {
462+
"gen_ai.operation.name": "embeddings"
463+
}
464+
}"#;
465+
let mut span = Annotated::from_json(span).unwrap();
466+
infer_ai_operation_type(span.value_mut().as_mut().unwrap(), &operation_type_map);
467+
assert_eq!(
468+
get_value!(span.data.gen_ai_operation_type!).as_str(),
469+
"ai_client"
470+
);
471+
}
472+
473+
/// Test that an AI span is detected from a gen_ai.operation.name attribute.
474+
#[test]
475+
fn test_is_ai_span_from_gen_ai_operation_name() {
476+
let span = r#"{
477+
"data": {
478+
"gen_ai.operation.name": "chat"
479+
}
480+
}"#;
481+
let span: Span = Annotated::from_json(span).unwrap().into_value().unwrap();
482+
assert!(is_ai_span(&span));
483+
}
484+
485+
/// Test that an AI span is detected from a span.op starting with "ai.".
486+
#[test]
487+
fn test_is_ai_span_from_span_op_ai() {
488+
let span = r#"{
489+
"op": "ai.chat"
490+
}"#;
491+
let span: Span = Annotated::from_json(span).unwrap().into_value().unwrap();
492+
assert!(is_ai_span(&span));
493+
}
494+
495+
/// Test that an AI span is detected from a span.op starting with "gen_ai.".
496+
#[test]
497+
fn test_is_ai_span_from_span_op_gen_ai() {
498+
let span = r#"{
499+
"op": "gen_ai.chat"
500+
}"#;
501+
let span: Span = Annotated::from_json(span).unwrap().into_value().unwrap();
502+
assert!(is_ai_span(&span));
503+
}
504+
505+
/// Test that a non-AI span is detected.
506+
#[test]
507+
fn test_is_ai_span_negative() {
508+
let span = r#"{
509+
}"#;
510+
let span: Span = Annotated::from_json(span).unwrap().into_value().unwrap();
511+
assert!(!is_ai_span(&span));
512+
}
367513
}

0 commit comments

Comments
 (0)