Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0237649
chore(autofix): Remove unused autofix v1 UI (#116100)
Zylphrex May 22, 2026
868510e
feat: flags and rpc for frontend code search tool (#116098)
shruthilayaj May 22, 2026
8469657
feat(dashboards): Require metric_unit in AI tracemetrics aggregates (…
DominikB2014 May 22, 2026
1ab42d6
fix: add catch-all path to explore route and redirect to index (#116066)
adrianviquez May 22, 2026
8e21c10
feat(explore): heatmap tooltip trace links (#115925)
nikkikapadia May 22, 2026
70b73f9
ref(api): type nullable fields in the base group serializer (#116068)
cvxluo May 23, 2026
62e5505
ref(snuba): Stop dropping deprecated spans dataset in reset_snuba (#1…
phacops May 24, 2026
5ceaa60
fix(dashboards): Anchor Editors dropdown to the right edge of the tri…
skaasten May 25, 2026
84065b8
fix(dashboards): Stop widget header action clicks from bubbling (#116…
skaasten May 25, 2026
0b8586e
fix(cross-events): Correct styling based off date selection (#116124)
nsdeschenes May 25, 2026
3eb788a
fix(explore): cross events date selector allow 7d anytime within 30 d…
nikkikapadia May 25, 2026
3ffc1a7
fix(dashboards): propagate global filters in Open in Issues link (#11…
DominikB2014 May 25, 2026
65cb915
chore(autofix): Add log for autofix introspection reason (#116132)
Zylphrex May 25, 2026
d74d871
feat(tracemetrics): Open in Explore for metrics dashboard widgets (#1…
narsaynorath May 25, 2026
f05d511
feat(tracemetrics): Convert equation alias to full equation for queri…
narsaynorath May 25, 2026
3a5d766
feat(dashboards): Validate display type against dataset config (#115951)
DominikB2014 May 25, 2026
be7e6a7
fix(metrics): default to largest interval when using heatmaps visuali…
nikkikapadia May 25, 2026
2e4a45d
ref(slack): remove widget unfurl feature flags (#116128)
DominikB2014 May 25, 2026
49c48a7
feat(amplitude): track whether users are viewing sentry-built dashboa…
bcoe May 25, 2026
8271548
fix(relocation) Fix type errors when spawning a task (#116130)
markstory May 25, 2026
8c618b2
fix(ui): Increase dropdown z-index to appear above sidebar (#116139)
jameskeane May 25, 2026
4579792
feat(tracemetrics): Include equations in Add to Dashboard (#116141)
narsaynorath May 25, 2026
dc499f6
chore(autofix): Remove intelligence level from group ai autofix endpo…
Zylphrex May 25, 2026
0989018
chore(autofix): Remove old useAutofixData hook (#116103)
Zylphrex May 25, 2026
24da847
feat(trace-waterfall): Add "EAP JSON" debug button for superusers
mjq May 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/sentry/api/serializers/models/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class BaseGroupResponseOptional(TypedDict, total=False):

class BaseGroupSerializerResponse(BaseGroupResponseOptional):
id: str
shareId: str
shareId: str | None
shortId: str
title: str
culprit: str | None
Expand All @@ -134,7 +134,7 @@ class BaseGroupSerializerResponse(BaseGroupResponseOptional):
issueCategory: str
metadata: dict[str, Any]
numComments: int
assignedTo: ActorSerializerResponse
assignedTo: ActorSerializerResponse | None
isBookmarked: bool
isSubscribed: bool
subscriptionDetails: SubscriptionDetails | None
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/api/serializers/models/group_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ class _Filtered(TypedDict):
class StreamGroupSerializerSnubaResponse(TypedDict):
id: str
# from base response
shareId: NotRequired[str]
shareId: NotRequired[str | None]
shortId: NotRequired[str]
title: NotRequired[str]
culprit: NotRequired[str | None]
Expand All @@ -304,7 +304,7 @@ class StreamGroupSerializerSnubaResponse(TypedDict):
issueCategory: NotRequired[str]
metadata: NotRequired[dict[str, Any]]
numComments: NotRequired[int]
assignedTo: NotRequired[ActorSerializerResponse]
assignedTo: NotRequired[ActorSerializerResponse | None]
isBookmarked: NotRequired[bool]
isSubscribed: NotRequired[bool]
subscriptionDetails: NotRequired[SubscriptionDetails | None]
Expand Down
95 changes: 94 additions & 1 deletion src/sentry/api/serializers/rest_framework/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,77 @@ def is_table_display_type(display_type):
MAX_WIDGET_COLS = 6


_DEFAULT_CHART_AND_TABLE_TYPES: frozenset[int] = frozenset(
{
DashboardWidgetDisplayTypes.LINE_CHART,
DashboardWidgetDisplayTypes.AREA_CHART,
DashboardWidgetDisplayTypes.BAR_CHART,
DashboardWidgetDisplayTypes.TABLE,
DashboardWidgetDisplayTypes.BIG_NUMBER,
DashboardWidgetDisplayTypes.CATEGORICAL_BAR_CHART,
}
)


class DatasetConfig(TypedDict):
supported_display_types: frozenset[int]


# Per-dataset config mirroring the frontend dataset configs
# (``static/app/views/dashboards/datasetConfig/*.tsx``). A display type is
# allowed for a widget_type iff it appears in ``supported_display_types`` here.
DATASET_CONFIG: dict[int, DatasetConfig] = {
DashboardWidgetTypes.DISCOVER: {"supported_display_types": _DEFAULT_CHART_AND_TABLE_TYPES},
# ERROR_EVENTS is intentionally omitted: it's the ``create_widget`` default
# when a request omits widget_type, so any system display type a prebuilt
# config doesn't tag will land here. Without a config entry the validation
# falls through and lets the request pass.
DashboardWidgetTypes.TRANSACTION_LIKE: {
"supported_display_types": _DEFAULT_CHART_AND_TABLE_TYPES
},
DashboardWidgetTypes.RELEASE_HEALTH: {
"supported_display_types": _DEFAULT_CHART_AND_TABLE_TYPES
},
DashboardWidgetTypes.METRICS: {"supported_display_types": _DEFAULT_CHART_AND_TABLE_TYPES},
DashboardWidgetTypes.LOGS: {"supported_display_types": _DEFAULT_CHART_AND_TABLE_TYPES},
DashboardWidgetTypes.ISSUE: {
"supported_display_types": frozenset(
{
DashboardWidgetDisplayTypes.TABLE,
DashboardWidgetDisplayTypes.LINE_CHART,
DashboardWidgetDisplayTypes.AREA_CHART,
DashboardWidgetDisplayTypes.BAR_CHART,
}
)
},
DashboardWidgetTypes.SPANS: {
"supported_display_types": _DEFAULT_CHART_AND_TABLE_TYPES
| frozenset(
{
DashboardWidgetDisplayTypes.DETAILS,
DashboardWidgetDisplayTypes.SERVER_TREE,
# WHEEL is used by built-in performance-score widgets.
DashboardWidgetDisplayTypes.WHEEL,
}
)
},
DashboardWidgetTypes.TRACEMETRICS: {
"supported_display_types": frozenset(
{
DashboardWidgetDisplayTypes.LINE_CHART,
DashboardWidgetDisplayTypes.AREA_CHART,
DashboardWidgetDisplayTypes.BAR_CHART,
DashboardWidgetDisplayTypes.BIG_NUMBER,
DashboardWidgetDisplayTypes.CATEGORICAL_BAR_CHART,
}
)
},
DashboardWidgetTypes.PREPROD_APP_SIZE: {
"supported_display_types": frozenset({DashboardWidgetDisplayTypes.LINE_CHART})
},
}


class WidgetLayoutSerializer(CamelSnakeSerializer[Dashboard]):
"""Widget grid layout position and dimensions.

Expand Down Expand Up @@ -326,7 +397,24 @@ class DashboardWidgetSerializer(CamelSnakeSerializer[Dashboard]):
)

def validate_display_type(self, display_type):
return DashboardWidgetDisplayTypes.get_id_for_type_name(display_type)
display_type_id = DashboardWidgetDisplayTypes.get_id_for_type_name(display_type)

widget_type_name = self.context.get("widget_type")
if widget_type_name is not None and display_type_id is not None:
widget_type_id = DashboardWidgetTypes.get_id_for_type_name(widget_type_name)
config = DATASET_CONFIG.get(widget_type_id)
if config is not None and display_type_id not in config["supported_display_types"]:
supported_names = sorted(
DashboardWidgetDisplayTypes.get_type_name(d) or str(d)
for d in config["supported_display_types"]
)
raise serializers.ValidationError(
f"Display type '{display_type}' is not supported for the "
f"'{widget_type_name}' dataset. Supported display types: "
f"{', '.join(supported_names)}."
)

return display_type_id

def _validate_widget_type(self, data):
widget_type = DashboardWidgetTypes.get_id_for_type_name(data.get("widget_type"))
Expand Down Expand Up @@ -358,6 +446,11 @@ def to_internal_value(self, data):
queries_serializer = self.fields["queries"]
additional_context = {}

# Always reset; with ``many=True`` DRF reuses one child serializer
# instance across items, so stale values would otherwise leak between
# widgets in the same request.
self.context["widget_type"] = data.get("widget_type")

if data.get("display_type"):
additional_context["display_type"] = data.get("display_type")
if data.get("widget_type"):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@
logger = logging.getLogger(__name__)

TRACE_METRICS_GUIDANCE = """When generating widgets with `widget_type: "tracemetrics"`:
- Aggregates use a multi-argument form: `func(attribute, metric_name, metric_type)`.
- Aggregates use a required 4-argument form: `func(attribute, metric_name, metric_type, metric_unit)`.
- `attribute` must be `value` (the numeric value of the metric); no other attributes are supported at this time.
- `metric_name` is the metric's name as ingested (e.g. `my.app.latency`).
- `metric_type` is exactly one of `counter`, `gauge`, or `distribution`.
- Examples: `count(value, my.app.requests, counter)`, `avg(value, my.app.cpu, gauge)`, `p95(value, my.app.latency, distribution)`.
- Single-argument forms like `p50(my.metric)` are INVALID for tracemetrics.
- Before emitting a tracemetrics widget you MUST look up the metric's `metric_type` using available tools (e.g. by querying the tracemetrics dataset for distinct `metric.name`/`metric.type` values, or fetching trace-item attributes). Do NOT guess the type — if you cannot confirm it, pick a different dataset or omit the widget."""
- `metric_unit` is the metric's unit as ingested (e.g. `milliseconds`, `bytes`). Use `none` only when the metric has no unit.
- Each `metric_type` only accepts a specific set of aggregate functions. Using a function not listed for the metric's type will fail:
- `counter`: `sum`, `per_second`, `per_minute`.
- `gauge`: `avg`, `min`, `max`, `per_second`, `per_minute`.
- `distribution`: `p50`, `p75`, `p90`, `p95`, `p99`, `avg`, `min`, `max`, `sum`, `count`, `per_second`, `per_minute`.
- Examples: `sum(value, my.app.requests, counter, none)`, `avg(value, my.app.cpu, gauge, percent)`, `p95(value, my.app.latency, distribution, milliseconds)`.
- Before emitting a tracemetrics widget you MUST look up the metric's `metric_type` AND `metric_unit` using available tools (e.g. by querying the tracemetrics dataset for distinct `metric.name`/`metric.type`/`metric.unit` values, or fetching trace-item attributes). Do NOT guess the type or unit — if you cannot confirm both, pick a different dataset or omit the widget."""

CREATE_ON_PAGE_CONTEXT = (
"The user is on the dashboard generation page. This session must ONLY generate a dashboard "
Expand Down
33 changes: 19 additions & 14 deletions src/sentry/dashboards/models/generate_dashboard_artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,25 @@ class GeneratedWidgetQuery(BaseModel):
"values; for table widgets they become data columns alongside columns[]. Valid "
"aggregate function values vary by dataset type. Do not make up functions or use "
"unsupported functions.\n\n"
"For the 'tracemetrics' widget_type, aggregates have a SPECIAL multi-argument form: "
"`func(attribute, metric_name, metric_type)` where attribute must be `value` "
"(the numeric value of the metric; no other attributes are supported at this time), "
"metric_name is the metric's name as ingested, and "
"metric_type is one of 'counter', 'gauge', or 'distribution'. Examples: "
"`count(value, my.app.requests, counter)`, "
"`avg(value, my.app.cpu, gauge)`, "
"`p95(value, my.app.latency, distribution)`. "
"Allowed functions for tracemetrics: count, count_unique, sum, avg, max, min, "
"p50, p75, p90, p95, p99. The single-argument form like `p50(my.metric)` is INVALID "
"for tracemetrics — the metric_name and metric_type MUST be passed as separate "
"positional arguments. You MUST NOT guess the metric_name or metric_type; look them "
"up first using the available tools (e.g. by querying the tracemetrics dataset for "
"distinct `metric.name` and `metric.type` values, or fetching trace-item attributes)."
"For the 'tracemetrics' widget_type, aggregates use a required 4-argument form: "
"`func(attribute, metric_name, metric_type, metric_unit)` where attribute must be "
"`value` (the numeric value of the metric; no other attributes are supported at this "
"time), metric_name is the metric's name as ingested, metric_type is one of "
"'counter', 'gauge', or 'distribution', and metric_unit is the metric's unit as "
"ingested (e.g. 'milliseconds', 'bytes'); use 'none' only when the metric has no "
"unit. Examples: `sum(value, my.app.requests, counter, none)`, "
"`avg(value, my.app.cpu, gauge, percent)`, "
"`p95(value, my.app.latency, distribution, milliseconds)`. "
"Each metric_type only accepts a specific set of aggregate functions, and using a "
"function outside that set will fail:\n"
"- counter: sum, per_second, per_minute.\n"
"- gauge: avg, min, max, per_second, per_minute.\n"
"- distribution: p50, p75, p90, p95, p99, avg, min, max, sum, count, per_second, "
"per_minute.\n"
"You MUST NOT guess metric_name, metric_type, or metric_unit; look them up first "
"using the available tools (e.g. by querying the tracemetrics dataset for distinct "
"`metric.name`, `metric.type`, and `metric.unit` values, or fetching trace-item "
"attributes)."
),
)
columns: list[str] = Field(
Expand Down
6 changes: 2 additions & 4 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("organizations:dashboards-basic", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True, default=True)
# Enables custom editable dashboards
manager.add("organizations:dashboards-edit", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True, default=True)
# Enable unfurling of dashboard widgets in Slack
manager.add("organizations:dashboards-widget-unfurl", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Enable metrics enhanced performance for AM2+ customers as they transition from AM2 to AM3
manager.add("organizations:dashboards-metrics-transition", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable drilldown flow for dashboards
Expand Down Expand Up @@ -272,6 +270,8 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("organizations:seer-explorer-context-engine-fe-override-ui-flag", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable code editing tools in Seer Agent chat
manager.add("organizations:seer-explorer-chat-coding", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable sentry source code search tool
manager.add("organizations:seer-agent-source-code-search", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable code mode tools (sentry_api_search/execute) in Seer Agent
manager.add("organizations:seer-explorer-code-mode-tools", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable code mode tools for Slack-initiated Explorer sessions
Expand Down Expand Up @@ -346,8 +346,6 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("organizations:insights-alerts", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable data browsing heat map widget
manager.add("organizations:data-browsing-heat-map-widget", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable data browsing widget unfurl
manager.add("organizations:data-browsing-widget-unfurl", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enable public RPC endpoint for local seer development
manager.add("organizations:seer-public-rpc", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Organizations on the old usage-based (v0) Seer plan
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/integrations/slack/unfurl/dashboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def _unfurl_dashboards(
enabled_orgs = {
slug: org
for slug, org in orgs_by_slug.items()
if features.has("organizations:dashboards-widget-unfurl", org, actor=user)
if features.has("organizations:dashboards-basic", org, actor=user)
}
if not enabled_orgs:
return {}
Expand Down
3 changes: 1 addition & 2 deletions src/sentry/integrations/slack/unfurl/explore.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,11 +375,10 @@ def _unfurl_explore(
)
orgs_by_slug = {org.slug: org for org in organizations}

# Check if any org has the feature flag enabled before doing any work
enabled_orgs = {
slug: org
for slug, org in orgs_by_slug.items()
if features.has("organizations:data-browsing-widget-unfurl", org, actor=user)
if features.has("organizations:visibility-explore-view", org, actor=user)
}
if not enabled_orgs:
return {}
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/integrations/slack/webhooks/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,8 @@ def _get_unfurlable_links(

feature_flag = {
LinkType.DISCOVER: "organizations:discover-basic",
LinkType.EXPLORE: "organizations:data-browsing-widget-unfurl",
LinkType.DASHBOARDS: "organizations:dashboards-widget-unfurl",
LinkType.EXPLORE: "organizations:visibility-explore-view",
LinkType.DASHBOARDS: "organizations:dashboards-basic",
}.get(link_type)

if (
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/relocation/services/relocation_export/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def reply_with_export(

logger.info("SaaS -> SaaS relocation RelocationFile saved", extra=logger_data)

uploading_complete.apply_async(args=[relocation.uuid])
uploading_complete.apply_async(args=[str(relocation.uuid)])
logger.info("SaaS -> SaaS relocation next task scheduled", extra=logger_data)


Expand Down
14 changes: 14 additions & 0 deletions src/sentry/seer/agent/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,13 @@ def start_run(
):
chat_body["is_context_engine_enabled"] = override_ce_enable

if features.has(
"organizations:seer-agent-source-code-search",
self.organization,
actor=self.user,
):
chat_body["enable_frontend_code_search"] = True

if features.has("organizations:seer-run-mirror-explorer", self.organization):
user_id = (
self.user.id
Expand Down Expand Up @@ -542,6 +549,13 @@ def continue_run(
if _has_context_engine(self.organization, self.user):
chat_body["is_context_engine_enabled"] = True

if features.has(
"organizations:seer-agent-source-code-search",
self.organization,
actor=self.user,
):
chat_body["enable_frontend_code_search"] = True

response = make_agent_chat_request(chat_body, viewer_context=self.viewer_context)

if response.status >= 400:
Expand Down
1 change: 1 addition & 0 deletions src/sentry/seer/agent/client_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class AgentChatRequest(TypedDict):
category_value: NotRequired[str]
metadata: NotRequired[dict[str, Any]]
is_context_engine_enabled: NotRequired[bool]
enable_frontend_code_search: NotRequired[bool]
max_iterations: NotRequired[int]
proxy_headers: NotRequired[dict[str, str] | None]
ui_tools: NotRequired[str | None]
Expand Down
13 changes: 13 additions & 0 deletions src/sentry/seer/autofix/on_completion_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,19 @@ def _maybe_continue_pipeline(
reached_stopping_point=reached_stopping_point,
)
)
logger.info(
"autofix.on_completion_hook.introspection",
extra={
"organization_id": organization.id,
"project_id": group.project_id,
"group_id": group.id,
"referrer": referrer.value,
"step": current_step.value,
"action": decision.action.value,
"reason": decision.reason,
"reached_stopping_point": reached_stopping_point,
},
)

if stopping_point is None or reached_stopping_point:
# We've reached the stopping point
Expand Down
8 changes: 1 addition & 7 deletions src/sentry/seer/endpoints/group_ai_autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,6 @@ class ExplorerAutofixRequestSerializer(CamelSnakeSerializer):
required=False,
help_text="Coding agent provider (e.g., 'github_copilot'). Alternative to integration_id for user-authenticated providers.",
)
intelligence_level = serializers.ChoiceField(
required=False,
choices=["low", "medium", "high"],
default="medium",
help_text="The intelligence level to use.",
)
user_context = serializers.CharField(
required=False,
max_length=1000,
Expand Down Expand Up @@ -328,7 +322,7 @@ def _post_agent(self, request: Request, group: Group) -> Response:
referrer=_parse_autofix_referrer(data.get("referrer")),
stopping_point=AutofixStoppingPoint(stopping_point) if stopping_point else None,
run_id=run_id,
intelligence_level=data["intelligence_level"],
intelligence_level="medium",
user_context=data.get("user_context"),
insert_index=data.get("insert_index"),
)
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/seer/endpoints/organization_seer_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
get_attributes_and_values,
get_attributes_for_span,
get_github_enterprise_integration_config,
get_organization_features,
get_organization_project_ids,
get_organization_slug,
has_repo_code_mappings,
Expand All @@ -87,6 +88,7 @@
# Common to Seer features
"get_organization_project_ids": map_org_id_param(get_organization_project_ids),
"get_organization_slug": map_org_id_param(get_organization_slug),
"get_organization_features": map_org_id_param(get_organization_features),
"validate_repo": validate_repo,
"get_github_enterprise_integration_config": get_github_enterprise_integration_config,
#
Expand Down
Loading
Loading