Skip to content

Commit 359e13a

Browse files
committed
eat(trace): Add errors data to the response of the trace
- This adds a query to the old discover table for errors data for a given trace then zips that back together with spans data that we've gotten from EAP - Longterm errors data will also come from EAP but that's not ready yet
1 parent 67000b5 commit 359e13a

File tree

3 files changed

+141
-37
lines changed

3 files changed

+141
-37
lines changed

src/sentry/api/endpoints/organization_trace.py

+104-33
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
from concurrent.futures import ThreadPoolExecutor
12
from datetime import datetime
2-
from typing import Any, TypedDict
3+
from typing import Any, NotRequired, TypedDict
34

45
import sentry_sdk
56
from django.http import HttpRequest, HttpResponse
@@ -16,29 +17,36 @@
1617
from sentry.models.project import Project
1718
from sentry.organizations.services.organization import RpcOrganization
1819
from sentry.search.eap.types import SearchResolverConfig
19-
from sentry.search.events.types import SnubaParams
20+
from sentry.search.events.builder.discover import DiscoverQueryBuilder
21+
from sentry.search.events.types import QueryBuilderConfig, SnubaParams
22+
from sentry.snuba.dataset import Dataset
2023
from sentry.snuba.referrer import Referrer
2124
from sentry.snuba.spans_rpc import run_trace_query
2225

26+
# 1 worker each for spans, errors, performance issues
27+
_query_thread_pool = ThreadPoolExecutor(max_workers=3)
28+
2329

2430
class SerializedEvent(TypedDict):
25-
children: list["SerializedEvent"]
31+
description: str
2632
event_id: str
27-
parent_span_id: str | None
33+
event_type: str
34+
is_transaction: bool
2835
project_id: int
2936
project_slug: str
30-
start_timestamp: datetime | None
31-
end_timestamp: datetime | None
37+
start_timestamp: datetime
3238
transaction: str
33-
description: str
34-
duration: float
35-
is_transaction: bool
36-
op: str
37-
event_type: str
39+
children: NotRequired[list["SerializedEvent"]]
40+
duration: NotRequired[float]
41+
end_timestamp: NotRequired[datetime]
42+
op: NotRequired[str]
43+
parent_span_id: NotRequired[str | None]
3844

3945

4046
@region_silo_endpoint
4147
class OrganizationTraceEndpoint(OrganizationEventsV2EndpointBase):
48+
"""Replaces OrganizationEventsTraceEndpoint"""
49+
4250
publish_status = {
4351
"GET": ApiPublishStatus.PRIVATE,
4452
}
@@ -65,37 +73,100 @@ def get_projects(
6573
include_all_accessible=True,
6674
)
6775

68-
def serialize_rpc_span(self, span: dict[str, Any]) -> SerializedEvent:
69-
return SerializedEvent(
70-
children=[self.serialize_rpc_span(child) for child in span["children"]],
71-
event_id=span["id"],
72-
project_id=span["project.id"],
73-
project_slug=span["project.slug"],
74-
parent_span_id=None if span["parent_span"] == "0" * 16 else span["parent_span"],
75-
start_timestamp=span["precise.start_ts"],
76-
end_timestamp=span["precise.finish_ts"],
77-
duration=span["span.duration"],
78-
transaction=span["transaction"],
79-
is_transaction=span["is_transaction"],
80-
description=span["description"],
81-
op=span["span.op"],
82-
event_type="span",
76+
def serialize_rpc_event(self, event: dict[str, Any]) -> SerializedEvent:
77+
if event.get("event_type") == "error":
78+
return SerializedEvent(
79+
event_id=event["id"],
80+
project_id=event["project.id"],
81+
project_slug=event["project.name"],
82+
start_timestamp=event["timestamp"],
83+
is_transaction=False,
84+
transaction=event["transaction"],
85+
description=event["message"],
86+
event_type="error",
87+
)
88+
elif event.get("event_type") == "span":
89+
return SerializedEvent(
90+
children=[self.serialize_rpc_event(child) for child in event["children"]],
91+
event_id=event["id"],
92+
project_id=event["project.id"],
93+
project_slug=event["project.slug"],
94+
parent_span_id=None if event["parent_span"] == "0" * 16 else event["parent_span"],
95+
start_timestamp=event["precise.start_ts"],
96+
end_timestamp=event["precise.finish_ts"],
97+
duration=event["span.duration"],
98+
transaction=event["transaction"],
99+
is_transaction=event["is_transaction"],
100+
description=event["description"],
101+
op=event["span.op"],
102+
event_type="span",
103+
)
104+
else:
105+
raise Exception(f"Unknown event encountered in trace: {event.get('event_type')}")
106+
107+
def run_errors_query(self, snuba_params: SnubaParams, trace_id: str):
108+
"""Run an error query, getting all the errors for a given trace id"""
109+
# TODO: replace this with EAP calls, this query is copied from the old trace view
110+
error_query = DiscoverQueryBuilder(
111+
Dataset.Events,
112+
params={},
113+
snuba_params=snuba_params,
114+
query=f"trace:{trace_id}",
115+
selected_columns=[
116+
"id",
117+
"project.name",
118+
"project.id",
119+
"timestamp",
120+
"trace.span",
121+
"transaction",
122+
"issue",
123+
"title",
124+
"message",
125+
"tags[level]",
126+
],
127+
# Don't add timestamp to this orderby as snuba will have to split the time range up and make multiple queries
128+
orderby=["id"],
129+
limit=10_000,
130+
config=QueryBuilderConfig(
131+
auto_fields=True,
132+
),
83133
)
134+
result = error_query.run_query(Referrer.API_TRACE_VIEW_GET_EVENTS.value)
135+
error_data = error_query.process_results(result)["data"]
136+
for event in error_data:
137+
event["event_type"] = "error"
138+
return error_data
84139

85140
@sentry_sdk.tracing.trace
86141
def query_trace_data(self, snuba_params: SnubaParams, trace_id: str) -> list[SerializedEvent]:
87-
trace_data = run_trace_query(
88-
trace_id, snuba_params, Referrer.API_TRACE_VIEW_GET_EVENTS.value, SearchResolverConfig()
142+
"""Queries span/error data for a given trace"""
143+
# This is a hack, long term EAP will store both errors and performance_issues eventually but is not ready
144+
# currently. But we want to move performance data off the old tables immediately. To keep the code simpler I'm
145+
# parallelizing the queries here, but ideally this parallelization lives in the spans_rpc module instead
146+
spans_future = _query_thread_pool.submit(
147+
run_trace_query,
148+
trace_id,
149+
snuba_params,
150+
Referrer.API_TRACE_VIEW_GET_EVENTS.value,
151+
SearchResolverConfig(),
89152
)
153+
errors_future = _query_thread_pool.submit(self.run_errors_query, snuba_params, trace_id)
154+
spans_data = spans_future.result()
155+
errors_data = errors_future.result()
156+
90157
result = []
91-
id_to_event = {event["id"]: event for event in trace_data}
92-
for span in trace_data:
93-
if span["parent_span"] in id_to_event:
94-
parent = id_to_event[span["parent_span"]]
158+
id_to_span = {event["id"]: event for event in spans_data}
159+
id_to_error = {event["trace.span"]: event for event in errors_data}
160+
for span in spans_data:
161+
if span["parent_span"] in id_to_span:
162+
parent = id_to_span[span["parent_span"]]
95163
parent["children"].append(span)
96164
else:
97165
result.append(span)
98-
return [self.serialize_rpc_span(root) for root in result]
166+
if span["id"] in id_to_error:
167+
error = id_to_error[span["id"]]
168+
span["children"].append(error)
169+
return [self.serialize_rpc_event(root) for root in result]
99170

100171
def has_feature(self, organization: Organization, request: Request) -> bool:
101172
return bool(

src/sentry/snuba/spans_rpc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ def run_trace_query(
445445
columns_by_name = {col.proto_definition.name: col for col in columns}
446446
for item_group in response.item_groups:
447447
for span_item in item_group.items:
448-
span: dict[str, Any] = {"id": span_item.id, "children": []}
448+
span: dict[str, Any] = {"id": span_item.id, "children": [], "event_type": "span"}
449449
for attribute in span_item.attributes:
450450
resolved_column = columns_by_name[attribute.key.name]
451451
if resolved_column.proto_definition.type == STRING:

tests/snuba/api/endpoints/test_organization_trace.py

+36-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from django.urls import reverse
44

5+
from sentry.utils.samples import load_data
56
from tests.snuba.api.endpoints.test_organization_events_trace import (
67
OrganizationEventsTraceEndpointBase,
78
)
@@ -23,17 +24,17 @@ def get_transaction_children(self, event):
2324
for child in event["children"]:
2425
if child["is_transaction"]:
2526
children.append(child)
26-
else:
27+
elif child["event_type"] == "span":
2728
children.extend(child["children"])
2829
return sorted(children, key=lambda event: event["description"])
2930

30-
def assert_trace_data(self, root, gen2_no_children=True):
31+
def assert_trace_data(self, root, gen2_no_children=True, has_error=False):
3132
"""see the setUp docstring for an idea of what the response structure looks like"""
3233
self.assert_event(root, self.root_event, "root")
3334
assert root["parent_span_id"] is None
3435
assert root["duration"] == 3000
3536
# 3 transactions, 2 child spans
36-
assert len(root["children"]) == 5
37+
assert len(root["children"]) == (5 if not has_error else 6)
3738
transaction_children = self.get_transaction_children(root)
3839
assert len(transaction_children) == 3
3940
self.assert_performance_issues(root)
@@ -112,3 +113,35 @@ def test_ignore_project_param(self):
112113
data = response.data
113114
assert len(data) == 1
114115
self.assert_trace_data(data[0])
116+
117+
def test_with_errors_data(self):
118+
self.load_trace(is_eap=True)
119+
start, _ = self.get_start_end_from_day_ago(1000)
120+
error_data = load_data(
121+
"javascript",
122+
timestamp=start,
123+
)
124+
error_data["contexts"]["trace"] = {
125+
"type": "trace",
126+
"trace_id": self.trace_id,
127+
"span_id": self.root_event.data["contexts"]["trace"]["span_id"],
128+
}
129+
error_data["tags"] = [["transaction", "/transaction/gen1-0"]]
130+
error = self.store_event(error_data, project_id=self.gen1_project.id)
131+
132+
with self.feature(self.FEATURES):
133+
response = self.client_get(
134+
data={},
135+
)
136+
assert response.status_code == 200, response.content
137+
data = response.data
138+
assert len(data) == 1
139+
self.assert_trace_data(data[0], has_error=True)
140+
error_event = None
141+
for child in data[0]["children"]:
142+
if child["event_type"] == "error":
143+
error_event = child
144+
break
145+
assert error_event is not None
146+
assert error_event["event_id"] == error.data["event_id"]
147+
assert error_event["project_slug"] == self.gen1_project.slug

0 commit comments

Comments
 (0)