From 80fae32d5e44c51a24011af9043f6c9a7fb24bfd Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Mon, 13 May 2024 15:17:03 -0400 Subject: [PATCH] fix(trace-explorer): Merge parallel spans (#70681) Parallel spans can create a deeply nested timeline. This tries to merge them together for a cleaner flatten visualization. We're explicitly not merging root spans as that can create strange visualizations. --- .../api/endpoints/organization_traces.py | 35 ++- .../api/endpoints/test_organization_traces.py | 220 ++++++++++++++++++ 2 files changed, 244 insertions(+), 11 deletions(-) diff --git a/src/sentry/api/endpoints/organization_traces.py b/src/sentry/api/endpoints/organization_traces.py index ea3518afe78d82..42e2c0b8e8c5d9 100644 --- a/src/sentry/api/endpoints/organization_traces.py +++ b/src/sentry/api/endpoints/organization_traces.py @@ -41,6 +41,7 @@ class TraceInterval(TypedDict): kind: Literal["project", "missing", "other"] opCategory: str | None duration: int + isRoot: bool components: NotRequired[list[tuple[int, int]]] @@ -722,7 +723,7 @@ def get_traces_breakdown_projects_query( "precise.start_ts", "precise.finish_ts", ], - orderby=["precise.start_ts", "precise.finish_ts"], + orderby=["precise.start_ts", "-precise.finish_ts"], # limit the number of segments we fetch per trace so a single # large trace does not result in the rest being blank limitby=("trace", int(MAX_SNUBA_RESULTS / len(trace_ids))), @@ -760,10 +761,11 @@ def get_traces_breakdown_categories_query( "transaction", "span.category", "sdk.name", + "parent_span", "precise.start_ts", "precise.finish_ts", ], - orderby=["precise.start_ts", "precise.finish_ts"], + orderby=["precise.start_ts", "-precise.finish_ts"], # limit the number of segments we fetch per trace so a single # large trace does not result in the rest being blank limitby=("trace", int(MAX_SNUBA_RESULTS / len(trace_ids))), @@ -1000,7 +1002,12 @@ def process_breakdowns(data, traces_range): def should_merge(interval_a, interval_b): return ( - interval_a["end"] >= interval_b["start"] + # only merge intervals that have parent spans, i.e. those that aren't the trace root + not interval_a["isRoot"] + and not interval_b["isRoot"] + # only merge intervals that overlap + and interval_a["end"] >= interval_b["start"] + # only merge intervals that are part of the same service and interval_a["project"] == interval_b["project"] and interval_a["sdkName"] == interval_b["sdkName"] and interval_a["opCategory"] == interval_b["opCategory"] @@ -1032,14 +1039,16 @@ def breakdown_push(trace, interval): "components": [ (last_interval["components"][-1][1], interval["components"][0][0]), ], + "isRoot": False, } ) breakdown.append(interval) def stack_push(trace, interval): - last_interval = stack_peek(trace) - if last_interval and should_merge(last_interval, interval): + for last_interval in reversed(stacks[trace]): + if not should_merge(last_interval, interval): + continue # update the end of this interval and it will # be updated in the breakdown as well last_interval["end"] = max(interval["end"], last_interval["end"]) @@ -1107,7 +1116,14 @@ def stack_clear(trace, until=None): row["quantized.start_ts"] = quantized_start row["quantized.finish_ts"] = quantized_end - data.sort(key=lambda row: (row["quantized.start_ts"], -row["quantized.finish_ts"])) + data.sort( + key=lambda row: ( + row["quantized.start_ts"], + row["precise.start_ts"], + -row["quantized.finish_ts"], + -row["precise.finish_ts"], + ) + ) last_timestamp_per_trace: dict[str, int] = defaultdict(int) @@ -1131,6 +1147,7 @@ def stack_clear(trace, until=None): "end": row["quantized.finish_ts"], "duration": 0, "components": [(row["precise.start_ts"], row["precise.finish_ts"])], + "isRoot": not bool(row.get("parent_span")), } # Clear the stack of any intervals that end before the current interval @@ -1139,11 +1156,6 @@ def stack_clear(trace, until=None): stack_push(trace, cur) - # Clear the stack of any intervals that end before the current interval - # ends. Here we do not need to push them to the breakdowns because - # that time has already be attributed to the most recent interval. - stack_clear(trace, until=cur["end"]) - for trace, trace_range in traces_range.items(): # Check to see if there is still a gap before the trace ends and fill it # with an other interval. @@ -1158,6 +1170,7 @@ def stack_clear(trace, until=None): "start": other_start, "end": other_end, "duration": 0, + "isRoot": False, } # Clear the remaining intervals on the stack to find the latest end time diff --git a/tests/sentry/api/endpoints/test_organization_traces.py b/tests/sentry/api/endpoints/test_organization_traces.py index 3c63bddc49eb15..0cf9755d89ade4 100644 --- a/tests/sentry/api/endpoints/test_organization_traces.py +++ b/tests/sentry/api/endpoints/test_organization_traces.py @@ -458,6 +458,7 @@ def test_matching_tag(self): "project": project_1.slug, "opCategory": None, "sdkName": "sentry.javascript.node", + "isRoot": False, "start": int(timestamps[0].timestamp() * 1000), "end": int(timestamps[0].timestamp() * 1000) + 60_100, "kind": "project", @@ -467,6 +468,7 @@ def test_matching_tag(self): "project": project_2.slug, "opCategory": None, "sdkName": "sentry.javascript.node", + "isRoot": False, "start": int(timestamps[1].timestamp() * 1000), "end": int(timestamps[3].timestamp() * 1000) + 30_003, "kind": "project", @@ -513,6 +515,7 @@ def test_matching_tag(self): "project": project_1.slug, "opCategory": None, "sdkName": "sentry.javascript.node", + "isRoot": False, "start": int(timestamps[4].timestamp() * 1000), "end": int(timestamps[4].timestamp() * 1000) + 90_123, "kind": "project", @@ -522,6 +525,7 @@ def test_matching_tag(self): "project": project_2.slug, "opCategory": None, "sdkName": "sentry.javascript.node", + "isRoot": False, "start": int(timestamps[5].timestamp() * 1000), "end": int(timestamps[6].timestamp() * 1000) + 20_006, "kind": "project", @@ -611,6 +615,7 @@ def test_matching_tag_breakdown_with_category(self): "project": project_1.slug, "opCategory": None, "sdkName": "sentry.javascript.node", + "isRoot": False, "start": int(timestamps[4].timestamp() * 1000), "end": int(timestamps[4].timestamp() * 1000) + 90_123, "kind": "project", @@ -620,6 +625,7 @@ def test_matching_tag_breakdown_with_category(self): "project": project_1.slug, "opCategory": "http", "sdkName": "", + "isRoot": False, "start": int(timestamps[7].timestamp() * 1000), "end": int(timestamps[7].timestamp() * 1000) + 1_000, "kind": "project", @@ -629,6 +635,7 @@ def test_matching_tag_breakdown_with_category(self): "project": project_2.slug, "opCategory": None, "sdkName": "sentry.javascript.node", + "isRoot": False, "start": int(timestamps[5].timestamp() * 1000), "end": int(timestamps[6].timestamp() * 1000) + 20_006, "kind": "project", @@ -638,6 +645,7 @@ def test_matching_tag_breakdown_with_category(self): "project": project_1.slug, "opCategory": "db", "sdkName": "", + "isRoot": False, "start": int(timestamps[8].timestamp() * 1000), "end": int(timestamps[8].timestamp() * 1000) + 3_000, "kind": "project", @@ -723,6 +731,7 @@ def test_matching_tag_metrics(self): "project": project_1.slug, "opCategory": None, "sdkName": "sentry.javascript.remix", + "isRoot": False, "start": int(timestamps[10].timestamp() * 1000), "end": int(timestamps[10].timestamp() * 1000) + 40_000, "kind": "project", @@ -732,6 +741,7 @@ def test_matching_tag_metrics(self): "project": project_1.slug, "opCategory": None, "sdkName": "sentry.javascript.node", + "isRoot": False, "start": int(timestamps[11].timestamp() * 1000), "end": int(timestamps[11].timestamp() * 1000) + 10_000, "kind": "project", @@ -808,6 +818,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.1, @@ -825,6 +836,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 100, + "isRoot": False, }, ], }, @@ -836,6 +848,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.1, @@ -844,6 +857,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.025, "precise.finish_ts": 0.075, @@ -861,6 +875,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 100, + "isRoot": False, }, { "project": "bar", @@ -870,6 +885,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 75, "kind": "project", "duration": 50, + "isRoot": False, }, ], }, @@ -881,6 +897,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.05, @@ -889,6 +906,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.025, "precise.finish_ts": 0.075, @@ -897,6 +915,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "baz", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "baz1", "precise.start_ts": 0.05, "precise.finish_ts": 0.1, @@ -914,6 +933,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "project", "duration": 50, + "isRoot": False, }, { "project": "bar", @@ -923,6 +943,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 75, "kind": "project", "duration": 50, + "isRoot": False, }, { "project": "baz", @@ -932,6 +953,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 50, + "isRoot": False, }, ], }, @@ -943,6 +965,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.025, @@ -951,6 +974,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.05, "precise.finish_ts": 0.075, @@ -968,6 +992,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 25, "kind": "project", "duration": 25, + "isRoot": False, }, { "project": None, @@ -977,6 +1002,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "missing", "duration": 25, + "isRoot": False, }, { "project": "bar", @@ -986,6 +1012,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 75, "kind": "project", "duration": 25, + "isRoot": False, }, ], }, @@ -997,6 +1024,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.1, @@ -1005,6 +1033,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo2", "precise.start_ts": 0.025, "precise.finish_ts": 0.075, @@ -1022,6 +1051,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 100, + "isRoot": False, }, ], }, @@ -1033,6 +1063,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.075, @@ -1041,6 +1072,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo2", "precise.start_ts": 0.025, "precise.finish_ts": 0.1, @@ -1058,6 +1090,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 100, + "isRoot": False, }, ], }, @@ -1069,6 +1102,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.025, @@ -1077,6 +1111,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo2", "precise.start_ts": 0.05, "precise.finish_ts": 0.075, @@ -1094,6 +1129,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 25, "kind": "project", "duration": 25, + "isRoot": False, }, { "project": None, @@ -1103,6 +1139,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "missing", "duration": 25, + "isRoot": False, }, { "project": "foo", @@ -1112,6 +1149,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 75, "kind": "project", "duration": 25, + "isRoot": False, }, ], }, @@ -1123,6 +1161,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.1, @@ -1131,6 +1170,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.02, "precise.finish_ts": 0.08, @@ -1139,6 +1179,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "baz", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "baz1", "precise.start_ts": 0.04, "precise.finish_ts": 0.06, @@ -1156,6 +1197,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 100, + "isRoot": False, }, { "project": "bar", @@ -1165,6 +1207,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 80, "kind": "project", "duration": 60, + "isRoot": False, }, { "project": "baz", @@ -1174,6 +1217,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 60, "kind": "project", "duration": 20, + "isRoot": False, }, ], }, @@ -1185,6 +1229,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.1, @@ -1193,6 +1238,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.025, "precise.finish_ts": 0.05, @@ -1200,6 +1246,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): { "trace": "a" * 32, "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "project": "baz", "transaction": "baz1", "precise.start_ts": 0.05, @@ -1218,6 +1265,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 100, + "isRoot": False, }, { "project": "bar", @@ -1227,6 +1275,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "project", "duration": 25, + "isRoot": False, }, { "project": "baz", @@ -1236,6 +1285,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 75, "kind": "project", "duration": 25, + "isRoot": False, }, ], }, @@ -1247,6 +1297,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.05, @@ -1255,6 +1306,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.02, "precise.finish_ts": 0.03, @@ -1263,6 +1315,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "baz", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "baz1", "precise.start_ts": 0.05, "precise.finish_ts": 0.075, @@ -1280,6 +1333,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "project", "duration": 50, + "isRoot": False, }, { "project": "bar", @@ -1289,6 +1343,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 30, "kind": "project", "duration": 10, + "isRoot": False, }, { "project": "baz", @@ -1298,6 +1353,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 75, "kind": "project", "duration": 25, + "isRoot": False, }, ], }, @@ -1309,6 +1365,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.05, @@ -1317,6 +1374,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.02, "precise.finish_ts": 0.03, @@ -1325,6 +1383,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "baz", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "baz1", "precise.start_ts": 0.04, "precise.finish_ts": 0.06, @@ -1342,6 +1401,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "project", "duration": 50, + "isRoot": False, }, { "project": "bar", @@ -1351,6 +1411,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 30, "kind": "project", "duration": 10, + "isRoot": False, }, { "project": "baz", @@ -1360,6 +1421,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 60, "kind": "project", "duration": 20, + "isRoot": False, }, ], }, @@ -1371,6 +1433,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.05, @@ -1379,6 +1442,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.01, "precise.finish_ts": 0.02, @@ -1387,6 +1451,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0.03, "precise.finish_ts": 0.04, @@ -1404,6 +1469,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "project", "duration": 50, + "isRoot": False, }, { "project": "bar", @@ -1413,6 +1479,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 20, "kind": "project", "duration": 10, + "isRoot": False, }, ], }, @@ -1424,6 +1491,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.1, @@ -1441,6 +1509,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "project", "duration": 50, + "isRoot": False, }, ], }, @@ -1452,6 +1521,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.05, @@ -1469,6 +1539,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 40, "kind": "project", "duration": 50, + "isRoot": False, }, { "project": None, @@ -1478,6 +1549,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "other", "duration": 50, + "isRoot": False, }, ], }, @@ -1489,6 +1561,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.012, @@ -1497,6 +1570,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0.013, "precise.finish_ts": 0.024, @@ -1505,6 +1579,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0.032, "precise.finish_ts": 0.040, @@ -1522,6 +1597,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 20, "kind": "project", "duration": 23, + "isRoot": False, }, { "project": None, @@ -1531,6 +1607,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 30, "kind": "missing", "duration": 8, + "isRoot": False, }, { "project": "foo", @@ -1540,6 +1617,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 40, "kind": "project", "duration": 8, + "isRoot": False, }, ], }, @@ -1551,6 +1629,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.1, @@ -1559,6 +1638,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.002, "precise.finish_ts": 0.044, @@ -1567,6 +1647,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0.007, "precise.finish_ts": 0.1, @@ -1584,6 +1665,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 100, + "isRoot": False, }, { "project": "bar", @@ -1593,6 +1675,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 40, "kind": "project", "duration": 42, + "isRoot": False, }, ], }, @@ -1604,6 +1687,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.051, @@ -1612,6 +1696,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0.069, "precise.finish_ts": 0.1, @@ -1629,11 +1714,146 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 82, + "isRoot": False, }, ], }, id="merges nearby spans", ), + pytest.param( + [ + { + "trace": "a" * 32, + "project": "foo", + "sdk.name": "sentry.javascript.remix", + "transaction": "foo1", + "precise.start_ts": 0, + "precise.finish_ts": 0.1, + }, + { + "trace": "a" * 32, + "project": "bar", + "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, + "transaction": "bar", + "precise.start_ts": 0.02, + "precise.finish_ts": 0.06, + }, + { + "trace": "a" * 32, + "project": "foo", + "sdk.name": "sentry.javascript.remix", + "parent_span": "a" * 16, + "transaction": "foo1", + "precise.start_ts": 0.03, + "precise.finish_ts": 0.07, + }, + { + "trace": "a" * 32, + "project": "bar", + "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, + "transaction": "bar1", + "precise.start_ts": 0.04, + "precise.finish_ts": 0.08, + }, + { + "trace": "a" * 32, + "project": "foo", + "sdk.name": "sentry.javascript.remix", + "parent_span": "a" * 16, + "transaction": "foo1", + "precise.start_ts": 0.05, + "precise.finish_ts": 0.07, + }, + ], + {"a" * 32: (0, 100, 10)}, + { + "a" + * 32: [ + { + "project": "foo", + "opCategory": None, + "sdkName": "sentry.javascript.remix", + "start": 0, + "end": 100, + "kind": "project", + "duration": 100, + "isRoot": True, + }, + { + "project": "bar", + "opCategory": None, + "sdkName": "sentry.javascript.node", + "start": 20, + "end": 80, + "kind": "project", + "duration": 60, + "isRoot": False, + }, + { + "project": "foo", + "opCategory": None, + "sdkName": "sentry.javascript.remix", + "start": 30, + "end": 70, + "kind": "project", + "duration": 40, + "isRoot": False, + }, + ], + }, + id="merges spans at different depths", + ), + pytest.param( + [ + { + "trace": "a" * 32, + "project": "foo", + "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, + "transaction": "foo1", + "precise.start_ts": 0.003, + "precise.finish_ts": 0.097, + }, + { + "trace": "a" * 32, + "project": "foo", + "sdk.name": "sentry.javascript.remix", + "parent_span": "a" * 16, + "transaction": "foo1", + "precise.start_ts": 0.002, + "precise.finish_ts": 0.098, + }, + ], + {"a" * 32: (0, 100, 10)}, + { + "a" + * 32: [ + { + "project": "foo", + "opCategory": None, + "sdkName": "sentry.javascript.remix", + "start": 0, + "end": 100, + "kind": "project", + "duration": 96, + "isRoot": False, + }, + { + "project": "foo", + "opCategory": None, + "sdkName": "sentry.javascript.node", + "start": 0, + "end": 100, + "kind": "project", + "duration": 94, + "isRoot": False, + }, + ], + }, + id="orders spans by precise timestamps", + ), ], ) def test_process_breakdowns(data, traces_range, expected):