Skip to content

Commit b4c5121

Browse files
DominikB2014claude
andauthored
feat(slack): Pass display type from Explore URL to chartcuterie (#112620)
Parses the `chartType` from the `visualize` (or `aggregateField`) JSON URL param in Explore Slack unfurls and passes it as the `type` field in the chart data sent to chartcuterie. This enables rendering bar and area charts in Slack unfurls, matching the user's selected chart type in Explore. Also updates `map_explore_query_args` to prefer the `visualize` param (used by spans explore) over `aggregateField` (used by logs explore), since the traces explore page uses `visualize`. The numeric `chartType` values from the frontend (0=bar, 1=line, 2=area) are mapped to `DisplayType` string values expected by chartcuterie. When no `chartType` is present, the `type` field is omitted from chart data, preserving backward compatibility (chartcuterie defaults to line). Refs DAIN-1481 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 019fd4d commit b4c5121

File tree

2 files changed

+75
-6
lines changed

2 files changed

+75
-6
lines changed

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

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def _unfurl_explore(
8181
continue
8282

8383
params = link.args["query"]
84+
chart_type = link.args.get("chart_type")
8485

8586
y_axes = params.getlist("yAxis")
8687
if not y_axes:
@@ -110,9 +111,11 @@ def _unfurl_explore(
110111
_logger.warning("Failed to load events-timeseries for explore unfurl")
111112
continue
112113

113-
chart_data = {
114+
chart_data: dict[str, Any] = {
114115
"timeSeries": resp.data.get("timeSeries", []),
115116
}
117+
if chart_type is not None:
118+
chart_data["type"] = chart_type
116119

117120
try:
118121
url = charts.generate_chart(style, chart_data)
@@ -138,27 +141,37 @@ def _unfurl_explore(
138141
return unfurls
139142

140143

144+
CHART_TYPE_TO_DISPLAY_TYPE = {
145+
0: "bar",
146+
1: "line",
147+
2: "area",
148+
}
149+
150+
141151
def map_explore_query_args(url: str, args: Mapping[str, str | None]) -> Mapping[str, Any]:
142152
"""
143153
Extracts explore arguments from the explore link's query string.
144-
Parses aggregateField JSON params to extract yAxes and groupBy.
154+
Parses visualize/aggregateField JSON params to extract yAxes, groupBy, and chartType.
145155
"""
146156
# Slack uses HTML escaped ampersands in its Event Links
147157
url = html.unescape(url)
148158
parsed_url = urlparse(url)
149159
raw_query = QueryDict(parsed_url.query)
150160

151-
# Parse aggregateField JSON params
152-
aggregate_fields = raw_query.getlist("aggregateField")
161+
# Parse visualize (spans explore) or aggregateField (logs explore) JSON params
162+
visualize_fields = raw_query.getlist("visualize") or raw_query.getlist("aggregateField")
153163
y_axes: list[str] = []
154164
group_bys: list[str] = []
155-
for field_json in aggregate_fields:
165+
chart_type: str | None = None
166+
for field_json in visualize_fields:
156167
try:
157168
parsed = json.loads(field_json)
158169
if "yAxes" in parsed and isinstance(parsed["yAxes"], list):
159170
y_axes.extend(parsed["yAxes"])
160171
if "groupBy" in parsed and parsed["groupBy"]:
161172
group_bys.append(parsed["groupBy"])
173+
if chart_type is None and "chartType" in parsed:
174+
chart_type = CHART_TYPE_TO_DISPLAY_TYPE.get(parsed["chartType"])
162175
except (json.JSONDecodeError, TypeError):
163176
continue
164177

@@ -178,7 +191,7 @@ def map_explore_query_args(url: str, args: Mapping[str, str | None]) -> Mapping[
178191
if values:
179192
query.setlist(param, values)
180193

181-
return dict(**args, query=query)
194+
return dict(**args, query=query, chart_type=chart_type)
182195

183196

184197
explore_traces_link_regex = re.compile(

tests/sentry/integrations/slack/test_unfurl.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@
198198
{
199199
"org_slug": "org1",
200200
"query": QueryDict("yAxis=avg(span.duration)&project=1&statsPeriod=24h"),
201+
"chart_type": None,
201202
},
202203
),
203204
),
@@ -208,6 +209,7 @@
208209
{
209210
"org_slug": "org1",
210211
"query": QueryDict("yAxis=count(span.duration)&statsPeriod=24h"),
212+
"chart_type": None,
211213
},
212214
),
213215
),
@@ -1669,3 +1671,57 @@ def test_unfurl_explore_end_to_end(
16691671
unfurls[url]
16701672
== SlackDiscoverMessageBuilder(title="Explore Traces", chart_url="chart-url").build()
16711673
)
1674+
1675+
@patch(
1676+
"sentry.integrations.slack.unfurl.explore.client.get",
1677+
)
1678+
@patch("sentry.charts.backend.generate_chart", return_value="chart-url")
1679+
def test_unfurl_explore_with_visualize_chart_type(
1680+
self, mock_generate_chart: MagicMock, mock_client_get: MagicMock
1681+
) -> None:
1682+
mock_client_get.return_value = MagicMock(data=self._build_mock_timeseries_response())
1683+
# visualize param with chartType=0 (bar)
1684+
url = f"https://sentry.io/organizations/{self.organization.slug}/explore/traces/?visualize=%7B%22yAxes%22%3A%5B%22avg(span.duration)%22%5D%2C%22chartType%22%3A0%7D&project={self.project.id}&statsPeriod=24h"
1685+
link_type, args = match_link(url)
1686+
1687+
if not args or not link_type:
1688+
raise AssertionError("Missing link_type/args")
1689+
1690+
assert link_type == LinkType.EXPLORE
1691+
1692+
links = [
1693+
UnfurlableUrl(url=url, args=args),
1694+
]
1695+
1696+
with self.feature(["organizations:data-browsing-widget-unfurl"]):
1697+
unfurls = link_handlers[link_type].fn(self.integration, links, self.user)
1698+
1699+
assert len(unfurls) == 1
1700+
assert len(mock_generate_chart.mock_calls) == 1
1701+
chart_data = mock_generate_chart.call_args[0][1]
1702+
assert chart_data["type"] == "bar"
1703+
1704+
@patch(
1705+
"sentry.integrations.slack.unfurl.explore.client.get",
1706+
)
1707+
@patch("sentry.charts.backend.generate_chart", return_value="chart-url")
1708+
def test_unfurl_explore_without_chart_type_omits_type(
1709+
self, mock_generate_chart: MagicMock, mock_client_get: MagicMock
1710+
) -> None:
1711+
mock_client_get.return_value = MagicMock(data=self._build_mock_timeseries_response())
1712+
url = f"https://sentry.io/organizations/{self.organization.slug}/explore/traces/?aggregateField=%7B%22yAxes%22%3A%5B%22avg(span.duration)%22%5D%7D&project={self.project.id}&statsPeriod=24h"
1713+
link_type, args = match_link(url)
1714+
1715+
if not args or not link_type:
1716+
raise AssertionError("Missing link_type/args")
1717+
1718+
links = [
1719+
UnfurlableUrl(url=url, args=args),
1720+
]
1721+
1722+
with self.feature(["organizations:data-browsing-widget-unfurl"]):
1723+
unfurls = link_handlers[link_type].fn(self.integration, links, self.user)
1724+
1725+
assert len(unfurls) == 1
1726+
chart_data = mock_generate_chart.call_args[0][1]
1727+
assert "type" not in chart_data

0 commit comments

Comments
 (0)