From 11b9f708d2ef8b39386716038971f2acacf842ac Mon Sep 17 00:00:00 2001
From: Andrei <168741329+andreiborza@users.noreply.github.com>
Date: Wed, 5 Mar 2025 14:23:43 +0100
Subject: [PATCH 01/39] chore(hybrid-cloud): Add logging to release registry
(#86374)
Adds logging to understand recent AWS integration failures.
See: https://github.com/getsentry/sentry/pull/62007
---
src/sentry/integrations/aws_lambda/utils.py | 12 ++++++++++++
src/sentry/tasks/release_registry.py | 15 +++++++++++++++
2 files changed, 27 insertions(+)
diff --git a/src/sentry/integrations/aws_lambda/utils.py b/src/sentry/integrations/aws_lambda/utils.py
index 6f0310ab99a636..de91e85d028b54 100644
--- a/src/sentry/integrations/aws_lambda/utils.py
+++ b/src/sentry/integrations/aws_lambda/utils.py
@@ -9,6 +9,7 @@
from sentry import options
from sentry.projects.services.project_key import ProjectKeyRole, project_key_service
from sentry.shared_integrations.exceptions import IntegrationError
+from sentry.silo.base import SiloMode
from sentry.tasks.release_registry import LAYER_INDEX_CACHE_KEY
SUPPORTED_RUNTIMES = [
@@ -113,6 +114,17 @@ def get_option_value(function, option):
key = f"aws-layer:{prefix}"
cache_value = cache_options.get(key)
+ if SiloMode.get_current_mode() == SiloMode.REGION:
+ with sentry_sdk.isolation_scope() as scope:
+ scope.set_context(
+ "aws_lambda_cache",
+ {
+ "cache_options": cache_options,
+ "key": key,
+ },
+ )
+ sentry_sdk.capture_message("Fetching aws layer from cache")
+
if cache_value is None:
raise IntegrationError(f"Could not find cache value with key {key}")
diff --git a/src/sentry/tasks/release_registry.py b/src/sentry/tasks/release_registry.py
index b8b3d48e2b620f..a817836438c23b 100644
--- a/src/sentry/tasks/release_registry.py
+++ b/src/sentry/tasks/release_registry.py
@@ -1,5 +1,6 @@
import logging
+import sentry_sdk
from django.conf import settings
from django.core.cache import cache
@@ -47,6 +48,11 @@ def fetch_release_registry_data(**kwargs):
More details about the registry: https://github.com/getsentry/sentry-release-registry/
"""
+ logger.info(
+ "release_registry.fetch.starting",
+ extra={"release_registry_baseurl": str(settings.SENTRY_RELEASE_REGISTRY_BASEURL)},
+ )
+
if not settings.SENTRY_RELEASE_REGISTRY_BASEURL:
logger.warning("Release registry URL is not specified, skipping the task.")
return
@@ -61,4 +67,13 @@ def fetch_release_registry_data(**kwargs):
# AWS Layers
layer_data = _fetch_registry_url("/aws-lambda-layers")
+ logger.info(
+ "release_registry.fetch.aws-lambda-layers",
+ extra={"layer_data": layer_data},
+ )
cache.set(LAYER_INDEX_CACHE_KEY, layer_data, CACHE_TTL)
+ logger.info(
+ "release_registry.fetch.aws-lambda-layers",
+ extra={"layer_cache": cache.get(LAYER_INDEX_CACHE_KEY)},
+ )
+ sentry_sdk.capture_message("Fetching release registry")
From d9b324975ce18b3f7d19892b75d56643aae5cefc Mon Sep 17 00:00:00 2001
From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com>
Date: Wed, 5 Mar 2025 08:48:03 -0500
Subject: [PATCH 02/39] ref: fix typing for organization_member.details
endpoint (#86349)
---
pyproject.toml | 1 -
src/sentry/api/endpoints/organization_member/details.py | 8 ++++++--
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index ebb6b3f0167208..177ae5dc358529 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -118,7 +118,6 @@ module = [
"sentry.api.endpoints.organization_events_facets_performance",
"sentry.api.endpoints.organization_events_meta",
"sentry.api.endpoints.organization_events_spans_performance",
- "sentry.api.endpoints.organization_member.details",
"sentry.api.endpoints.organization_projects",
"sentry.api.endpoints.organization_releases",
"sentry.api.endpoints.organization_request_project_creation",
diff --git a/src/sentry/api/endpoints/organization_member/details.py b/src/sentry/api/endpoints/organization_member/details.py
index 797af542797543..47a3a3ad5e1504 100644
--- a/src/sentry/api/endpoints/organization_member/details.py
+++ b/src/sentry/api/endpoints/organization_member/details.py
@@ -5,7 +5,7 @@
from django.db import router, transaction
from django.db.models import Q
from drf_spectacular.utils import extend_schema, inline_serializer
-from rest_framework import serializers
+from rest_framework import serializers, status
from rest_framework.exceptions import PermissionDenied
from rest_framework.request import Request
from rest_framework.response import Response
@@ -199,6 +199,9 @@ def put(
For example, an organization Manager may change someone's role from
Member to Manager, but not to Owner.
"""
+ if not request.user.is_authenticated:
+ return Response(status=status.HTTP_400_BAD_REQUEST)
+
allowed_roles = get_allowed_org_roles(request, organization)
serializer = OrganizationMemberRequestSerializer(
data=request.data,
@@ -245,6 +248,7 @@ def put(
if not is_reinvite_request_only:
return Response({"detail": ERR_EDIT_WHEN_REINVITING}, status=403)
if member.is_pending:
+ assert member.email is not None
if ratelimits.for_organization_member_invite(
organization=organization,
email=member.email,
@@ -270,7 +274,7 @@ def put(
return Response({"detail": ERR_EXPIRED}, status=400)
member.send_invite_email()
elif auth_provider and not getattr(member.flags, "sso:linked"):
- member.send_sso_link_email(request.user.id, auth_provider)
+ member.send_sso_link_email(request.user.email, auth_provider)
else:
# TODO(dcramer): proper error message
return Response({"detail": ERR_UNINVITABLE}, status=400)
From 26a06e9fdbce49b5ea6c6dc31ae43342215e69d4 Mon Sep 17 00:00:00 2001
From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com>
Date: Wed, 5 Mar 2025 08:48:10 -0500
Subject: [PATCH 03/39] ref: fix types for
endpoints.organization_incident_index (#86347)
---
pyproject.toml | 1 -
.../endpoints/organization_incident_index.py | 17 +++++++++--------
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 177ae5dc358529..5fe22aa2f855d3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -139,7 +139,6 @@ module = [
"sentry.incidents.endpoints.bases",
"sentry.incidents.endpoints.organization_alert_rule_details",
"sentry.incidents.endpoints.organization_alert_rule_index",
- "sentry.incidents.endpoints.organization_incident_index",
"sentry.integrations.aws_lambda.integration",
"sentry.integrations.bitbucket_server.integration",
"sentry.integrations.example.integration",
diff --git a/src/sentry/incidents/endpoints/organization_incident_index.py b/src/sentry/incidents/endpoints/organization_incident_index.py
index 96e5964eb7f049..05975e76e65084 100644
--- a/src/sentry/incidents/endpoints/organization_incident_index.py
+++ b/src/sentry/incidents/endpoints/organization_incident_index.py
@@ -55,11 +55,12 @@ def get(self, request: Request, organization) -> Response:
query_alert_rule = request.GET.get("alertRule")
query_include_snapshots = request.GET.get("includeSnapshots")
if query_alert_rule is not None:
- alert_rule_ids = [int(query_alert_rule)]
+ query_alert_rule_id = int(query_alert_rule)
+ alert_rule_ids = [query_alert_rule_id]
if query_include_snapshots:
snapshot_alerts = list(
AlertRuleActivity.objects.filter(
- previous_alert_rule=query_alert_rule,
+ previous_alert_rule=query_alert_rule_id,
type=AlertRuleActivityType.SNAPSHOT.value,
)
)
@@ -67,16 +68,16 @@ def get(self, request: Request, organization) -> Response:
alert_rule_ids.append(snapshot_alert.alert_rule_id)
incidents = incidents.filter(alert_rule__in=alert_rule_ids)
- query_start = request.GET.get("start")
- if query_start is not None:
+ query_start_s = request.GET.get("start")
+ if query_start_s is not None:
# exclude incidents closed before the window
- query_start = ensure_aware(parse_date(query_start))
+ query_start = ensure_aware(parse_date(query_start_s))
incidents = incidents.exclude(date_closed__lt=query_start)
- query_end = request.GET.get("end")
- if query_end is not None:
+ query_end_s = request.GET.get("end")
+ if query_end_s is not None:
# exclude incidents started after the window
- query_end = ensure_aware(parse_date(query_end))
+ query_end = ensure_aware(parse_date(query_end_s))
incidents = incidents.exclude(date_started__gt=query_end)
query_status = request.GET.get("status")
From aec68331374d674316b0795c94bc7d948313a91c Mon Sep 17 00:00:00 2001
From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com>
Date: Wed, 5 Mar 2025 08:48:19 -0500
Subject: [PATCH 04/39] ref: fix types for
organization_request_project_creation (#86346)
---
pyproject.toml | 1 -
src/sentry/api/endpoints/auth_index.py | 2 +-
.../api/endpoints/organization_request_project_creation.py | 5 ++++-
3 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 5fe22aa2f855d3..09333e430e7089 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -120,7 +120,6 @@ module = [
"sentry.api.endpoints.organization_events_spans_performance",
"sentry.api.endpoints.organization_projects",
"sentry.api.endpoints.organization_releases",
- "sentry.api.endpoints.organization_request_project_creation",
"sentry.api.endpoints.organization_search_details",
"sentry.api.endpoints.project_index",
"sentry.api.endpoints.project_ownership",
diff --git a/src/sentry/api/endpoints/auth_index.py b/src/sentry/api/endpoints/auth_index.py
index 0b116fb2be466b..8d43250e4d1a7e 100644
--- a/src/sentry/api/endpoints/auth_index.py
+++ b/src/sentry/api/endpoints/auth_index.py
@@ -190,7 +190,7 @@ def post(self, request: Request) -> Response:
curl -X ###METHOD### -u username:password ###URL###
"""
- if isinstance(request.user, AnonymousUser) or not request.user.is_authenticated:
+ if not request.user.is_authenticated:
return Response(status=status.HTTP_400_BAD_REQUEST)
if is_demo_user(request.user):
diff --git a/src/sentry/api/endpoints/organization_request_project_creation.py b/src/sentry/api/endpoints/organization_request_project_creation.py
index e3561921e0f1b9..ac92dfafebb37d 100644
--- a/src/sentry/api/endpoints/organization_request_project_creation.py
+++ b/src/sentry/api/endpoints/organization_request_project_creation.py
@@ -1,5 +1,5 @@
from django.utils.translation import gettext_lazy as _
-from rest_framework import serializers
+from rest_framework import serializers, status
from rest_framework.request import Request
from rest_framework.response import Response
@@ -26,6 +26,9 @@ def post(self, request: Request, organization) -> Response:
Send an email requesting a project be created
"""
+ if not request.user.is_authenticated:
+ return Response(status=status.HTTP_400_BAD_REQUEST)
+
serializer = OrganizationRequestProjectCreationSerializer(data=request.data)
if not serializer.is_valid():
return self.respond(serializer.errors, status=400)
From 78a017baa4f1853a96189d18dc6c95f0bbe5f43d Mon Sep 17 00:00:00 2001
From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com>
Date: Wed, 5 Mar 2025 08:48:26 -0500
Subject: [PATCH 05/39] ref: fix typing for api.endpoints.project_index
(#86298)
---
pyproject.toml | 1 -
src/sentry/api/endpoints/project_index.py | 10 ++++++----
.../models/sentry_app_installation.py | 20 +++++++++----------
3 files changed, 16 insertions(+), 15 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 09333e430e7089..69b0ecfa5a9218 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -121,7 +121,6 @@ module = [
"sentry.api.endpoints.organization_projects",
"sentry.api.endpoints.organization_releases",
"sentry.api.endpoints.organization_search_details",
- "sentry.api.endpoints.project_index",
"sentry.api.endpoints.project_ownership",
"sentry.api.endpoints.project_repo_path_parsing",
"sentry.api.endpoints.project_rules_configuration",
diff --git a/src/sentry/api/endpoints/project_index.py b/src/sentry/api/endpoints/project_index.py
index b0b3a8c99f2ccd..c84d65fa963c9d 100644
--- a/src/sentry/api/endpoints/project_index.py
+++ b/src/sentry/api/endpoints/project_index.py
@@ -46,14 +46,14 @@ def get(self, request: Request) -> Response:
queryset = queryset.none()
if request.auth and not request.user.is_authenticated:
- if hasattr(request.auth, "project"):
+ if request.auth.project_id:
queryset = queryset.filter(id=request.auth.project_id)
elif request.auth.organization_id is not None:
queryset = queryset.filter(organization_id=request.auth.organization_id)
else:
queryset = queryset.none()
elif not (is_active_superuser(request) and request.GET.get("show") == "all"):
- if request.user.is_sentry_app:
+ if request.user.is_authenticated and request.user.is_sentry_app:
queryset = SentryAppInstallation.objects.get_projects(request.auth)
if isinstance(queryset, EmptyQuerySet):
raise AuthenticationFailed("Token not found")
@@ -69,8 +69,10 @@ def get(self, request: Request) -> Response:
tokens = tokenize_query(query)
for key, value in tokens.items():
if key == "query":
- value = " ".join(value)
- queryset = queryset.filter(Q(name__icontains=value) | Q(slug__icontains=value))
+ value_s = " ".join(value)
+ queryset = queryset.filter(
+ Q(name__icontains=value_s) | Q(slug__icontains=value_s)
+ )
elif key == "slug":
queryset = queryset.filter(in_iexact("slug", value))
elif key == "name":
diff --git a/src/sentry/sentry_apps/models/sentry_app_installation.py b/src/sentry/sentry_apps/models/sentry_app_installation.py
index 989c3a6c43e348..7b4a7b5a1fbed2 100644
--- a/src/sentry/sentry_apps/models/sentry_app_installation.py
+++ b/src/sentry/sentry_apps/models/sentry_app_installation.py
@@ -5,7 +5,6 @@
from typing import TYPE_CHECKING, Any, ClassVar, overload
from django.db import models
-from django.db.models import QuerySet
from django.utils import timezone
from jsonschema import ValidationError
@@ -14,7 +13,9 @@
from sentry.constants import SentryAppInstallationStatus
from sentry.db.models import BoundedPositiveIntegerField, FlexibleForeignKey, control_silo_model
from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey
+from sentry.db.models.manager.base_query_set import BaseQuerySet
from sentry.db.models.paranoia import ParanoidManager, ParanoidModel
+from sentry.hybridcloud.models.outbox import ControlOutboxBase, outbox_context
from sentry.hybridcloud.outbox.base import ReplicatedControlModel
from sentry.hybridcloud.outbox.category import OutboxCategory
from sentry.projects.services.project import RpcProject
@@ -27,14 +28,11 @@
from sentry.types.region import find_regions_for_orgs
if TYPE_CHECKING:
- from sentry.models.apitoken import ApiToken
- from sentry.sentry_apps.models.sentry_app_component import SentryAppComponent
from sentry.models.project import Project
-
-from sentry.hybridcloud.models.outbox import ControlOutboxBase, outbox_context
+ from sentry.sentry_apps.models.sentry_app_component import SentryAppComponent
-def default_uuid():
+def default_uuid() -> str:
return str(uuid.uuid4())
@@ -46,17 +44,19 @@ def get_organization_filter_kwargs(self, organization_ids: list[int]):
"date_deleted": None,
}
- def get_installed_for_organization(self, organization_id: int) -> QuerySet:
+ def get_installed_for_organization(
+ self, organization_id: int
+ ) -> BaseQuerySet[SentryAppInstallation]:
return self.filter(**self.get_organization_filter_kwargs([organization_id]))
- def get_by_api_token(self, token_id: int) -> QuerySet:
+ def get_by_api_token(self, token_id: int) -> BaseQuerySet[SentryAppInstallation]:
return self.filter(status=SentryAppInstallationStatus.INSTALLED, api_token_id=token_id)
- def get_projects(self, token: ApiToken | AuthenticatedToken) -> QuerySet[Project]:
+ def get_projects(self, token: AuthenticatedToken | None) -> BaseQuerySet[Project]:
from sentry.models.apitoken import is_api_token_auth
from sentry.models.project import Project
- if not is_api_token_auth(token) or token.organization_id is None:
+ if token is None or not is_api_token_auth(token) or token.organization_id is None:
return Project.objects.none()
return Project.objects.filter(organization_id=token.organization_id)
From 73a3cfe3d8825e7810dd3b2df2f6aaf640dc8c29 Mon Sep 17 00:00:00 2001
From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com>
Date: Wed, 5 Mar 2025 08:48:31 -0500
Subject: [PATCH 06/39] ref: fix types for api.bases.organization_events
(#86296)
---
pyproject.toml | 1 -
src/sentry/api/bases/organization.py | 43 ++++++++++++++++---
src/sentry/api/bases/organization_events.py | 24 +++++++----
src/sentry/api/serializers/snuba.py | 12 +++---
.../endpoints/organization_replay_index.py | 11 +----
5 files changed, 59 insertions(+), 32 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 69b0ecfa5a9218..3a81c07f05445c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -112,7 +112,6 @@ ignore_missing_imports = true
[[tool.mypy.overrides]]
module = [
"sentry.api.base",
- "sentry.api.bases.organization_events",
"sentry.api.endpoints.group_integration_details",
"sentry.api.endpoints.group_integrations",
"sentry.api.endpoints.organization_events_facets_performance",
diff --git a/src/sentry/api/bases/organization.py b/src/sentry/api/bases/organization.py
index ec767c02ff1b7a..5f99eed91bf0e7 100644
--- a/src/sentry/api/bases/organization.py
+++ b/src/sentry/api/bases/organization.py
@@ -2,7 +2,7 @@
from collections.abc import Sequence
from datetime import datetime
-from typing import Any, TypedDict
+from typing import Any, Literal, NotRequired, TypedDict, overload
import sentry_sdk
from django.core.cache import cache
@@ -305,14 +305,24 @@ def convert_args(
return (args, kwargs)
-class FilterParams(TypedDict, total=False):
+class FilterParams(TypedDict):
start: datetime | None
end: datetime | None
project_id: list[int]
project_objects: list[Project]
organization_id: int
- environment: list[str] | None
- environment_objects: list[Environment] | None
+ environment: NotRequired[list[str]]
+ environment_objects: NotRequired[list[Environment]]
+
+
+class FilterParamsDateNotNull(TypedDict):
+ start: datetime
+ end: datetime
+ project_id: list[int]
+ project_objects: list[Project]
+ organization_id: int
+ environment: NotRequired[list[str]]
+ environment_objects: NotRequired[list[Environment]]
def _validate_fetched_projects(
@@ -461,14 +471,35 @@ def get_environments(
) -> list[Environment]:
return get_environments(request, organization)
+ @overload
def get_filter_params(
self,
request: Request,
organization: Organization | RpcOrganization,
- date_filter_optional: bool = False,
project_ids: list[int] | set[int] | None = None,
project_slugs: list[str] | set[str] | None = None,
- ) -> FilterParams:
+ ) -> FilterParamsDateNotNull: ...
+
+ @overload
+ def get_filter_params(
+ self,
+ request: Request,
+ organization: Organization | RpcOrganization,
+ project_ids: list[int] | set[int] | None = None,
+ project_slugs: list[str] | set[str] | None = None,
+ *,
+ date_filter_optional: Literal[True],
+ ) -> FilterParams: ...
+
+ def get_filter_params(
+ self,
+ request: Request,
+ organization: Organization | RpcOrganization,
+ project_ids: list[int] | set[int] | None = None,
+ project_slugs: list[str] | set[str] | None = None,
+ *,
+ date_filter_optional: bool = False,
+ ) -> FilterParams | FilterParamsDateNotNull:
"""
Extracts common filter parameters from the request and returns them
in a standard format.
diff --git a/src/sentry/api/bases/organization_events.py b/src/sentry/api/bases/organization_events.py
index 225a32f61c79e5..2fef759f473591 100644
--- a/src/sentry/api/bases/organization_events.py
+++ b/src/sentry/api/bases/organization_events.py
@@ -17,10 +17,10 @@
from sentry.api.api_owners import ApiOwner
from sentry.api.base import CURSOR_LINK_HEADER
from sentry.api.bases import NoProjects
-from sentry.api.bases.organization import OrganizationEndpoint
+from sentry.api.bases.organization import FilterParamsDateNotNull, OrganizationEndpoint
from sentry.api.helpers.mobile import get_readable_device_name
from sentry.api.helpers.teams import get_teams
-from sentry.api.serializers.snuba import BaseSnubaSerializer, SnubaTSResultSerializer
+from sentry.api.serializers.snuba import SnubaTSResultSerializer
from sentry.api.utils import handle_query_errors
from sentry.discover.arithmetic import is_equation, strip_equation
from sentry.discover.models import DatasetSourcesTypes, DiscoverSavedQueryTypes
@@ -37,6 +37,7 @@
from sentry.snuba import discover
from sentry.snuba.metrics.extraction import MetricSpecType
from sentry.snuba.utils import DATASET_LABELS, DATASET_OPTIONS, get_dataset
+from sentry.users.services.user.serial import serialize_generic_user
from sentry.utils import snuba
from sentry.utils.cursors import Cursor
from sentry.utils.dates import get_interval_from_range, get_rollup_from_request, parse_stats_period
@@ -129,7 +130,7 @@ def get_snuba_params(
detail=f"You can view up to {MAX_FIELDS} fields at a time. Please delete some and try again."
)
- filter_params: dict[str, Any] = self.get_filter_params(request, organization)
+ filter_params = self.get_filter_params(request, organization)
if quantize_date_params:
filter_params = self.quantize_date_params(request, filter_params)
params = SnubaParams(
@@ -137,7 +138,9 @@ def get_snuba_params(
end=filter_params["end"],
environments=filter_params.get("environment_objects", []),
projects=filter_params["project_objects"],
- user=request.user if request.user else None,
+ user=serialize_generic_user(
+ request.user if request.user.is_authenticated else None
+ ),
teams=self.get_teams(request, organization),
organization=organization,
)
@@ -163,7 +166,9 @@ def get_orderby(self, request: Request) -> list[str] | None:
return orderby
return None
- def quantize_date_params(self, request: Request, params: dict[str, Any]) -> dict[str, Any]:
+ def quantize_date_params(
+ self, request: Request, params: FilterParamsDateNotNull
+ ) -> FilterParamsDateNotNull:
# We only need to perform this rounding on relative date periods
if "statsPeriod" not in request.GET:
return params
@@ -418,7 +423,8 @@ def get_event_stats_data(
request: Request,
organization: Organization,
get_event_stats: Callable[
- [list[str], str, SnubaParams, int, bool, timedelta | None], SnubaTSResult
+ [list[str], str, SnubaParams, int, bool, timedelta | None],
+ SnubaTSResult | dict[str, SnubaTSResult],
],
top_events: int = 0,
query_column: str = "count()",
@@ -466,7 +472,7 @@ def get_event_stats_data(
stats_period = parse_stats_period(get_interval_from_range(date_range, False))
rollup = int(stats_period.total_seconds()) if stats_period is not None else 3600
if comparison_delta is not None:
- retention = quotas.get_event_retention(organization=organization)
+ retention = quotas.backend.get_event_retention(organization=organization)
comparison_start = snuba_params.start_date - comparison_delta
if retention and comparison_start < timezone.now() - timedelta(days=retention):
raise ValidationError("Comparison period is outside your retention window")
@@ -484,7 +490,7 @@ def get_event_stats_data(
# there were no top events found. In this case, result contains a zerofilled series
# that acts as a placeholder.
is_multiple_axis = len(query_columns) > 1
- if top_events > 0 and isinstance(result, dict):
+ if isinstance(result, dict):
results = {}
for key, event_result in result.items():
if is_multiple_axis:
@@ -588,7 +594,7 @@ def serialize_multiple_axis(
self,
request: Request,
organization: Organization,
- serializer: BaseSnubaSerializer,
+ serializer: SnubaTSResultSerializer,
event_result: SnubaTSResult,
snuba_params: SnubaParams,
columns: Sequence[str],
diff --git a/src/sentry/api/serializers/snuba.py b/src/sentry/api/serializers/snuba.py
index be06f38d6add17..1149b9d09b3d55 100644
--- a/src/sentry/api/serializers/snuba.py
+++ b/src/sentry/api/serializers/snuba.py
@@ -50,7 +50,11 @@ def calculate_time_frame(start, end, rollup):
return {"start": rollup_start, "end": rollup_end}
-class BaseSnubaSerializer:
+class SnubaTSResultSerializer:
+ """
+ Serializer for time-series Snuba data.
+ """
+
def __init__(self, organization, lookup, user):
self.organization = organization
self.lookup = lookup
@@ -62,12 +66,6 @@ def get_attrs(self, item_list):
return self.lookup.serializer(self.organization, item_list, self.user)
-
-class SnubaTSResultSerializer(BaseSnubaSerializer):
- """
- Serializer for time-series Snuba data.
- """
-
def serialize(
self,
result,
diff --git a/src/sentry/replays/endpoints/organization_replay_index.py b/src/sentry/replays/endpoints/organization_replay_index.py
index 494f660dc6411d..b10231e37e138d 100644
--- a/src/sentry/replays/endpoints/organization_replay_index.py
+++ b/src/sentry/replays/endpoints/organization_replay_index.py
@@ -88,17 +88,10 @@ def data_fn(offset: int, limit: int):
if not isinstance(sort, str):
sort = None
- start = filter_params["start"]
- end = filter_params["end"]
- if start is None or end is None:
- # It's not possible to reach this point but the type hint is wrong so I have
- # to do this for completeness sake.
- return Response({"detail": "Missing start or end period."}, status=400)
-
response = query_replays_collection_paginated(
project_ids=filter_params["project_id"],
- start=start,
- end=end,
+ start=filter_params["start"],
+ end=filter_params["end"],
environment=filter_params.get("environment") or [],
sort=sort,
fields=request.query_params.getlist("field"),
From d4315a14abd314c254c0229075e550c5fcc9d2f9 Mon Sep 17 00:00:00 2001
From: "Armen Zambrano G." <44410+armenzg@users.noreply.github.com>
Date: Wed, 5 Mar 2025 09:01:33 -0500
Subject: [PATCH 07/39] fix(code_mappings): Do not append extension (#86283)
In #84779 I fixed an issue, however, appending `.java` at the end is not
the right fix and may need to be handled differently somewhere else in
the codebase.
This also changes the test structure and adds an extra test case.
---
src/sentry/utils/event_frames.py | 8 ++-
.../test_code_mapping.py | 51 ++++++++++---------
2 files changed, 32 insertions(+), 27 deletions(-)
diff --git a/src/sentry/utils/event_frames.py b/src/sentry/utils/event_frames.py
index 600ae39064e3e0..c0955217df5a70 100644
--- a/src/sentry/utils/event_frames.py
+++ b/src/sentry/utils/event_frames.py
@@ -42,8 +42,12 @@ def java_frame_munger(frame: EventFrame) -> str | None:
if not frame.filename or not frame.module:
return None
- if "$$" in frame.module:
- return frame.module.split("$$")[0].replace(".", "/") + ".java"
+ if "$" in frame.module:
+ path = frame.module.split("$")[0].replace(".", "/")
+ if frame.abs_path and frame.abs_path.count(".") == 1:
+ # Append extension
+ path = path + "." + frame.abs_path.split(".")[-1]
+ return path
if "/" not in str(frame.filename) and frame.module:
# Replace the last module segment with the filename, as the
diff --git a/tests/sentry/issues/auto_source_code_config/test_code_mapping.py b/tests/sentry/issues/auto_source_code_config/test_code_mapping.py
index cbb3ba14f9b0c4..42fc4ad70e3ad1 100644
--- a/tests/sentry/issues/auto_source_code_config/test_code_mapping.py
+++ b/tests/sentry/issues/auto_source_code_config/test_code_mapping.py
@@ -512,36 +512,37 @@ def test_convert_stacktrace_frame_path_to_source_path_java_no_source_context(sel
organization_integration=self.oi,
project=self.project,
repo=self.repo,
- # XXX: Discuss support for dot notation
- # XXX: Discuss support for forgetting a last back slash
stack_root="com/example/",
source_root="src/com/example/",
automatically_generated=False,
)
- assert (
- convert_stacktrace_frame_path_to_source_path(
- frame=EventFrame(
- filename="MainActivity.java",
- module="com.example.vu.android.MainActivity",
- ),
- code_mapping=code_mapping,
- platform="java",
- sdk_name="sentry.java.android",
- )
- == "src/com/example/vu/android/MainActivity.java"
- )
- assert (
- convert_stacktrace_frame_path_to_source_path(
- frame=EventFrame(
- filename="D8$$SyntheticClass",
- module="com.example.vu.android.MainActivity$$ExternalSyntheticLambda4",
- ),
- code_mapping=code_mapping,
- platform="java",
- sdk_name="sentry.java.android",
+ for module, abs_path, expected_path in [
+ (
+ "com.example.vu.android.MainActivity",
+ "MainActivity.java",
+ "src/com/example/vu/android/MainActivity.java",
+ ),
+ (
+ "com.example.vu.android.MainActivity$$ExternalSyntheticLambda4",
+ "D8$$SyntheticClass",
+ # Extension not appended since abs_path did not contain one
+ "src/com/example/vu/android/MainActivity",
+ ),
+ (
+ "com.example.vu.android.empowerplant.MainFragment$3$1",
+ "MainFragment.java",
+ "src/com/example/vu/android/empowerplant/MainFragment.java",
+ ),
+ ]:
+ assert (
+ convert_stacktrace_frame_path_to_source_path(
+ frame=EventFrame(filename=abs_path, abs_path=abs_path, module=module),
+ code_mapping=code_mapping,
+ platform="java",
+ sdk_name="sentry.java.android",
+ )
+ == expected_path
)
- == "src/com/example/vu/android/MainActivity.java"
- )
def test_convert_stacktrace_frame_path_to_source_path_backslashes(self) -> None:
assert (
From f729b8e0b4a44f1c8b171813196a5f33b1eba782 Mon Sep 17 00:00:00 2001
From: Jonas
Date: Wed, 5 Mar 2025 09:11:55 -0500
Subject: [PATCH 08/39] help: change to use input component (#86363)
Same change as https://github.com/getsentry/sentry/pull/86355
---
.../app/components/modals/helpSearchModal.tsx | 27 ++++++++-----------
1 file changed, 11 insertions(+), 16 deletions(-)
diff --git a/static/app/components/modals/helpSearchModal.tsx b/static/app/components/modals/helpSearchModal.tsx
index f35bd913348676..116bc0385e30f6 100644
--- a/static/app/components/modals/helpSearchModal.tsx
+++ b/static/app/components/modals/helpSearchModal.tsx
@@ -2,10 +2,10 @@ import {ClassNames, css, useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import type {ModalRenderProps} from 'sentry/actionCreators/modal';
+import {Input} from 'sentry/components/core/input';
import HelpSearch from 'sentry/components/helpSearch';
import Hook from 'sentry/components/hook';
import {t} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
import type {Organization} from 'sentry/types/organization';
import withOrganization from 'sentry/utils/withOrganization';
@@ -40,9 +40,10 @@ function HelpSearchModal({
border-top: 1px solid ${theme.border};
`}
renderInput={({getInputProps}) => (
-
-
-
+
)}
resultFooter={
@@ -54,19 +55,13 @@ function HelpSearchModal({
);
}
-const InputWrapper = styled('div')`
- padding: ${space(0.25)};
-`;
-
-const Input = styled('input')`
- width: 100%;
- padding: ${space(1)};
- border: none;
- border-radius: 8px;
- outline: none;
-
- &:focus {
+const InputWithoutFocusStyles = styled(Input)`
+ &:focus,
+ &:active,
+ &:hover {
outline: none;
+ box-shadow: none;
+ border: none;
}
`;
From 2d227f2af09fc35179ed71e70a2d8f45bfef050d Mon Sep 17 00:00:00 2001
From: Jonas
Date: Wed, 5 Mar 2025 09:12:09 -0500
Subject: [PATCH 09/39] commandpalette: use input component (#86358)
Will check with @Jesse-Box about hiding focus border, I don't think it's
terrible to have it, but we can also remove it.
Before

After

---
.../app/components/modals/commandPalette.tsx | 51 +++++++------------
1 file changed, 18 insertions(+), 33 deletions(-)
diff --git a/static/app/components/modals/commandPalette.tsx b/static/app/components/modals/commandPalette.tsx
index ade4e91a3d6635..9fed1201fc4b26 100644
--- a/static/app/components/modals/commandPalette.tsx
+++ b/static/app/components/modals/commandPalette.tsx
@@ -3,10 +3,9 @@ import {ClassNames, css, useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import type {ModalRenderProps} from 'sentry/actionCreators/modal';
-import {Input} from 'sentry/components/core/input';
+import {InputGroup} from 'sentry/components/core/input/inputGroup';
import {Search} from 'sentry/components/search';
import {t} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
import {trackAnalytics} from 'sentry/utils/analytics';
function CommandPalette({Body}: ModalRenderProps) {
@@ -38,15 +37,13 @@ function CommandPalette({Body}: ModalRenderProps) {
border-top: 1px solid ${theme.border};
`}
renderInput={({getInputProps}) => (
-
-
-
+
)}
/>
)}
@@ -57,30 +54,18 @@ function CommandPalette({Body}: ModalRenderProps) {
export default CommandPalette;
-export const modalCss = css`
- [role='document'] {
- padding: 0;
+const InputWithoutFocusStyles = styled(InputGroup.Input)`
+ &:focus,
+ &:active,
+ &:hover {
+ outline: none;
+ box-shadow: none;
+ border: none;
}
`;
-const InputWrapper = styled('div')`
- padding: ${space(0.25)};
-`;
-
-const StyledInput = styled(Input)`
- width: 100%;
- padding: ${space(1)};
- border-radius: ${p => p.theme.borderRadius};
-
- outline: none;
- border: none;
- box-shadow: none;
-
- :focus,
- :active,
- :hover {
- outline: none;
- border: none;
- box-shadow: none;
+export const modalCss = css`
+ [role='document'] {
+ padding: 0;
}
`;
From 432d5bc0435edd7e8f3b837876daf4e469491b74 Mon Sep 17 00:00:00 2001
From: Jonas
Date: Wed, 5 Mar 2025 09:12:39 -0500
Subject: [PATCH 10/39] settings: use input component (#86355)
Use input component and remove custom styles
Before

After

---
.../components/settingsSearch/index.tsx | 45 ++++---------------
1 file changed, 9 insertions(+), 36 deletions(-)
diff --git a/static/app/views/settings/components/settingsSearch/index.tsx b/static/app/views/settings/components/settingsSearch/index.tsx
index c6ef63f475cf23..9f645e45dd2974 100644
--- a/static/app/views/settings/components/settingsSearch/index.tsx
+++ b/static/app/views/settings/components/settingsSearch/index.tsx
@@ -1,6 +1,7 @@
import {useMemo, useRef} from 'react';
import styled from '@emotion/styled';
+import {InputGroup} from 'sentry/components/core/input/inputGroup';
import {Search} from 'sentry/components/search';
import {IconSearch} from 'sentry/icons';
import {t} from 'sentry/locale';
@@ -24,14 +25,17 @@ function SettingsSearch() {
minSearch={MIN_SEARCH_LENGTH}
maxResults={MAX_RESULTS}
renderInput={({getInputProps}) => (
-
-
-
+
+
+
+
-
+
)}
/>
);
@@ -39,37 +43,6 @@ function SettingsSearch() {
export default SettingsSearch;
-const SearchInputWrapper = styled('div')`
- position: relative;
-`;
-
-const SearchInputIcon = styled(IconSearch)`
- color: ${p => p.theme.gray300};
- position: absolute;
- left: 10px;
- top: 8px;
-`;
-
-const SearchInput = styled('input')`
- color: ${p => p.theme.formText};
- background-color: ${p => p.theme.background};
- transition: border-color 0.15s ease;
- font-size: 14px;
+const StyledSearchInput = styled(InputGroup.Input)`
width: 260px;
- line-height: 1;
- padding: 5px 8px 4px 28px;
- border: 1px solid ${p => p.theme.border};
- border-radius: 30px;
- height: 28px;
-
- box-shadow: inset ${p => p.theme.dropShadowMedium};
-
- &:focus {
- outline: none;
- border: 1px solid ${p => p.theme.border};
- }
-
- &::placeholder {
- color: ${p => p.theme.formPlaceholder};
- }
`;
From 6539af99ecf76597e6a572f4f79c0406e823ad0d Mon Sep 17 00:00:00 2001
From: ArthurKnaus
Date: Wed, 5 Mar 2025 15:18:31 +0100
Subject: [PATCH 11/39] feat(laravel-insights): Improve table responsiveness
(#86378)
Give min size to all columns except `Path`.
Add ellipsis + tooltip to transaction name.
---
.../pages/backend/laravelOverviewPage.tsx | 74 +++++++++++--------
1 file changed, 44 insertions(+), 30 deletions(-)
diff --git a/static/app/views/insights/pages/backend/laravelOverviewPage.tsx b/static/app/views/insights/pages/backend/laravelOverviewPage.tsx
index adac656464d98c..ca2b61606b79e5 100644
--- a/static/app/views/insights/pages/backend/laravelOverviewPage.tsx
+++ b/static/app/views/insights/pages/backend/laravelOverviewPage.tsx
@@ -1,5 +1,5 @@
import {Fragment, useCallback, useMemo} from 'react';
-import {useTheme} from '@emotion/react';
+import {css, useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import type {Location} from 'history';
import pick from 'lodash/pick';
@@ -949,23 +949,8 @@ const getCellColor = (value: number, thresholds: Record) => {
return Object.entries(thresholds).find(([_, threshold]) => value >= threshold)?.[0];
};
-const PathCell = styled('div')`
- display: flex;
- flex-direction: column;
- padding: ${space(1)} ${space(2)};
- justify-content: center;
- gap: ${space(0.5)};
- min-width: 200px;
-`;
-
-const ControllerText = styled('div')`
- color: ${p => p.theme.gray300};
- font-size: ${p => p.theme.fontSizeSmall};
- line-height: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- min-width: 0px;
+const StyledPanelTable = styled(PanelTable)`
+ grid-template-columns: max-content minmax(200px, 1fr) repeat(5, max-content);
`;
const Cell = styled('div')`
@@ -992,6 +977,22 @@ const HeaderCell = styled(Cell)`
padding: 0;
`;
+const PathCell = styled(Cell)`
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ gap: ${space(0.5)};
+ min-width: 0px;
+`;
+
+const ControllerText = styled('div')`
+ ${p => p.theme.overflowEllipsis};
+ color: ${p => p.theme.gray300};
+ font-size: ${p => p.theme.fontSizeSmall};
+ line-height: 1;
+ min-width: 0px;
+`;
+
function RoutesTable({query}: {query?: string}) {
const organization = useOrganization();
const pageFilterChartParams = usePageFilterChartParams();
@@ -1087,7 +1088,7 @@ function RoutesTable({query}: {query?: string}) {
}, [transactionsRequest.data, routeControllersRequest.data]);
return (
- {transaction.method}
-
- {transaction.transaction}
-
+
+ {transaction.transaction}
+
+
{routeControllersRequest.isLoading ? (
) : (
@@ -1136,6 +1149,7 @@ function RoutesTable({query}: {query?: string}) {
position="top"
maxWidth={400}
showOnlyOnOverflow
+ skipWrapper
>
{transaction.controller}
@@ -1157,6 +1171,6 @@ function RoutesTable({query}: {query?: string}) {
);
})}
-
+
);
}
From 5414e81190c4b4bae963ae1f5f30eed8117d15f8 Mon Sep 17 00:00:00 2001
From: Jonas
Date: Wed, 5 Mar 2025 09:35:27 -0500
Subject: [PATCH 12/39] deploybadge: deprecate black and white variant (#86193)
Only used in quicktrace and not part of the spec
---
static/app/components/badge/groupPriority.tsx | 5 ++---
.../app/components/core/badge/tag.stories.tsx | 2 --
static/app/components/core/badge/tag.tsx | 20 +++++++++----------
.../group/inboxBadges/groupStatusTag.tsx | 4 ++--
.../group/inboxBadges/statusBadge.tsx | 5 ++---
.../components/visualize/index.tsx | 18 ++++++++---------
static/app/views/releases/utils/index.tsx | 4 ++--
.../productTrial/productTrialTag.tsx | 5 ++---
8 files changed, 28 insertions(+), 35 deletions(-)
diff --git a/static/app/components/badge/groupPriority.tsx b/static/app/components/badge/groupPriority.tsx
index a1da5d6339089a..4aa53f1562c649 100644
--- a/static/app/components/badge/groupPriority.tsx
+++ b/static/app/components/badge/groupPriority.tsx
@@ -1,5 +1,4 @@
import {Fragment, useMemo} from 'react';
-import type {Theme} from '@emotion/react';
import styled from '@emotion/styled';
import {VisuallyHidden} from '@react-aria/visually-hidden';
@@ -9,7 +8,7 @@ import {usePrompt} from 'sentry/actionCreators/prompts';
import {IconCellSignal} from 'sentry/components/badge/iconCellSignal';
import {Button, LinkButton} from 'sentry/components/button';
import {Chevron} from 'sentry/components/chevron';
-import {Tag} from 'sentry/components/core/badge/tag';
+import {Tag, type TagProps} from 'sentry/components/core/badge/tag';
import type {MenuItemProps} from 'sentry/components/dropdownMenu';
import {DropdownMenu} from 'sentry/components/dropdownMenu';
import {DropdownMenuFooter} from 'sentry/components/dropdownMenu/footer';
@@ -48,7 +47,7 @@ const PRIORITY_KEY_TO_LABEL: Record = {
const PRIORITY_OPTIONS = [PriorityLevel.HIGH, PriorityLevel.MEDIUM, PriorityLevel.LOW];
-function getTagTypeForPriority(priority: string): keyof Theme['tag'] {
+function getTagTypeForPriority(priority: string): TagProps['type'] {
switch (priority) {
case PriorityLevel.HIGH:
return 'error';
diff --git a/static/app/components/core/badge/tag.stories.tsx b/static/app/components/core/badge/tag.stories.tsx
index f4651577488b5d..978c5eaddf928c 100644
--- a/static/app/components/core/badge/tag.stories.tsx
+++ b/static/app/components/core/badge/tag.stories.tsx
@@ -28,8 +28,6 @@ export default storyBook('Tag', (story, APIReference) => {
ErrorWarningInfo
- Black
- WhitePromotionHighlight
diff --git a/static/app/components/core/badge/tag.tsx b/static/app/components/core/badge/tag.tsx
index 186aab8b5df980..002031e52c7c48 100644
--- a/static/app/components/core/badge/tag.tsx
+++ b/static/app/components/core/badge/tag.tsx
@@ -9,15 +9,13 @@ import {space} from 'sentry/styles/space';
type TagType =
// @TODO(jonasbadalic): "default" is a bad API naming
- | 'default'
- | 'promotion'
- | 'highlight'
- | 'warning'
- | 'success'
- | 'error'
- | 'info'
- | 'white'
- | 'black';
+ 'default' | 'info' | 'success' | 'warning' | 'error' | 'promotion' | 'highlight';
+
+/**
+ * @deprecated Do not use these tag types
+ */
+type DeprecatedTagType = 'white' | 'black';
+
export interface TagProps extends React.HTMLAttributes {
/**
* Icon on the left side.
@@ -30,7 +28,7 @@ export interface TagProps extends React.HTMLAttributes {
/**
* Dictates color scheme of the tag.
*/
- type?: TagType;
+ type?: TagType | DeprecatedTagType;
}
export const Tag = forwardRef(
@@ -65,7 +63,7 @@ export const Tag = forwardRef(
);
const StyledTag = styled('div')<{
- type: TagType;
+ type: NonNullable;
}>`
font-size: ${p => p.theme.fontSizeSmall};
background-color: ${p => p.theme.tag[p.type].background};
diff --git a/static/app/components/group/inboxBadges/groupStatusTag.tsx b/static/app/components/group/inboxBadges/groupStatusTag.tsx
index 50e5a867c95be6..8a0888cfb56f66 100644
--- a/static/app/components/group/inboxBadges/groupStatusTag.tsx
+++ b/static/app/components/group/inboxBadges/groupStatusTag.tsx
@@ -2,7 +2,7 @@ import {Fragment} from 'react';
import type {Theme} from '@emotion/react';
import styled from '@emotion/styled';
-import {Tag} from 'sentry/components/core/badge/tag';
+import {Tag, type TagProps} from 'sentry/components/core/badge/tag';
import TimeSince from 'sentry/components/timeSince';
import {Tooltip} from 'sentry/components/tooltip';
@@ -11,7 +11,7 @@ interface GroupStatusBadgeProps {
dateAdded?: string;
fontSize?: 'sm' | 'md';
tooltip?: React.ReactNode;
- type?: keyof Theme['tag'];
+ type?: TagProps['type'];
}
/**
diff --git a/static/app/components/group/inboxBadges/statusBadge.tsx b/static/app/components/group/inboxBadges/statusBadge.tsx
index 3a5210b4aa4ade..f3d8ddcd4cc39c 100644
--- a/static/app/components/group/inboxBadges/statusBadge.tsx
+++ b/static/app/components/group/inboxBadges/statusBadge.tsx
@@ -1,5 +1,4 @@
-import type {Theme} from '@emotion/react';
-
+import type {TagProps} from 'sentry/components/core/badge/tag';
import {GroupStatusTag} from 'sentry/components/group/inboxBadges/groupStatusTag';
import {t} from 'sentry/locale';
import type {Group} from 'sentry/types/group';
@@ -14,7 +13,7 @@ interface SubstatusBadgeProps {
export function getBadgeProperties(
status: Group['status'],
substatus: Group['substatus']
-): {status: string; tagType: keyof Theme['tag']; tooltip?: string} | undefined {
+): {status: string; tagType: TagProps['type']; tooltip?: string} | undefined {
if (status === 'resolved') {
return {
tagType: 'highlight',
diff --git a/static/app/views/dashboards/widgetBuilder/components/visualize/index.tsx b/static/app/views/dashboards/widgetBuilder/components/visualize/index.tsx
index edee0b6963ebcf..36995bcd0d4a20 100644
--- a/static/app/views/dashboards/widgetBuilder/components/visualize/index.tsx
+++ b/static/app/views/dashboards/widgetBuilder/components/visualize/index.tsx
@@ -1,14 +1,13 @@
import {Fragment, type ReactNode, useMemo, useState} from 'react';
import {closestCenter, DndContext, DragOverlay} from '@dnd-kit/core';
import {arrayMove, SortableContext, verticalListSortingStrategy} from '@dnd-kit/sortable';
-import type {Theme} from '@emotion/react';
import styled from '@emotion/styled';
import cloneDeep from 'lodash/cloneDeep';
import {Button} from 'sentry/components/button';
import {CompactSelect} from 'sentry/components/compactSelect';
import {TriggerLabel} from 'sentry/components/compactSelect/control';
-import {Tag} from 'sentry/components/core/badge/tag';
+import {Tag, type TagProps} from 'sentry/components/core/badge/tag';
import {Input} from 'sentry/components/core/input';
import {Radio} from 'sentry/components/core/radio';
import {RadioLineItem} from 'sentry/components/forms/controls/radioGroup';
@@ -863,32 +862,33 @@ export function renderTag(kind: FieldValueKind, label: string, dataType?: string
return {dataType};
}
}
- let text, tagType;
+ let text: string | undefined, tagType: TagProps['type'] | undefined;
+
switch (kind) {
case FieldValueKind.FUNCTION:
text = 'f(x)';
- tagType = 'warning' as keyof Theme['tag'];
+ tagType = 'warning';
break;
case FieldValueKind.CUSTOM_MEASUREMENT:
case FieldValueKind.MEASUREMENT:
text = 'field';
- tagType = 'highlight' as keyof Theme['tag'];
+ tagType = 'highlight';
break;
case FieldValueKind.BREAKDOWN:
text = 'field';
- tagType = 'highlight' as keyof Theme['tag'];
+ tagType = 'highlight';
break;
case FieldValueKind.TAG:
text = kind;
- tagType = 'warning' as keyof Theme['tag'];
+ tagType = 'warning';
break;
case FieldValueKind.NUMERIC_METRICS:
text = 'f(x)';
- tagType = 'warning' as keyof Theme['tag'];
+ tagType = 'warning';
break;
case FieldValueKind.FIELD:
text = DEPRECATED_FIELDS.includes(label) ? 'deprecated' : 'field';
- tagType = 'highlight' as keyof Theme['tag'];
+ tagType = 'highlight';
break;
default:
text = kind;
diff --git a/static/app/views/releases/utils/index.tsx b/static/app/views/releases/utils/index.tsx
index 5c67b1a5be0514..d51f71cc6ba431 100644
--- a/static/app/views/releases/utils/index.tsx
+++ b/static/app/views/releases/utils/index.tsx
@@ -1,10 +1,10 @@
-import type {Theme} from '@emotion/react';
import type {Location} from 'history';
import pick from 'lodash/pick';
import round from 'lodash/round';
import moment from 'moment-timezone';
import type {DateTimeObject} from 'sentry/components/charts/utils';
+import type {TagProps} from 'sentry/components/core/badge/tag';
import ExternalLink from 'sentry/components/links/externalLink';
import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
import {PAGE_URL_PARAM, URL_PARAM} from 'sentry/constants/pageFilters';
@@ -208,7 +208,7 @@ const adoptionStagesLink = (
export const ADOPTION_STAGE_LABELS: Record<
string,
- {name: string; tooltipTitle: React.ReactNode; type: keyof Theme['tag']}
+ {name: string; tooltipTitle: React.ReactNode; type: TagProps['type']}
> = {
low_adoption: {
name: t('Low Adoption'),
diff --git a/static/gsApp/components/productTrial/productTrialTag.tsx b/static/gsApp/components/productTrial/productTrialTag.tsx
index 71917e9b2b6918..a105ddce8bcb81 100644
--- a/static/gsApp/components/productTrial/productTrialTag.tsx
+++ b/static/gsApp/components/productTrial/productTrialTag.tsx
@@ -1,7 +1,6 @@
-import type {Theme} from '@emotion/react';
import moment from 'moment-timezone';
-import {Tag} from 'sentry/components/core/badge/tag';
+import {Tag, type TagProps} from 'sentry/components/core/badge/tag';
import {IconBusiness} from 'sentry/icons';
import {IconClock} from 'sentry/icons/iconClock';
import {IconFlag} from 'sentry/icons/iconFlag';
@@ -13,7 +12,7 @@ import type {ProductTrial} from 'getsentry/types';
interface ProductTrialTagProps {
trial: ProductTrial;
showTrialEnded?: boolean;
- type?: keyof Theme['tag'];
+ type?: TagProps['type'];
}
function ProductTrialTag({trial, type, showTrialEnded = false}: ProductTrialTagProps) {
From 2861b4809d6a65c820ab3689beb44c83a7912b44 Mon Sep 17 00:00:00 2001
From: Jonas
Date: Wed, 5 Mar 2025 09:35:40 -0500
Subject: [PATCH 13/39] checkbox: add chonk style (#86263)
Implement chonk styling for checkbox component
**Light mode**

**Dark mode**

---
.../components/core/checkbox/index.chonk.tsx | 48 +++++++++
static/app/components/core/checkbox/index.tsx | 98 ++++++++++---------
2 files changed, 101 insertions(+), 45 deletions(-)
create mode 100644 static/app/components/core/checkbox/index.chonk.tsx
diff --git a/static/app/components/core/checkbox/index.chonk.tsx b/static/app/components/core/checkbox/index.chonk.tsx
new file mode 100644
index 00000000000000..343349dfbd0263
--- /dev/null
+++ b/static/app/components/core/checkbox/index.chonk.tsx
@@ -0,0 +1,48 @@
+import {chonkStyled} from 'sentry/utils/theme/theme.chonk';
+
+export const ChonkNativeHiddenCheckbox = chonkStyled('input')`
+ position: absolute;
+ opacity: 0;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ cursor: pointer;
+
+ & + * {
+ color: ${p => p.theme.colors.static.white};
+ border: 1px solid ${p => p.theme.colors.dynamic.surface100};
+
+ svg {
+ stroke: ${p => p.theme.colors.static.white};
+ }
+ }
+
+ &:focus-visible + * {
+ outline: none;
+ box-shadow: 0 0 0 2px ${p => p.theme.background},
+ 0 0 0 4px ${p => p.theme.focusBorder};
+ }
+
+ &:disabled + *,
+ &[aria-disabled='true'] + * {
+ background-color: ${p => p.theme.colors.dynamic.surface500};
+ border: 1px solid ${p => p.theme.colors.dynamic.surface100};
+ cursor: not-allowed;
+ }
+
+ &:checked + *,
+ &:indeterminate + * {
+ background-color: ${p => p.theme.colors.static.blue400};
+ color: ${p => p.theme.white};
+ }
+
+ &:disabled:checked + *,
+ &:disabled:indeterminate + * {
+ background-color: ${p => p.theme.colors.static.blue400};
+ border: 1px solid ${p => p.theme.colors.static.blue400};
+ opacity: 0.6;
+ }
+`;
diff --git a/static/app/components/core/checkbox/index.tsx b/static/app/components/core/checkbox/index.tsx
index 6b7b07ff9e40b8..18e07930318aa5 100644
--- a/static/app/components/core/checkbox/index.tsx
+++ b/static/app/components/core/checkbox/index.tsx
@@ -4,6 +4,9 @@ import styled from '@emotion/styled';
import InteractionStateLayer from 'sentry/components/interactionStateLayer';
import mergeRefs from 'sentry/utils/mergeRefs';
import type {FormSize} from 'sentry/utils/theme';
+import {withChonk} from 'sentry/utils/theme/withChonk';
+
+import * as ChonkCheckbox from './index.chonk';
type CheckboxConfig = {
borderRadius: string;
@@ -85,49 +88,56 @@ const CheckboxWrapper = styled('div')<{
border-radius: ${p => checkboxSizeMap[p.size].borderRadius};
`;
-const NativeHiddenCheckbox = styled('input')`
- position: absolute;
- opacity: 0;
- top: 0;
- left: 0;
- height: 100%;
- width: 100%;
- margin: 0;
- padding: 0;
- cursor: pointer;
-
- & + * {
- color: ${p => p.theme.textColor};
- border: 1px solid ${p => p.theme.gray200};
- }
-
- &:focus-visible + * {
- border: 1px solid ${p => p.theme.focusBorder};
- box-shadow: ${p => p.theme.focusBorder} 0 0 0 1px;
- }
-
- &:checked:focus-visible + *,
- &:indeterminate:focus-visible + * {
- box-shadow: ${p => p.theme.focus} 0 0 0 3px;
- }
-
- &:disabled + * {
- background-color: ${p => p.theme.backgroundSecondary};
- border: 1px solid ${p => p.theme.disabledBorder};
- }
-
- &:checked + *,
- &:indeterminate + * {
- background-color: ${p => p.theme.active};
- color: ${p => p.theme.white};
- }
-
- &:disabled:checked + *,
- &:disabled:indeterminate + * {
- background-color: ${p => p.theme.disabled};
- border: 1px solid ${p => p.theme.disabledBorder};
- }
-`;
+const NativeHiddenCheckbox = withChonk(
+ styled('input')`
+ position: absolute;
+ opacity: 0;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ cursor: pointer;
+
+ & + * {
+ box-shadow: ${p => p.theme.dropShadowMedium} inset;
+ color: ${p => p.theme.textColor};
+ border: 1px solid ${p => p.theme.gray200};
+
+ svg {
+ stroke: ${p => p.theme.white};
+ }
+ }
+
+ &:focus-visible + * {
+ box-shadow: ${p => p.theme.focusBorder} 0 0 0 1px;
+ }
+
+ &:checked:focus-visible + *,
+ &:indeterminate:focus-visible + * {
+ box-shadow: ${p => p.theme.focus} 0 0 0 3px;
+ }
+
+ &:disabled + * {
+ background-color: ${p => p.theme.backgroundSecondary};
+ border: 1px solid ${p => p.theme.disabledBorder};
+ }
+
+ &:checked + *,
+ &:indeterminate + * {
+ background-color: ${p => p.theme.active};
+ color: ${p => p.theme.white};
+ }
+
+ &:disabled:checked + *,
+ &:disabled:indeterminate + * {
+ background-color: ${p => p.theme.disabled};
+ border: 1px solid ${p => p.theme.disabledBorder};
+ }
+ `,
+ ChonkCheckbox.ChonkNativeHiddenCheckbox
+);
const FakeCheckbox = styled('div')<{
size: FormSize;
@@ -137,7 +147,6 @@ const FakeCheckbox = styled('div')<{
align-items: center;
justify-content: center;
color: inherit;
- box-shadow: ${p => p.theme.dropShadowMedium} inset;
width: ${p => checkboxSizeMap[p.size].box};
height: ${p => checkboxSizeMap[p.size].box};
border-radius: ${p => checkboxSizeMap[p.size].borderRadius};
@@ -151,6 +160,5 @@ const CheckboxIcon = styled('svg')<{size: string}>`
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
- stroke: ${p => p.theme.white};
stroke-width: calc(1.4px + ${p => p.size} * 0.04);
`;
From 008edc4b917c96bfdf71b055813e5fa7d066d914 Mon Sep 17 00:00:00 2001
From: Jonas
Date: Wed, 5 Mar 2025 09:36:23 -0500
Subject: [PATCH 14/39] switch: use native-like input props (#86291)
Refactor component to use input-like props `isActive -> checked`,
`isDisabled -> disabled` and `toggle -> onClick` which will later become
onChange as I will change this component to and actual checkbox
---
.../app/components/core/switch/index.spec.tsx | 28 +++++++++
.../components/core/switch/index.stories.tsx | 25 ++++++--
static/app/components/core/switch/index.tsx | 60 ++++---------------
.../featureFlags/customOverride.tsx | 4 +-
.../featureFlags/featureFlagItem.tsx | 4 +-
.../feedbackConfigToggle.tsx | 8 +--
.../components/forms/fields/booleanField.tsx | 14 ++---
.../deprecatedAggregateFlamegraph.tsx | 4 +-
.../replaysOnboarding/replayConfigToggle.tsx | 4 +-
.../performance/contexts/onDemandControl.tsx | 2 +-
static/app/views/dashboards/manage/index.tsx | 4 +-
static/app/views/discover/landing.tsx | 6 +-
.../awsLambdaFunctionSelect.tsx | 4 +-
.../issueDetails/actions/publishModal.tsx | 4 +-
.../views/organizationStats/usageStatsOrg.tsx | 4 +-
.../settings/account/accountSubscriptions.tsx | 4 +-
.../dynamicSampling/samplingModeSwitch.tsx | 6 +-
.../installedPlugin.tsx | 6 +-
.../integrationServerlessRow.tsx | 6 +-
.../projectFilters/projectFiltersSettings.tsx | 6 +-
.../settings/project/projectServiceHooks.tsx | 2 +-
.../projectPlugins/projectPluginRow.tsx | 6 +-
22 files changed, 107 insertions(+), 104 deletions(-)
create mode 100644 static/app/components/core/switch/index.spec.tsx
diff --git a/static/app/components/core/switch/index.spec.tsx b/static/app/components/core/switch/index.spec.tsx
new file mode 100644
index 00000000000000..702513564d2b1b
--- /dev/null
+++ b/static/app/components/core/switch/index.spec.tsx
@@ -0,0 +1,28 @@
+import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+
+import {Switch} from 'sentry/components/core/switch';
+
+describe('Switch', () => {
+ it('disabled', () => {
+ render();
+ expect(screen.getByRole('checkbox')).toBeDisabled();
+ });
+
+ it('checked', () => {
+ render();
+ expect(screen.getByRole('checkbox')).toBeChecked();
+ });
+
+ it('disallows toggling a disabled switch', async () => {
+ const onClick = jest.fn();
+
+ render();
+
+ const switchButton = screen.getByRole('checkbox');
+ expect(switchButton).not.toBeChecked();
+
+ await userEvent.click(switchButton);
+ expect(switchButton).not.toBeChecked();
+ expect(onClick).not.toHaveBeenCalled();
+ });
+});
diff --git a/static/app/components/core/switch/index.stories.tsx b/static/app/components/core/switch/index.stories.tsx
index 5fb554eb4ef830..c0807f889f40b1 100644
--- a/static/app/components/core/switch/index.stories.tsx
+++ b/static/app/components/core/switch/index.stories.tsx
@@ -27,14 +27,27 @@ export default storyBook('Switch', (story, APIReference) => {
You can pass a callback function into the {' '}
prop to control what happens when the toggle is clicked. Pair this with a{' '}
useState or some other code to set the active state of the toggle,
- which is controlled by the prop .
+ which is controlled by the prop .
+
+ You can also pass a prop to disable the
+ toggle.
+
- The component is a toggle button. It doesn't have any
- property to specify a text label, so you'll need to add your own accompanying
- label if you need one.
+ The component is a checkbox button. It doesn't have
+ any property to specify a text label, so you'll need to add your own
+ accompanying label if you need one.
Here we are specifying the label with an HTML label element, which
is also accessibility friendly -- clicking the label also affects the toggle!
-
-
- You can pass a callback function into the {' '}
- prop to control what happens when the toggle is clicked. Pair this with a{' '}
- useState or some other code to set the active state of the toggle,
- which is controlled by the prop .
-
- You can also pass a prop to disable the
- toggle.
+