Skip to main content

relay_spans/
op.rs

1use relay_conventions::attributes::*;
2use relay_event_schema::protocol::{Attributes, SpanKind};
3use relay_protocol::Annotated;
4
5/// Generates a `sentry.op` attribute for V2 span, if possible.
6///
7/// This uses attributes of the span to figure out an appropriate operation name, inferring what the
8/// SDK might have sent. Reliably infers an op for well-known OTel span kinds like database
9/// operations. Does not infer an op for frontend and mobile spans sent by Sentry SDKs that don't
10/// have an OTel equivalent (e.g., resource loads).
11pub fn derive_op_for_v2_span(attributes: &Annotated<Attributes>) -> String {
12    // NOTE: `op` is not a required field in the SDK, so the fallback is an empty string.
13    let op = String::from("default");
14
15    let Some(attributes) = attributes.value() else {
16        return op;
17    };
18
19    if attributes.contains_key(HTTP__REQUEST__METHOD) {
20        let kind = attributes.get_value(SENTRY__KIND).and_then(|v| v.as_str());
21        return match kind {
22            Some(kind) if kind == SpanKind::Client.as_str() => String::from("http.client"),
23            Some(kind) if kind == SpanKind::Server.as_str() => String::from("http.server"),
24            _ => {
25                if attributes.contains_key(SENTRY__HTTP__PREFETCH) {
26                    String::from("http.prefetch")
27                } else {
28                    String::from("http")
29                }
30            }
31        };
32    }
33
34    if attributes.contains_key(DB__SYSTEM__NAME) {
35        return String::from("db");
36    }
37
38    if let Some(operation) = attributes
39        .get_value(GEN_AI__OPERATION__NAME)
40        .and_then(|v| v.as_str())
41    {
42        return format!("gen_ai.{operation}");
43    }
44
45    if attributes.contains_key(GEN_AI__PROVIDER__NAME) {
46        return String::from("gen_ai");
47    }
48
49    if attributes.contains_key(RPC__SERVICE) {
50        return String::from("rpc");
51    }
52
53    if attributes.contains_key(MESSAGING__SYSTEM) {
54        return String::from("message");
55    }
56
57    if let Some(faas_trigger) = attributes.get_value(FAAS__TRIGGER).and_then(|v| v.as_str()) {
58        return faas_trigger.to_owned();
59    }
60
61    op
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    fn op_for(pairs: &[(&str, &str)]) -> String {
69        let attributes = Attributes::from_iter(
70            pairs
71                .iter()
72                .map(|(k, v)| ((*k).to_owned(), Annotated::new((*v).to_owned().into()))),
73        );
74        derive_op_for_v2_span(&Annotated::new(attributes))
75    }
76
77    #[test]
78    fn test_gen_ai_op_operation_name_takes_precedence_over_provider() {
79        assert_eq!(
80            op_for(&[
81                (GEN_AI__OPERATION__NAME, "chat"),
82                (GEN_AI__PROVIDER__NAME, "openai"),
83            ]),
84            "gen_ai.chat"
85        );
86        assert_eq!(
87            op_for(&[
88                (GEN_AI__OPERATION__NAME, "invoke_agent"),
89                (GEN_AI__PROVIDER__NAME, "openai"),
90            ]),
91            "gen_ai.invoke_agent"
92        );
93    }
94
95    #[test]
96    fn test_gen_ai_op_execute_tool_without_provider() {
97        assert_eq!(
98            op_for(&[(GEN_AI__OPERATION__NAME, "execute_tool")]),
99            "gen_ai.execute_tool"
100        );
101    }
102
103    #[test]
104    fn test_gen_ai_op_falls_back_to_provider() {
105        assert_eq!(op_for(&[(GEN_AI__PROVIDER__NAME, "openai")]), "gen_ai");
106    }
107}