From 22f50fea42429c519a50fdc2f48d4b02abdb5594 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Thu, 9 Apr 2026 16:46:32 -0400 Subject: [PATCH] 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 --- .../integrations/slack/unfurl/explore.py | 25 +++++++-- .../sentry/integrations/slack/test_unfurl.py | 56 +++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/sentry/integrations/slack/unfurl/explore.py b/src/sentry/integrations/slack/unfurl/explore.py index 5195adbb3a7f81..6ea2d0019d95d8 100644 --- a/src/sentry/integrations/slack/unfurl/explore.py +++ b/src/sentry/integrations/slack/unfurl/explore.py @@ -81,6 +81,7 @@ def _unfurl_explore( continue params = link.args["query"] + chart_type = link.args.get("chart_type") y_axes = params.getlist("yAxis") if not y_axes: @@ -110,9 +111,11 @@ def _unfurl_explore( _logger.warning("Failed to load events-timeseries for explore unfurl") continue - chart_data = { + chart_data: dict[str, Any] = { "timeSeries": resp.data.get("timeSeries", []), } + if chart_type is not None: + chart_data["type"] = chart_type try: url = charts.generate_chart(style, chart_data) @@ -138,27 +141,37 @@ def _unfurl_explore( return unfurls +CHART_TYPE_TO_DISPLAY_TYPE = { + 0: "bar", + 1: "line", + 2: "area", +} + + def map_explore_query_args(url: str, args: Mapping[str, str | None]) -> Mapping[str, Any]: """ Extracts explore arguments from the explore link's query string. - Parses aggregateField JSON params to extract yAxes and groupBy. + Parses visualize/aggregateField JSON params to extract yAxes, groupBy, and chartType. """ # Slack uses HTML escaped ampersands in its Event Links url = html.unescape(url) parsed_url = urlparse(url) raw_query = QueryDict(parsed_url.query) - # Parse aggregateField JSON params - aggregate_fields = raw_query.getlist("aggregateField") + # Parse visualize (spans explore) or aggregateField (logs explore) JSON params + visualize_fields = raw_query.getlist("visualize") or raw_query.getlist("aggregateField") y_axes: list[str] = [] group_bys: list[str] = [] - for field_json in aggregate_fields: + chart_type: str | None = None + for field_json in visualize_fields: try: parsed = json.loads(field_json) if "yAxes" in parsed and isinstance(parsed["yAxes"], list): y_axes.extend(parsed["yAxes"]) if "groupBy" in parsed and parsed["groupBy"]: group_bys.append(parsed["groupBy"]) + if chart_type is None and "chartType" in parsed: + chart_type = CHART_TYPE_TO_DISPLAY_TYPE.get(parsed["chartType"]) except (json.JSONDecodeError, TypeError): continue @@ -178,7 +191,7 @@ def map_explore_query_args(url: str, args: Mapping[str, str | None]) -> Mapping[ if values: query.setlist(param, values) - return dict(**args, query=query) + return dict(**args, query=query, chart_type=chart_type) explore_traces_link_regex = re.compile( diff --git a/tests/sentry/integrations/slack/test_unfurl.py b/tests/sentry/integrations/slack/test_unfurl.py index f8118c95366e37..a07bf848a9c6d5 100644 --- a/tests/sentry/integrations/slack/test_unfurl.py +++ b/tests/sentry/integrations/slack/test_unfurl.py @@ -198,6 +198,7 @@ { "org_slug": "org1", "query": QueryDict("yAxis=avg(span.duration)&project=1&statsPeriod=24h"), + "chart_type": None, }, ), ), @@ -208,6 +209,7 @@ { "org_slug": "org1", "query": QueryDict("yAxis=count(span.duration)&statsPeriod=24h"), + "chart_type": None, }, ), ), @@ -1669,3 +1671,57 @@ def test_unfurl_explore_end_to_end( unfurls[url] == SlackDiscoverMessageBuilder(title="Explore Traces", chart_url="chart-url").build() ) + + @patch( + "sentry.integrations.slack.unfurl.explore.client.get", + ) + @patch("sentry.charts.backend.generate_chart", return_value="chart-url") + def test_unfurl_explore_with_visualize_chart_type( + self, mock_generate_chart: MagicMock, mock_client_get: MagicMock + ) -> None: + mock_client_get.return_value = MagicMock(data=self._build_mock_timeseries_response()) + # visualize param with chartType=0 (bar) + 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" + link_type, args = match_link(url) + + if not args or not link_type: + raise AssertionError("Missing link_type/args") + + assert link_type == LinkType.EXPLORE + + links = [ + UnfurlableUrl(url=url, args=args), + ] + + with self.feature(["organizations:data-browsing-widget-unfurl"]): + unfurls = link_handlers[link_type].fn(self.integration, links, self.user) + + assert len(unfurls) == 1 + assert len(mock_generate_chart.mock_calls) == 1 + chart_data = mock_generate_chart.call_args[0][1] + assert chart_data["type"] == "bar" + + @patch( + "sentry.integrations.slack.unfurl.explore.client.get", + ) + @patch("sentry.charts.backend.generate_chart", return_value="chart-url") + def test_unfurl_explore_without_chart_type_omits_type( + self, mock_generate_chart: MagicMock, mock_client_get: MagicMock + ) -> None: + mock_client_get.return_value = MagicMock(data=self._build_mock_timeseries_response()) + 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" + link_type, args = match_link(url) + + if not args or not link_type: + raise AssertionError("Missing link_type/args") + + links = [ + UnfurlableUrl(url=url, args=args), + ] + + with self.feature(["organizations:data-browsing-widget-unfurl"]): + unfurls = link_handlers[link_type].fn(self.integration, links, self.user) + + assert len(unfurls) == 1 + chart_data = mock_generate_chart.call_args[0][1] + assert "type" not in chart_data