Skip to content

Commit 22f50fe

Browse files
DominikB2014claude
andcommitted
feat(slack): Pass display type from Explore URL to chartcuterie
Parse the chartType from the visualize (or aggregateField) JSON URL param in Explore Slack unfurls and pass it as the "type" field in the chart data sent to chartcuterie. This enables rendering bar and area charts in addition to line charts. The numeric chartType values (0=bar, 1=line, 2=area) from the frontend are mapped to DisplayType string values expected by chartcuterie. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2ce924b commit 22f50fe

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)