Skip to content

Commit f43fbcc

Browse files
authored
feat(trace-spans): Allow querying additional attributes (#95101)
Allow querying additional attributes when fetching the spans of a trace.
1 parent df6789d commit f43fbcc

File tree

4 files changed

+59
-5
lines changed

4 files changed

+59
-5
lines changed

src/sentry/api/endpoints/organization_trace.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class SerializedSpan(SerializedEvent):
7171
start_timestamp: datetime
7272
is_transaction: bool
7373
transaction_id: str
74+
additional_attributes: NotRequired[dict[str, Any]]
7475

7576

7677
@region_silo_endpoint
@@ -169,12 +170,21 @@ def _qualify_short_id(project: str, short_id: int | None) -> str | None:
169170
raise Exception(f"Unknown event encountered in trace: {event.get('event_type')}")
170171

171172
def serialize_rpc_event(
172-
self, event: dict[str, Any], group_cache: dict[int, Group]
173+
self,
174+
event: dict[str, Any],
175+
group_cache: dict[int, Group],
176+
additional_attributes: list[str] | None = None,
173177
) -> SerializedEvent | SerializedIssue:
174178
if event.get("event_type") == "span":
179+
attribute_dict = {
180+
attribute: event[attribute]
181+
for attribute in additional_attributes or []
182+
if attribute in event
183+
}
175184
return SerializedSpan(
176185
children=[
177-
self.serialize_rpc_event(child, group_cache) for child in event["children"]
186+
self.serialize_rpc_event(child, group_cache, additional_attributes)
187+
for child in event["children"]
178188
],
179189
errors=[self.serialize_rpc_issue(error, group_cache) for error in event["errors"]],
180190
occurrences=[
@@ -204,6 +214,7 @@ def serialize_rpc_event(
204214
op=event["span.op"],
205215
name=event["span.name"],
206216
event_type="span",
217+
additional_attributes=attribute_dict,
207218
)
208219
else:
209220
return self.serialize_rpc_issue(event, group_cache)
@@ -306,7 +317,11 @@ def run_perf_issues_query(self, occurrence_query: DiscoverQueryBuilder):
306317

307318
@sentry_sdk.tracing.trace
308319
def query_trace_data(
309-
self, snuba_params: SnubaParams, trace_id: str, error_id: str | None = None
320+
self,
321+
snuba_params: SnubaParams,
322+
trace_id: str,
323+
error_id: str | None = None,
324+
additional_attributes: list[str] | None = None,
310325
) -> list[SerializedEvent]:
311326
"""Queries span/error data for a given trace"""
312327
# This is a hack, long term EAP will store both errors and performance_issues eventually but is not ready
@@ -326,6 +341,7 @@ def query_trace_data(
326341
snuba_params,
327342
Referrer.API_TRACE_VIEW_GET_EVENTS.value,
328343
SearchResolverConfig(),
344+
additional_attributes,
329345
)
330346
errors_future = _query_thread_pool.submit(
331347
self.run_errors_query,
@@ -376,7 +392,9 @@ def query_trace_data(
376392
for errors in id_to_error.values():
377393
result.extend(errors)
378394
group_cache: dict[int, Group] = {}
379-
return [self.serialize_rpc_event(root, group_cache) for root in result]
395+
return [
396+
self.serialize_rpc_event(root, group_cache, additional_attributes) for root in result
397+
]
380398

381399
def has_feature(self, organization: Organization, request: Request) -> bool:
382400
return bool(
@@ -393,6 +411,8 @@ def get(self, request: Request, organization: Organization, trace_id: str) -> Ht
393411
except NoProjects:
394412
return Response(status=404)
395413

414+
additional_attributes = request.GET.getlist("additional_attributes", [])
415+
396416
update_snuba_params_with_timestamp(request, snuba_params)
397417

398418
error_id = request.GET.get("errorId")
@@ -402,7 +422,9 @@ def get(self, request: Request, organization: Organization, trace_id: str) -> Ht
402422
def data_fn(offset: int, limit: int) -> list[SerializedEvent]:
403423
"""offset and limit don't mean anything on this endpoint currently"""
404424
with handle_query_errors():
405-
spans = self.query_trace_data(snuba_params, trace_id, error_id)
425+
spans = self.query_trace_data(
426+
snuba_params, trace_id, error_id, additional_attributes
427+
)
406428
return spans
407429

408430
return self.paginate(

src/sentry/snuba/spans_rpc.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,11 @@ def run_trace_query(
182182
params: SnubaParams,
183183
referrer: str,
184184
config: SearchResolverConfig,
185+
additional_attributes: list[str] | None = None,
185186
) -> list[dict[str, Any]]:
187+
if additional_attributes is None:
188+
additional_attributes = []
189+
186190
trace_attributes = [
187191
"parent_span",
188192
"description",
@@ -201,6 +205,7 @@ def run_trace_query(
201205
"sdk.name",
202206
"measurements.time_to_initial_display",
203207
"measurements.time_to_full_display",
208+
*additional_attributes,
204209
]
205210
for key in {
206211
"lcp",

tests/snuba/api/endpoints/test_organization_events_trace.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ def load_trace(self, is_eap=False):
5656
"description": f"GET gen1-{i}",
5757
"span_id": root_span_id,
5858
"trace_id": self.trace_id,
59+
"data": {
60+
"gen_ai.request.model": "gpt-4o",
61+
"gen_ai.usage.total_tokens": 100,
62+
},
5963
}
6064
for i, root_span_id in enumerate(self.root_span_ids)
6165
],

tests/snuba/api/endpoints/test_organization_trace.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,29 @@ def test_with_only_errors(self):
234234
assert len(data) == 1
235235
assert data[0]["event_id"] == error.event_id
236236

237+
def test_with_additional_attributes(self):
238+
self.load_trace(is_eap=True)
239+
with self.feature(self.FEATURES):
240+
response = self.client_get(
241+
data={
242+
"timestamp": self.day_ago,
243+
"additional_attributes": [
244+
"gen_ai.request.model",
245+
"gen_ai.usage.total_tokens",
246+
],
247+
},
248+
)
249+
assert response.status_code == 200, response.content
250+
data = response.data
251+
assert len(data) == 1
252+
253+
# The root span doesn't have any of the additional attributes and returns defaults
254+
assert data[0]["additional_attributes"]["gen_ai.request.model"] == ""
255+
assert data[0]["additional_attributes"]["gen_ai.usage.total_tokens"] == 0
256+
257+
assert data[0]["children"][0]["additional_attributes"]["gen_ai.request.model"] == "gpt-4o"
258+
assert data[0]["children"][0]["additional_attributes"]["gen_ai.usage.total_tokens"] == 100
259+
237260
def test_with_target_error(self):
238261
start, _ = self.get_start_end_from_day_ago(1000)
239262
error_data = load_data(

0 commit comments

Comments
 (0)