Skip to content

Conversation

@vgrozdanic
Copy link
Member

@vgrozdanic vgrozdanic commented Sep 10, 2025

Description

In relay, we want to infer the gen_ai.operation.type attribute (it's a newly added attribute) from gen_ai.operation.name.

This will be used in UI to easier query and plot data based on operation type. Currently we have a long list of attributes that we query for one operation type, but as that list grows, it is becoming harder and harder to pass all the parameters as query param in a request to EAP.

Implementation plan

PRs will be separated to make it easier to review. The mapping will be defined in sentry, and it will be propagated to relays as a part of global config to make it easier for us to change and extend the mappings.

(in relay):

(in sentry):

  • during global config generation, generate ai_operation_type_map from defined list of attributes

Part of TET-1092: Introduce gen_ai.operation.type attribute

@linear
Copy link

linear bot commented Sep 10, 2025

@vgrozdanic vgrozdanic force-pushed the vg/map-operation-type branch from cb4caba to 2ceb394 Compare September 10, 2025 12:07
@vgrozdanic vgrozdanic force-pushed the vg/map-operation-type branch from 2ceb394 to 129ad7a Compare September 10, 2025 12:22
- Add gen_ai_cost_total_tokens attribute and double write total tokens cost. ([#5121](https://github.com/getsentry/relay/pull/5121))
- Change mapping of incoming OTLP spans with `ERROR` status to Sentry's `internal_error` status. ([#5127](https://github.com/getsentry/relay/pull/5127))
- Add `ai_operation_type_map` to global config ([#5125](https://github.com/getsentry/relay/pull/5125))
- Add `ai_operation_type_map` to global config. ([#5125](https://github.com/getsentry/relay/pull/5125))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added dot at the end of the change log entry

use crate::normalize::AiOperationTypeMap;
use crate::normalize::request;
use crate::span::ai::enrich_ai_span_data;
use crate::span::ai::enrich_ai_event_data;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a function rename - it was called enrich_ai_span_data but it was operating on an event that had a lot of spans.

Also a new function enrich_ai_span_data is now introduced that operates only on span, and it is called inside of enrich_ai_event_data to prevent code duplication

pub ai_model_costs: Option<&'a ModelCosts>,

/// Configuration for mapping AI operation types from span.op to gen_ai.operation.type
pub ai_operation_type_map: Option<&'a AiOperationTypeMap>,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just like with ai_model_costs we don't have access to global config object in a code path where we would need it, so we add this map to NormalizationConfig which is accessible where we need it

@vgrozdanic vgrozdanic force-pushed the vg/map-operation-type branch from 129ad7a to a5ea007 Compare September 10, 2025 12:29
@vgrozdanic vgrozdanic force-pushed the vg/map-operation-type branch from a5ea007 to 7c18e64 Compare September 10, 2025 12:31
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file is a bit refactored:

  • extract_ai_data is no longer a pub function - there is no need for it
  • map_ai_measurements_to_data is no longer a pub function - there is no need for it
  • is_ai_span is no longer a pub function - there is no need for it
  • to prevent logic duplication, we now have only 2 pub functions enrich_ai_event_data and enrich_ai_span_data (also called within enrich_ai_event_data) and only those two unctions are publicly exposed

if let Some(model_costs_config) = ai_model_costs {
extract_ai_data(span, model_costs_config);
}
enrich_ai_span_data(span, ai_model_costs, ai_operation_type_map);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a benefit of the refactor. next time we want to extend AI enrichment, we only need to change enrich_ai_span_data function, and not worry about updating all of the other places where it could be called

@vgrozdanic vgrozdanic marked this pull request as ready for review September 10, 2025 12:34
@vgrozdanic vgrozdanic requested a review from a team as a code owner September 10, 2025 12:34
cursor[bot]

This comment was marked as outdated.

Copy link
Member

@jjbayer jjbayer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pressing request changes because of the .unwrap().

Comment on lines 348 to 351
let exact_key = Pattern::new(span_op).ok()?;
if self.operation_types.contains_key(&exact_key) {
return self.operation_types.get(&exact_key).map(String::as_str);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this part. Why would we set the type to the key of the hash map instead of the value?

Copy link
Member Author

@vgrozdanic vgrozdanic Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value of the hashmap is String, so if possible, we do only a hash lookup with Pattern value of span_op, if not we will iterate over all of the keys to search for a fallback.

I don't really understand the question: "Why would we set the type to the key of the hash map instead of the value?"

If it helps and adds more context why it is done like this, we do the similar thing for model cost hash map:

pub fn cost_per_token(&self, model_id: &str) -> Option<ModelCostV2> {

Copy link
Member

@jjbayer jjbayer Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do this without creating a Pattern for the key if you add a Borrow implementation to Pattern:

impl Borrow<str> for Pattern {
    fn borrow(&self) -> &str {
        &self.pattern
    }
}

From the docs:

The key may be any borrowed form of the map’s key type, but Hash and Eq on the borrowed form must match those for the key type.

Suggested change
let exact_key = Pattern::new(span_op).ok()?;
if self.operation_types.contains_key(&exact_key) {
return self.operation_types.get(&exact_key).map(String::as_str);
}
if let Some(value) = self.operation_types.get(span_op) {
return Some(value.as_str());
}

Same for cost_per_token.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did it for get_operation_type, for cost_per_token i'll do it as a follow up because i need to change a bit more code (the reference will be returned instead the owned object), and it will have nicer commit associated with it

return;
};

if let Some(operation_type) = operation_type_map.get_operation_type(span.op.value().unwrap()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could panic because of the .unwrap().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, while i was debugging this, i just put unwrap to quickly iterate on it 😬

Removed it now

cursor[bot]

This comment was marked as outdated.

@vgrozdanic vgrozdanic requested a review from jjbayer September 10, 2025 13:35
Copy link
Member

@jjbayer jjbayer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can optimize a little (see comment) but overall this looks good!

Comment on lines 348 to 351
let exact_key = Pattern::new(span_op).ok()?;
if self.operation_types.contains_key(&exact_key) {
return self.operation_types.get(&exact_key).map(String::as_str);
}
Copy link
Member

@jjbayer jjbayer Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do this without creating a Pattern for the key if you add a Borrow implementation to Pattern:

impl Borrow<str> for Pattern {
    fn borrow(&self) -> &str {
        &self.pattern
    }
}

From the docs:

The key may be any borrowed form of the map’s key type, but Hash and Eq on the borrowed form must match those for the key type.

Suggested change
let exact_key = Pattern::new(span_op).ok()?;
if self.operation_types.contains_key(&exact_key) {
return self.operation_types.get(&exact_key).map(String::as_str);
}
if let Some(value) = self.operation_types.get(span_op) {
return Some(value.as_str());
}

Same for cost_per_token.

@vgrozdanic vgrozdanic requested a review from jjbayer September 11, 2025 09:10
Copy link
Member

@jjbayer jjbayer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check comments before merging.

@vgrozdanic vgrozdanic added this pull request to the merge queue Sep 11, 2025
Merged via the queue into master with commit fee8f0f Sep 11, 2025
28 checks passed
@vgrozdanic vgrozdanic deleted the vg/map-operation-type branch September 11, 2025 11:38
github-merge-queue bot pushed a commit that referenced this pull request Sep 12, 2025
Based on the comment
(#5129 (comment))
this PR improves the code for AI cost calculation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants