Skip to content

Commit 6b8244a

Browse files
DominikB2014claude
andcommitted
feat(slack): Pass timeseries data directly to chartcuterie for Explore unfurls
Switch Explore Slack unfurls from converting timeseries data to EventsStats format to passing the /events-timeseries/ response directly to the new SLACK_EXPLORE_LINE chartcuterie render descriptor. This removes the unnecessary conversion step that converted timestamps from milliseconds to seconds only for chartcuterie to multiply them back to milliseconds, and wrapped values in [{count}] tuples only for them to be unwrapped again. Depends on #112584 for the SLACK_EXPLORE_LINE render descriptor. Refs DAIN-1483 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5d3818e commit 6b8244a

File tree

3 files changed

+21
-96
lines changed

3 files changed

+21
-96
lines changed

src/sentry/charts/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class ChartType(Enum):
2424
SLACK_PERFORMANCE_FUNCTION_REGRESSION = "slack:performance.functionRegression"
2525
SLACK_METRIC_DETECTOR_EVENTS = "slack:metricDetector.events"
2626
SLACK_METRIC_DETECTOR_SESSIONS = "slack:metricDetector.sessions"
27+
SLACK_EXPLORE_LINE = "slack:explore.line"
2728

2829

2930
class ChartSize(TypedDict):

src/sentry/integrations/slack/unfurl/explore.py

Lines changed: 5 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -34,78 +34,9 @@
3434

3535
DEFAULT_PERIOD = "14d"
3636
DEFAULT_Y_AXIS = "count(span.duration)"
37-
38-
# All `multiPlotType: line` fields in /static/app/utils/discover/fields.tsx
39-
LINE_PLOT_FIELDS = {
40-
"count_unique",
41-
"min",
42-
"max",
43-
"p50",
44-
"p75",
45-
"p90",
46-
"p95",
47-
"p99",
48-
"p100",
49-
"percentile",
50-
"avg",
51-
}
52-
5337
TOP_N = 5
5438

5539

56-
def _serialize_single_series(series: dict[str, Any]) -> dict[str, Any]:
57-
"""Convert a single TimeSeries into events-stats format."""
58-
values = series.get("values", [])
59-
data = []
60-
for row in values:
61-
# events-timeseries uses milliseconds, events-stats uses seconds
62-
timestamp = int(row["timestamp"] / 1000)
63-
data.append((timestamp, [{"count": row.get("value", 0)}]))
64-
65-
start = int(values[0]["timestamp"] / 1000) if values else 0
66-
end = int(values[-1]["timestamp"] / 1000) if values else 0
67-
68-
return {
69-
"data": data,
70-
"start": start,
71-
"end": end,
72-
"isMetricsData": False,
73-
}
74-
75-
76-
def timeseries_to_chart_data(
77-
resp_data: dict[str, Any], y_axis: str, has_groups: bool = False
78-
) -> dict[str, Any]:
79-
"""
80-
Converts an events-timeseries StatsResponse into the events-stats format
81-
that Chartcuterie expects.
82-
83-
For single series:
84-
{"data": [(timestamp_sec, [{"count": N}]), ...], "start": sec, "end": sec}
85-
86-
For top events (grouped):
87-
{"group_label": {"data": [...], "order": N, ...}, ...}
88-
"""
89-
time_series = resp_data.get("timeSeries", [])
90-
matching = [ts for ts in time_series if ts.get("yAxis") == y_axis]
91-
92-
if not matching:
93-
return {"data": [], "start": 0, "end": 0, "isMetricsData": False}
94-
95-
if has_groups:
96-
# Top events: return dict keyed by group label
97-
result = {}
98-
for i, ts in enumerate(matching):
99-
group_by = ts.get("groupBy", [])
100-
label = ",".join(str(g.get("value", "")) for g in group_by) if group_by else str(i)
101-
series_data = _serialize_single_series(ts)
102-
series_data["order"] = ts.get("meta", {}).get("order", i)
103-
result[label] = series_data
104-
return result
105-
106-
return _serialize_single_series(matching[0])
107-
108-
10940
def unfurl_explore(
11041
integration: Integration | RpcIntegration,
11142
links: list[UnfurlableUrl],
@@ -158,16 +89,9 @@ def _unfurl_explore(
15889

15990
group_bys = params.getlist("groupBy")
16091

161-
# Only one yAxis is charted; multiple charts per unfurl not yet supported.
92+
style = ChartType.SLACK_EXPLORE_LINE
16293
if group_bys:
163-
aggregate_fn = y_axes[-1].split("(")[0]
164-
if aggregate_fn in LINE_PLOT_FIELDS:
165-
style = ChartType.SLACK_DISCOVER_TOP5_PERIOD_LINE
166-
else:
167-
style = ChartType.SLACK_DISCOVER_TOP5_PERIOD
16894
params.setlist("topEvents", [str(TOP_N)])
169-
else:
170-
style = ChartType.SLACK_DISCOVER_TOTAL_PERIOD
17195

17296
if not params.get("statsPeriod") and not params.get("start"):
17397
params["statsPeriod"] = DEFAULT_PERIOD
@@ -186,11 +110,11 @@ def _unfurl_explore(
186110
_logger.warning("Failed to load events-timeseries for explore unfurl")
187111
continue
188112

189-
# QueryDict.items() sends only the last value per key to the API,
190-
# so we must match that by charting the last yAxis
191113
y_axis = y_axes[-1]
192-
stats = timeseries_to_chart_data(resp.data, y_axis, has_groups=bool(group_bys))
193-
chart_data = {"seriesName": y_axis, "stats": stats}
114+
chart_data = {
115+
"seriesName": y_axis,
116+
"timeSeries": resp.data.get("timeSeries", []),
117+
}
194118

195119
try:
196120
url = charts.generate_chart(style, chart_data)

tests/sentry/integrations/slack/test_unfurl.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,9 +1497,10 @@ def test_unfurl_explore(
14971497
== SlackDiscoverMessageBuilder(title="Explore Traces", chart_url="chart-url").build()
14981498
)
14991499
assert len(mock_generate_chart.mock_calls) == 1
1500-
assert mock_generate_chart.call_args[0][0] == ChartType.SLACK_DISCOVER_TOTAL_PERIOD
1500+
assert mock_generate_chart.call_args[0][0] == ChartType.SLACK_EXPLORE_LINE
15011501
chart_data = mock_generate_chart.call_args[0][1]
15021502
assert chart_data["seriesName"] == "avg(span.duration)"
1503+
assert "timeSeries" in chart_data
15031504

15041505
@patch(
15051506
"sentry.integrations.slack.unfurl.explore.client.get",
@@ -1546,8 +1547,7 @@ def test_unfurl_explore_with_groupby(
15461547

15471548
assert len(unfurls) == 1
15481549
assert len(mock_generate_chart.mock_calls) == 1
1549-
# avg is a line plot field, so top5line display mode
1550-
assert mock_generate_chart.call_args[0][0] == ChartType.SLACK_DISCOVER_TOP5_PERIOD_LINE
1550+
assert mock_generate_chart.call_args[0][0] == ChartType.SLACK_EXPLORE_LINE
15511551

15521552
@patch(
15531553
"sentry.integrations.slack.unfurl.explore.client.get",
@@ -1651,19 +1651,19 @@ def test_unfurl_explore_end_to_end(
16511651
chart_type = mock_generate_chart.call_args[0][0]
16521652
chart_data = mock_generate_chart.call_args[0][1]
16531653

1654-
assert chart_type == ChartType.SLACK_DISCOVER_TOTAL_PERIOD
1654+
assert chart_type == ChartType.SLACK_EXPLORE_LINE
16551655
assert chart_data["seriesName"] == "avg(span.duration)"
1656-
# Stats should be in events-stats format: {data: [(ts, [{count: N}]), ...], start, end}
1657-
stats = chart_data["stats"]
1658-
assert "data" in stats
1659-
assert "start" in stats
1660-
assert "end" in stats
1661-
assert len(stats["data"]) == INTERVALS_PER_DAY
1662-
# Each data point is (timestamp, [{count: value}])
1663-
first_point = stats["data"][0]
1664-
assert isinstance(first_point[0], int)
1665-
assert isinstance(first_point[1], list)
1666-
assert "count" in first_point[1][0]
1656+
# timeSeries should be passed through directly from the API response
1657+
time_series = chart_data["timeSeries"]
1658+
assert isinstance(time_series, list)
1659+
assert len(time_series) > 0
1660+
first_series = time_series[0]
1661+
assert first_series["yAxis"] == "avg(span.duration)"
1662+
assert len(first_series["values"]) == INTERVALS_PER_DAY
1663+
# Each data point has timestamp (ms) and value directly
1664+
first_point = first_series["values"][0]
1665+
assert "timestamp" in first_point
1666+
assert "value" in first_point
16671667

16681668
# Step 5: Verify the unfurl result
16691669
assert len(unfurls) == 1

0 commit comments

Comments
 (0)