Skip to content

Commit ef92b1f

Browse files
authored
fix(autofix): Identify profiles by transaction name instead of span id (#97528)
Use a looser matching condition to find profiles. I think the spans dataset does not include error spans, breaking the old logic. Also refactor the profile fetching RPC tool to use the shared util and support continuous profiles.
1 parent f0949c7 commit ef92b1f

File tree

3 files changed

+62
-76
lines changed

3 files changed

+62
-76
lines changed

src/sentry/seer/autofix/autofix.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ def find_transaction_parent(span_id: str) -> str | None:
488488
event_node.update(
489489
{
490490
"title": f"{op} - {transaction_title}" if op else transaction_title,
491+
"transaction": transaction_title,
491492
"duration": duration_str,
492493
"profile_id": profile_id,
493494
"span_ids": span_ids, # Store for later use
@@ -629,9 +630,9 @@ def _get_profile_from_trace_tree(
629630
return None
630631

631632
events = trace_tree.get("events", [])
632-
event_span_id = event.data.get("contexts", {}).get("trace", {}).get("span_id")
633+
event_transaction_name = event.transaction
633634

634-
if not event_span_id:
635+
if not event_transaction_name:
635636
return None
636637

637638
# Flatten all events in the tree for easier traversal
@@ -645,18 +646,11 @@ def collect_all_events(node):
645646
for root_node in events:
646647
collect_all_events(root_node)
647648

648-
# Find the first transaction that contains the event's span ID
649-
# or has a span_id matching the event's span_id
649+
# Find the first transaction that matches the event's transaction name and has a profile
650650
matching_transaction = None
651651
for node in all_events:
652652
if node.get("is_transaction", False):
653-
# Check if this transaction's span_id matches the event_span_id
654-
if node.get("span_id") == event_span_id:
655-
matching_transaction = node
656-
break
657-
658-
# Check if this transaction contains the event_span_id in its span_ids
659-
if event_span_id in node.get("span_ids", []):
653+
if node.get("transaction") == event_transaction_name and node.get("profile_id"):
660654
matching_transaction = node
661655
break
662656

@@ -665,6 +659,7 @@ def collect_all_events(node):
665659
"[Autofix] No matching transaction with profile_id found for event",
666660
extra={
667661
"trace_to_search_id": event.trace_id,
662+
"event_transaction_name": event_transaction_name,
668663
"matching_transaction": matching_transaction,
669664
"project_slug": project.slug,
670665
"organization_slug": project.organization.slug,
@@ -688,7 +683,6 @@ def collect_all_events(node):
688683
start_ts = matching_transaction.get("precise_start_ts")
689684
end_ts = matching_transaction.get("precise_finish_ts")
690685

691-
# Fetch the profile data using the shared utility
692686
profile = fetch_profile_data(
693687
profile_id=profile_id,
694688
organization_id=project.organization_id,

src/sentry/seer/autofix/autofix_tools.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,35 @@
1-
import orjson
2-
31
from sentry import eventstore
42
from sentry.api.serializers import EventSerializer, serialize
5-
from sentry.profiles.utils import get_from_profiling_service
6-
from sentry.seer.explorer.utils import _convert_profile_to_execution_tree
3+
from sentry.seer.explorer.utils import _convert_profile_to_execution_tree, fetch_profile_data
74

85

9-
def get_profile_details(organization_id: int, project_id: int, profile_id: str):
10-
response = get_from_profiling_service(
11-
"GET",
12-
f"/organizations/{organization_id}/projects/{project_id}/profiles/{profile_id}",
13-
params={"format": "sample"},
6+
def get_profile_details(
7+
organization_id: int,
8+
project_id: int,
9+
profile_id: str,
10+
is_continuous: bool = False,
11+
precise_start_ts: float | None = None,
12+
precise_finish_ts: float | None = None,
13+
):
14+
profile = fetch_profile_data(
15+
profile_id=profile_id,
16+
organization_id=organization_id,
17+
project_id=project_id,
18+
start_ts=precise_start_ts,
19+
end_ts=precise_finish_ts,
20+
is_continuous=is_continuous,
1421
)
1522

16-
if response.status == 200:
17-
profile = orjson.loads(response.data)
23+
if profile:
1824
execution_tree = _convert_profile_to_execution_tree(profile)
19-
output = (
25+
return (
2026
None
2127
if not execution_tree
2228
else {
2329
"profile_matches_issue": True,
2430
"execution_tree": execution_tree,
2531
}
2632
)
27-
return output
2833

2934

3035
def get_error_event_details(project_id: int, event_id: str):

tests/sentry/seer/autofix/test_autofix.py

Lines changed: 38 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -981,21 +981,16 @@ class TestGetProfileFromTraceTree(APITestCase, SnubaTestCase):
981981
def test_get_profile_from_trace_tree(self, mock_get_from_profiling_service) -> None:
982982
"""
983983
Test the _get_profile_from_trace_tree method which finds a transaction
984-
that contains the event's span_id or has a matching span_id.
984+
that matches the event's transaction name and has a profile.
985985
"""
986-
# Setup mock event with span_id
986+
# Setup mock event with transaction name
987987
event = Mock()
988988
event.event_id = "error-event-id"
989989
event.trace_id = "1234567890abcdef1234567890abcdef"
990-
event.data = {
991-
"contexts": {
992-
"trace": {
993-
"span_id": "event-span-id",
994-
}
995-
}
996-
}
990+
event.transaction = "/api/users"
991+
event.data = {"transaction": "/api/users"}
997992

998-
# Create a mock trace tree with a transaction that includes the event span_id in its span_ids
993+
# Create a mock trace tree with a transaction that matches the event's transaction name
999994
profile_id = "profile123456789"
1000995
trace_tree = {
1001996
"trace_id": "1234567890abcdef1234567890abcdef",
@@ -1005,8 +1000,8 @@ def test_get_profile_from_trace_tree(self, mock_get_from_profiling_service) -> N
10051000
"span_id": "root-span-id",
10061001
"is_transaction": True,
10071002
"is_error": False,
1003+
"transaction": "/api/users",
10081004
"profile_id": profile_id,
1009-
"span_ids": ["some-span", "event-span-id", "another-span"],
10101005
"children": [
10111006
{
10121007
"event_id": "error-event-id",
@@ -1060,35 +1055,31 @@ def test_get_profile_from_trace_tree(self, mock_get_from_profiling_service) -> N
10601055
)
10611056

10621057
@patch("sentry.seer.explorer.utils.get_from_profiling_service")
1063-
def test_get_profile_from_trace_tree_matching_span_id(
1058+
def test_get_profile_from_trace_tree_matching_transaction_name(
10641059
self, mock_get_from_profiling_service
10651060
) -> None:
10661061
"""
1067-
Test _get_profile_from_trace_tree with a transaction whose own span_id
1068-
matches the event's span_id.
1062+
Test _get_profile_from_trace_tree with a transaction whose name
1063+
matches the event's transaction name.
10691064
"""
1070-
# Setup mock event with span_id
1065+
# Setup mock event with transaction name
10711066
event = Mock()
10721067
event.event_id = "error-event-id"
10731068
event.trace_id = "1234567890abcdef1234567890abcdef"
1074-
event.data = {
1075-
"contexts": {
1076-
"trace": {
1077-
"span_id": "tx-span-id",
1078-
}
1079-
}
1080-
}
1069+
event.transaction = "/api/orders"
1070+
event.data = {"transaction": "/api/orders"}
10811071

1082-
# Create a mock trace tree with a transaction whose span_id matches event span_id
1072+
# Create a mock trace tree with a transaction whose name matches event transaction name
10831073
profile_id = "profile123456789"
10841074
trace_tree = {
10851075
"trace_id": "1234567890abcdef1234567890abcdef",
10861076
"events": [
10871077
{
10881078
"event_id": "tx-id",
1089-
"span_id": "tx-span-id", # This matches the event's span_id
1079+
"span_id": "tx-span-id",
10901080
"is_transaction": True,
10911081
"is_error": False,
1082+
"transaction": "/api/orders", # This matches the event's transaction name
10921083
"profile_id": profile_id,
10931084
"children": [
10941085
{
@@ -1149,11 +1140,13 @@ def test_get_profile_from_trace_tree_continuous_e2e(
11491140
End-to-end: build a trace tree from span query results where the transaction has only
11501141
profiler.id (continuous profile). Then verify we fetch a continuous profile and return an execution tree.
11511142
"""
1152-
# Base error event whose span_id should be found within the transaction's spans
1143+
# Base error event whose transaction name should match the transaction in the trace
11531144
trace_id = "1234567890abcdef1234567890abcdef"
11541145
error_span_id = "bbbbbbbbbbbbbbbb" # 16-hex span id
11551146
tx_span_id = "aaaaaaaaaaaaaaaa" # 16-hex transaction span id
11561147
data = load_data("python")
1148+
# Set transaction to match what we'll have in the trace tree
1149+
data["transaction"] = "/api/test"
11571150
data.update({"contexts": {"trace": {"trace_id": trace_id, "span_id": error_span_id}}})
11581151
event = self.store_event(data=data, project_id=self.project.id)
11591152

@@ -1172,7 +1165,7 @@ def test_get_profile_from_trace_tree_continuous_e2e(
11721165
"precise.start_ts": tx_start,
11731166
"precise.finish_ts": tx_end,
11741167
"is_transaction": True,
1175-
"transaction": "Root",
1168+
"transaction": "/api/test",
11761169
"project.id": self.project.id,
11771170
"platform": "python",
11781171
"profile.id": None,
@@ -1267,19 +1260,14 @@ def test_get_profile_from_trace_tree_api_error(self, mock_get_from_profiling_ser
12671260
"""
12681261
Test the behavior when the profiling service API returns an error.
12691262
"""
1270-
# Setup mock event with span_id
1263+
# Setup mock event with transaction name
12711264
event = Mock()
12721265
event.event_id = "error-event-id"
12731266
event.trace_id = "1234567890abcdef1234567890abcdef"
1274-
event.data = {
1275-
"contexts": {
1276-
"trace": {
1277-
"span_id": "event-span-id",
1278-
}
1279-
}
1280-
}
1267+
event.transaction = "/api/test"
1268+
event.data = {"transaction": "/api/test"}
12811269

1282-
# Create a mock trace tree with a transaction that includes the event span_id
1270+
# Create a mock trace tree with a transaction that matches the event transaction name
12831271
profile_id = "profile123456789"
12841272
trace_tree = {
12851273
"trace_id": "1234567890abcdef1234567890abcdef",
@@ -1289,8 +1277,8 @@ def test_get_profile_from_trace_tree_api_error(self, mock_get_from_profiling_ser
12891277
"span_id": "root-span-id",
12901278
"is_transaction": True,
12911279
"is_error": False,
1280+
"transaction": "/api/test",
12921281
"profile_id": profile_id,
1293-
"span_ids": ["event-span-id"],
12941282
"children": [
12951283
{
12961284
"event_id": "error-event-id",
@@ -1327,19 +1315,14 @@ def test_get_profile_from_trace_tree_no_matching_transaction(
13271315
"""
13281316
Test that the function returns None when no matching transaction is found.
13291317
"""
1330-
# Setup mock event with span_id
1318+
# Setup mock event with transaction name
13311319
event = Mock()
13321320
event.event_id = "error-event-id"
13331321
event.trace_id = "1234567890abcdef1234567890abcdef"
1334-
event.data = {
1335-
"contexts": {
1336-
"trace": {
1337-
"span_id": "event-span-id",
1338-
}
1339-
}
1340-
}
1322+
event.transaction = "/api/different"
1323+
event.data = {"transaction": "/api/different"}
13411324

1342-
# Create a mock trace tree with a transaction that DOESN'T include the event span_id
1325+
# Create a mock trace tree with a transaction that DOESN'T match the event transaction name
13431326
trace_tree = {
13441327
"trace_id": "1234567890abcdef1234567890abcdef",
13451328
"events": [
@@ -1348,8 +1331,8 @@ def test_get_profile_from_trace_tree_no_matching_transaction(
13481331
"span_id": "root-span-id",
13491332
"is_transaction": True,
13501333
"is_error": False,
1334+
"transaction": "/api/other", # Doesn't match event's transaction name
13511335
"profile_id": "profile123456789",
1352-
"span_ids": ["different-span-id"], # Doesn't include event's span_id
13531336
"children": [
13541337
{
13551338
"event_id": "error-event-id",
@@ -1371,15 +1354,18 @@ def test_get_profile_from_trace_tree_no_matching_transaction(
13711354
mock_get_from_profiling_service.assert_not_called()
13721355

13731356
@patch("sentry.seer.explorer.utils.get_from_profiling_service")
1374-
def test_get_profile_from_trace_tree_no_span_id(self, mock_get_from_profiling_service) -> None:
1357+
def test_get_profile_from_trace_tree_no_transaction_name(
1358+
self, mock_get_from_profiling_service
1359+
) -> None:
13751360
"""
1376-
Test the behavior when the event doesn't have a span_id.
1361+
Test the behavior when the event doesn't have a transaction name.
13771362
"""
1378-
# Setup mock event WITHOUT span_id
1363+
# Setup mock event WITHOUT transaction name
13791364
event = Mock()
13801365
event.event_id = "error-event-id"
13811366
event.trace_id = "1234567890abcdef1234567890abcdef"
1382-
event.data = {"contexts": {"trace": {}}} # No span_id
1367+
event.transaction = None
1368+
event.data = {} # No transaction
13831369

13841370
# Create a mock trace tree
13851371
trace_tree = {
@@ -1390,6 +1376,7 @@ def test_get_profile_from_trace_tree_no_span_id(self, mock_get_from_profiling_se
13901376
"span_id": "tx-span-id",
13911377
"is_transaction": True,
13921378
"is_error": False,
1379+
"transaction": "/api/test",
13931380
"profile_id": "profile123456789",
13941381
"children": [],
13951382
}
@@ -1400,7 +1387,7 @@ def test_get_profile_from_trace_tree_no_span_id(self, mock_get_from_profiling_se
14001387
profile_result = _get_profile_from_trace_tree(trace_tree, event, self.project)
14011388

14021389
assert profile_result is None
1403-
# API should not be called if event has no span_id
1390+
# API should not be called if event has no transaction name
14041391
mock_get_from_profiling_service.assert_not_called()
14051392

14061393

0 commit comments

Comments
 (0)