diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 99056b40d41de3..9ccb63af15f43b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -586,6 +586,7 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get /static/app/gettingStartedDocs/ @getsentry/value-discovery /static/app/types/project.tsx @getsentry/value-discovery /static/app/views/onboarding/ @getsentry/value-discovery +/tests/js/fixtures/detectedPlatform.ts @getsentry/value-discovery /static/app/views/projectInstall/ @getsentry/value-discovery /src/sentry/onboarding_tasks/ @getsentry/value-discovery ## End of Value Discovery @@ -597,6 +598,8 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get /tests/sentry/seer/ @getsentry/machine-learning-ai /src/sentry/seer/fetch_issues/ @getsentry/machine-learning-ai @getsentry/coding-workflows-sentry-backend /tests/sentry/seer/fetch_issues/ @getsentry/machine-learning-ai @getsentry/coding-workflows-sentry-backend +/src/sentry/tasks/seer/ @getsentry/machine-learning-ai +/tests/sentry/tasks/seer/ @getsentry/machine-learning-ai ## End of ML & AI ## Issues @@ -647,7 +650,7 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get /src/sentry/tasks/clear_expired_snoozes.py @getsentry/issue-detection-backend /src/sentry/tasks/codeowners/ @getsentry/issue-detection-backend /src/sentry/tasks/commit_context.py @getsentry/issue-detection-backend -/src/sentry/tasks/delete_seer_grouping_records.py @getsentry/issue-detection-backend +/src/sentry/tasks/seer/delete_seer_grouping_records.py @getsentry/issue-detection-backend /src/sentry/tasks/embeddings_grouping/ @getsentry/issue-detection-backend /src/sentry/tasks/groupowner.py @getsentry/issue-detection-backend /src/sentry/tasks/merge.py @getsentry/issue-detection-backend @@ -658,6 +661,7 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get /static/app/components/events/eventTags/ @getsentry/issue-workflow /static/app/components/events/highlights/ @getsentry/issue-workflow /static/app/components/issues/ @getsentry/issue-workflow +/static/app/components/stackTrace/ @getsentry/issue-workflow /static/app/views/issueList/ @getsentry/issue-workflow /static/app/views/issueList/pages/supergroups.tsx @getsentry/issue-detection-frontend /static/app/views/issueList/supergroups/ @getsentry/issue-detection-frontend @@ -685,14 +689,13 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get /tests/sentry/tasks/test_auto_ongoing_issues.py @getsentry/issue-detection-backend /tests/sentry/tasks/test_auto_remove_inbox.py @getsentry/issue-detection-backend /tests/sentry/tasks/test_auto_resolve_issues.py @getsentry/issue-detection-backend -/tests/sentry/tasks/test_backfill_seer_grouping_records.py @getsentry/issue-detection-backend +/tests/sentry/tasks/seer/test_delete_seer_grouping_records.py @getsentry/issue-detection-backend /tests/sentry/tasks/test_check_new_issue_threshold_met.py @getsentry/issue-detection-backend /tests/sentry/tasks/test_clear_expired_resolutions.py @getsentry/issue-detection-backend /tests/sentry/tasks/test_clear_expired_rulesnoozes.py @getsentry/issue-detection-backend /tests/sentry/tasks/test_clear_expired_snoozes.py @getsentry/issue-detection-backend /tests/sentry/tasks/test_code_owners.py @getsentry/issue-detection-backend /tests/sentry/tasks/test_commit_context.py @getsentry/issue-detection-backend -/tests/sentry/tasks/test_delete_seer_grouping_records.py @getsentry/issue-detection-backend /tests/sentry/tasks/test_groupowner.py @getsentry/issue-detection-backend /tests/sentry/tasks/test_merge.py @getsentry/issue-detection-backend /tests/sentry/tasks/test_post_process.py @getsentry/issue-detection-backend diff --git a/.github/codeowners-coverage-baseline.txt b/.github/codeowners-coverage-baseline.txt index 76e313ff2dae78..e7e04e0b076873 100644 --- a/.github/codeowners-coverage-baseline.txt +++ b/.github/codeowners-coverage-baseline.txt @@ -869,7 +869,6 @@ static/app/components/modals/explore/saveQueryModal.spec.tsx static/app/components/modals/explore/saveQueryModal.tsx static/app/components/modals/featureTourModal.spec.tsx static/app/components/modals/featureTourModal.tsx -static/app/components/modals/generateDashboardFromSeerModal.tsx static/app/components/modals/helpSearchModal.spec.tsx static/app/components/modals/helpSearchModal.tsx static/app/components/modals/importDashboardFromFileModal.tsx @@ -1071,7 +1070,6 @@ static/app/components/repositories/scmIntegrationTree/useScmTreeFilters.tsx static/app/components/repositories/scmRepoTreeModal.tsx static/app/components/repositoryRow.spec.tsx static/app/components/repositoryRow.tsx -static/app/components/reprocessedBox.tsx static/app/components/resolutionBox.spec.tsx static/app/components/resolutionBox.tsx static/app/components/resourceCard.tsx @@ -2524,14 +2522,12 @@ tests/sentry/tasks/test_activity.py tests/sentry/tasks/test_assemble.py tests/sentry/tasks/test_auth.py tests/sentry/tasks/test_auto_enable_codecov.py -tests/sentry/tasks/test_autofix.py tests/sentry/tasks/test_base.py tests/sentry/tasks/test_beacon.py tests/sentry/tasks/test_check_am2_compatibility.py tests/sentry/tasks/test_check_auth.py tests/sentry/tasks/test_collect_project_platforms.py tests/sentry/tasks/test_commits.py -tests/sentry/tasks/test_context_engine_index.py tests/sentry/tasks/test_delete_pending_groups.py tests/sentry/tasks/test_digests.py tests/sentry/tasks/test_email.py @@ -2541,8 +2537,6 @@ tests/sentry/tasks/test_organization_contributors.py tests/sentry/tasks/test_process_buffer.py tests/sentry/tasks/test_relay.py tests/sentry/tasks/test_reprocessing2.py -tests/sentry/tasks/test_seer.py -tests/sentry/tasks/test_seer_explorer_index.py tests/sentry/tasks/test_store.py tests/sentry/tasks/test_symbolication.py tests/sentry/tasks/test_update_code_owners_schema.py diff --git a/pyproject.toml b/pyproject.toml index ce788d1ada3ce7..23ee17f75a183b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ dependencies = [ "sentry-arroyo>=2.38.1", "sentry-conventions>=0.3.0", "sentry-forked-email-reply-parser>=0.5.12.post1", - "sentry-kafka-schemas>=2.1.26", + "sentry-kafka-schemas>=2.1.27", "sentry-ophio>=1.1.3", "sentry-protos>=0.8.7", "sentry-redis-tools>=0.5.0", @@ -676,9 +676,9 @@ module = [ "sentry.tasks.beacon", "sentry.tasks.codeowners.*", "sentry.tasks.commit_context", - "sentry.tasks.delete_seer_grouping_records", "sentry.tasks.on_demand_metrics", "sentry.tasks.reprocessing2", + "sentry.tasks.seer.delete_seer_grouping_records", "sentry.tasks.store", "sentry.tasks.unmerge", "sentry.taskworker.*", @@ -888,8 +888,8 @@ module = [ "tests.sentry.snuba.test_tasks", "tests.sentry.spans.grouping.*", "tests.sentry.tasks.integrations.*", + "tests.sentry.tasks.seer.test_delete_seer_grouping_records", "tests.sentry.tasks.test_code_owners", - "tests.sentry.tasks.test_delete_seer_grouping_records", "tests.sentry.tasks.test_on_demand_metrics", "tests.sentry.tempest.*", "tests.sentry.templatetags.*", diff --git a/setup.cfg b/setup.cfg index 3da942d5f47356..2d59e072898ce7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -88,6 +88,8 @@ sentry = # All other linting (E, W, F, B, LOG, I) is handled by ruff. # See [tool.ruff] in pyproject.toml for the main linting configuration. select = S +# S016 is temporarily disabled until the ThreadPoolExecutor migration is complete. +extend-ignore = S016 per-file-ignores = # these scripts must have minimal dependencies so opt out of the usual sentry rules .github/*: S diff --git a/src/sentry/api/endpoints/organization_events_trends_v2.py b/src/sentry/api/endpoints/organization_events_trends_v2.py index 54699a08872259..a4222c663c25e5 100644 --- a/src/sentry/api/endpoints/organization_events_trends_v2.py +++ b/src/sentry/api/endpoints/organization_events_trends_v2.py @@ -8,7 +8,6 @@ from rest_framework.response import Response from snuba_sdk import Column -from sentry import features from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase @@ -69,15 +68,7 @@ class OrganizationEventsNewTrendsStatsEndpoint(OrganizationEventsEndpointBase): } ) - def has_feature(self, organization, request): - return features.has( - "organizations:performance-new-trends", organization, actor=request.user - ) - def get(self, request: Request, organization: Organization) -> Response: - if not self.has_feature(organization, request): - return Response(status=404) - viewer_context = SeerViewerContext(organization_id=organization.id, user_id=request.user.id) try: diff --git a/src/sentry/api/endpoints/organization_plugin_deprecation_info.py b/src/sentry/api/endpoints/organization_plugin_deprecation_info.py index d6ee36ece454d9..dc041e55584da8 100644 --- a/src/sentry/api/endpoints/organization_plugin_deprecation_info.py +++ b/src/sentry/api/endpoints/organization_plugin_deprecation_info.py @@ -62,7 +62,7 @@ def get_plugin_rules_urls( and action.get("service") == plugin ): matching_rule_urls.append( - f"{url_prefix}/alerts/rules/{rule.project.slug}/{rule.id}/details/" + f"{url_prefix}/issues/alerts/rules/{rule.project.slug}/{rule.id}/details/" ) break return matching_rule_urls diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index b04e3b9519c2bf..e796ee585b971d 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -539,6 +539,9 @@ def env( CSP_FRAME_ANCESTORS = [ "'none'", ] +CSP_FRAME_SRC = [ + "demo.arcade.software", +] CSP_OBJECT_SRC = [ "'none'", ] @@ -867,6 +870,7 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: "sentry.integrations.github.tasks.codecov_account_unlink", "sentry.integrations.github.tasks.link_all_repos", "sentry.integrations.github.tasks.pr_comment", + "sentry.integrations.gitlab.tasks", "sentry.integrations.jira.tasks", "sentry.integrations.opsgenie.tasks", "sentry.integrations.slack.tasks.find_channel_id_for_alert_rule", @@ -922,7 +926,7 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: "sentry.tasks.auto_remove_inbox", "sentry.tasks.auto_resolve_issues", "sentry.tasks.auto_source_code_config", - "sentry.tasks.autofix", + "sentry.tasks.seer.autofix", "sentry.tasks.beacon", "sentry.tasks.check_am2_compatibility", "sentry.tasks.clear_expired_resolutions", @@ -934,7 +938,7 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: "sentry.tasks.commit_context", "sentry.tasks.commits", "sentry.tasks.delete_pending_groups", - "sentry.tasks.delete_seer_grouping_records", + "sentry.tasks.seer.delete_seer_grouping_records", "sentry.tasks.digests", "sentry.tasks.email", "sentry.tasks.files", @@ -954,7 +958,7 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: "sentry.tasks.repository", "sentry.tasks.reprocessing2", "sentry.tasks.scim.privilege_sync", - "sentry.tasks.seer", + "sentry.tasks.seer.cleanup", "sentry.tasks.statistical_detectors", "sentry.tasks.store", "sentry.tasks.summaries.weekly_reports", @@ -972,8 +976,8 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: "sentry.workflow_engine.tasks.delayed_workflows", "sentry.workflow_engine.tasks.workflows", "sentry.workflow_engine.tasks.actions", - "sentry.tasks.seer_explorer_index", - "sentry.tasks.context_engine_index", + "sentry.tasks.seer.explorer_index", + "sentry.tasks.seer.context_engine_index", # Used for tests "sentry.taskworker.tasks.examples", ) @@ -1412,7 +1416,6 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: # Sentry and internal client configuration SENTRY_EARLY_FEATURES = { - "organizations:performance-new-trends": "Enable new trends", "organizations:performance-new-widget-designs": "Enable updated landing page widget designs", "organizations:profiling-global-suspect-functions": "Enable global suspect functions in profiling", } @@ -2692,6 +2695,8 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: "taskworker-internal-dlq": "default", "taskworker-limited": "default", "taskworker-limited-dlq": "default", + "taskworker-launchpad": "default", + "taskworker-launchpad-dlq": "default", "taskworker-long": "default", "taskworker-long-dlq": "default", "taskworker-products": "default", diff --git a/src/sentry/conf/types/cell_config.py b/src/sentry/conf/types/cell_config.py index 0268f5f37901d3..49bf08a716fb96 100644 --- a/src/sentry/conf/types/cell_config.py +++ b/src/sentry/conf/types/cell_config.py @@ -16,4 +16,5 @@ class LocalityConfig(TypedDict): name: str category: str cells: list[str] + new_org_cell: str visible: NotRequired[bool] diff --git a/src/sentry/conf/types/kafka_definition.py b/src/sentry/conf/types/kafka_definition.py index be2c4fff4dd620..f6268e9bf891ce 100644 --- a/src/sentry/conf/types/kafka_definition.py +++ b/src/sentry/conf/types/kafka_definition.py @@ -96,6 +96,8 @@ class Topic(Enum): TASKWORKER_INGEST_PROFILING_DLQ = "taskworker-ingest-profiling-dlq" TASKWORKER_INTERNAL = "taskworker-internal" TASKWORKER_INTERNAL_DLQ = "taskworker-internal-dlq" + TASKWORKER_LAUNCHPAD = "taskworker-launchpad" + TASKWORKER_LAUNCHPAD_DLQ = "taskworker-launchpad-dlq" TASKWORKER_LIMITED = "taskworker-limited" TASKWORKER_LIMITED_DLQ = "taskworker-limited-dlq" TASKWORKER_LONG = "taskworker-long" diff --git a/src/sentry/core/endpoints/project_details.py b/src/sentry/core/endpoints/project_details.py index eba14f7bb321e6..a4ecd79c357fcc 100644 --- a/src/sentry/core/endpoints/project_details.py +++ b/src/sentry/core/endpoints/project_details.py @@ -57,7 +57,7 @@ from sentry.notifications.utils import has_alert_integration from sentry.relay.datascrubbing import validate_pii_config_update, validate_pii_selectors from sentry.seer.autofix.constants import AutofixAutomationTuningSettings -from sentry.tasks.delete_seer_grouping_records import call_seer_delete_project_grouping_records +from sentry.tasks.seer.delete_seer_grouping_records import call_seer_delete_project_grouping_records from sentry.tempest.utils import has_tempest_access logger = logging.getLogger(__name__) diff --git a/src/sentry/dashboards/models/generate_dashboard_artifact.py b/src/sentry/dashboards/models/generate_dashboard_artifact.py index bc645a3c386d08..3852c5323537fa 100644 --- a/src/sentry/dashboards/models/generate_dashboard_artifact.py +++ b/src/sentry/dashboards/models/generate_dashboard_artifact.py @@ -72,6 +72,8 @@ def check_blocklist(cls, v: str) -> str: class GeneratedWidgetLayout(BaseModel): + """Layout position and size on a 6-column grid. Widget widths in each row should sum to 6 to fill the grid completely.""" + x: int = Field( default=0, description=f"Column position (0-{GRID_WIDTH - 1}). x + w must not exceed {GRID_WIDTH}.", @@ -115,12 +117,17 @@ def fit_within_grid(cls, w: int, values: dict[str, Any]) -> int: class GeneratedWidget(BaseModel): + """A single dashboard widget. Default sizes by display type: big_number 2w x 1h (3 per row), line/area/bar/stacked_area/top_n 3w x 2h (2 per row), table 6w x 2h (full row).""" + title: str = Field(..., max_length=255) # Matches serializer description: str = Field( ..., max_length=255 ) # Length matches serializer, required field for generation display_type: DisplayType - widget_type: WidgetType + widget_type: WidgetType = Field( + ..., + description="Dataset to query. Use 'spans' as the default — it covers most use cases. Use 'error-events' for error-specific data, 'issue' for issue tracking, 'logs' for log data, 'tracemetrics' for trace metrics.", + ) queries: list[GeneratedWidgetQuery] layout: GeneratedWidgetLayout limit: int | None = Field(default=None, le=10, ge=1) @@ -128,5 +135,7 @@ class GeneratedWidget(BaseModel): class GeneratedDashboard(BaseModel): + """A complete dashboard definition on a 6-column grid. Widget widths per row must sum to 6. This is the sole output artifact.""" + title: str = Field(..., max_length=255) # Matches serializer widgets: list[GeneratedWidget] = Field(..., max_items=Dashboard.MAX_WIDGETS) diff --git a/src/sentry/deletions/defaults/group.py b/src/sentry/deletions/defaults/group.py index 197c6bc7cac6ef..aacaa44971e8a1 100644 --- a/src/sentry/deletions/defaults/group.py +++ b/src/sentry/deletions/defaults/group.py @@ -30,7 +30,9 @@ from sentry.notifications.models.notificationmessage import NotificationMessage from sentry.services.eventstore.models import Event from sentry.snuba.dataset import Dataset -from sentry.tasks.delete_seer_grouping_records import may_schedule_task_to_delete_hashes_from_seer +from sentry.tasks.seer.delete_seer_grouping_records import ( + may_schedule_task_to_delete_hashes_from_seer, +) from sentry.utils import metrics from ..base import BaseDeletionTask, BaseRelation, ModelDeletionTask, ModelRelation diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index da2bea99349778..c4d90b51361673 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -198,8 +198,6 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:performance-mep-bannerless-ui", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Re-enable histograms for Metrics Enhanced Performance Views manager.add("organizations:performance-mep-reintroduce-histograms", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Enable new trends - manager.add("organizations:performance-new-trends", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable updated landing page widget designs manager.add("organizations:performance-new-widget-designs", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable MongoDB support for the Queries module diff --git a/src/sentry/hybridcloud/services/control_organization_provisioning/model.py b/src/sentry/hybridcloud/services/control_organization_provisioning/model.py index 20e76051f7c38b..e72dfd93ef27ce 100644 --- a/src/sentry/hybridcloud/services/control_organization_provisioning/model.py +++ b/src/sentry/hybridcloud/services/control_organization_provisioning/model.py @@ -1,3 +1,7 @@ +from typing import Any + +from pydantic import root_validator + from sentry.hybridcloud.rpc import RpcModel @@ -8,3 +12,14 @@ class RpcOrganizationSlugReservation(RpcModel): slug: str region_name: str reservation_type: int + + @root_validator(pre=True) + @classmethod + def _accept_cell_name(cls, values: dict[str, Any]) -> dict[str, Any]: + if "cell_name" in values and "region_name" not in values: + values["region_name"] = values.pop("cell_name") + return values + + @property + def cell_name(self) -> str: + return self.region_name diff --git a/src/sentry/incidents/endpoints/bases.py b/src/sentry/incidents/endpoints/bases.py index e8c1fcd1d6778f..c05d556977499c 100644 --- a/src/sentry/incidents/endpoints/bases.py +++ b/src/sentry/incidents/endpoints/bases.py @@ -106,6 +106,53 @@ def convert_args( return args, kwargs +class WorkflowEngineProjectAlertRuleEndpoint(ProjectAlertRuleEndpoint): + def convert_args( + self, request: Request, alert_rule_id: int, *args: Any, **kwargs: Any + ) -> tuple[tuple[Any, ...], dict[str, Any]]: + args, kwargs = super(ProjectAlertRuleEndpoint, self).convert_args(request, *args, **kwargs) + project = kwargs["project"] + validated_alert_rule_id = to_valid_int_id("alert_rule_id", alert_rule_id, raise_404=True) + + # Allow orgs that have downgraded plans to delete metric alerts + if request.method != "DELETE" and not features.has( + "organizations:incidents", project.organization, actor=request.user + ): + raise ResourceDoesNotExist + + if not request.access.has_project_access(project): + raise PermissionDenied + + if features.has("organizations:workflow-engine-rule-serializers", project.organization): + try: + ard = AlertRuleDetector.objects.get( + alert_rule_id=validated_alert_rule_id, + detector__project=project, + ) + kwargs["alert_rule"] = ard.detector + except AlertRuleDetector.DoesNotExist: + # XXX: this means the detector was single written and has no ARD or related AlertRule object + try: + detector_id = get_object_id_from_fake_id(validated_alert_rule_id) + kwargs["alert_rule"] = Detector.objects.get( + id=detector_id, + project=project, + ) + except Detector.DoesNotExist: + raise ResourceDoesNotExist + + return args, kwargs + + try: + kwargs["alert_rule"] = AlertRule.objects.get( + projects=project, id=validated_alert_rule_id + ) + except AlertRule.DoesNotExist: + raise ResourceDoesNotExist + + return args, kwargs + + class WorkflowEngineOrganizationAlertRuleEndpoint(OrganizationAlertRuleEndpoint): def convert_args( self, request: Request, alert_rule_id: int, *args: Any, **kwargs: Any diff --git a/src/sentry/incidents/endpoints/project_alert_rule_details.py b/src/sentry/incidents/endpoints/project_alert_rule_details.py index 83b21fa3e23439..e779ae2514acfa 100644 --- a/src/sentry/incidents/endpoints/project_alert_rule_details.py +++ b/src/sentry/incidents/endpoints/project_alert_rule_details.py @@ -4,17 +4,20 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint -from sentry.incidents.endpoints.bases import ProjectAlertRuleEndpoint +from sentry.incidents.endpoints.bases import WorkflowEngineProjectAlertRuleEndpoint from sentry.incidents.endpoints.organization_alert_rule_details import ( fetch_alert_rule, remove_alert_rule, update_alert_rule, ) +from sentry.incidents.models.alert_rule import AlertRule +from sentry.models.project import Project +from sentry.workflow_engine.models import Detector from sentry.workflow_engine.utils.legacy_metric_tracking import track_alert_endpoint_execution @cell_silo_endpoint -class ProjectAlertRuleDetailsEndpoint(ProjectAlertRuleEndpoint): +class ProjectAlertRuleDetailsEndpoint(WorkflowEngineProjectAlertRuleEndpoint): owner = ApiOwner.ISSUES publish_status = { "DELETE": ApiPublishStatus.EXPERIMENTAL, @@ -23,7 +26,7 @@ class ProjectAlertRuleDetailsEndpoint(ProjectAlertRuleEndpoint): } @track_alert_endpoint_execution("GET", "sentry-api-0-project-alert-rule-details") - def get(self, request: Request, project, alert_rule) -> Response: + def get(self, request: Request, project: Project, alert_rule: AlertRule | Detector) -> Response: """ Fetch a metric alert rule. @deprecated. Use OrganizationAlertRuleDetailsEndpoint instead. `````````````````` @@ -32,7 +35,7 @@ def get(self, request: Request, project, alert_rule) -> Response: return fetch_alert_rule(request, project.organization, alert_rule) @track_alert_endpoint_execution("PUT", "sentry-api-0-project-alert-rule-details") - def put(self, request: Request, project, alert_rule) -> Response: + def put(self, request: Request, project: Project, alert_rule: AlertRule | Detector) -> Response: """ Update a metric alert rule. @deprecated. Use OrganizationAlertRuleDetailsEndpoint instead. `````````````````` @@ -41,7 +44,9 @@ def put(self, request: Request, project, alert_rule) -> Response: return update_alert_rule(request, project.organization, alert_rule) @track_alert_endpoint_execution("DELETE", "sentry-api-0-project-alert-rule-details") - def delete(self, request: Request, project, alert_rule) -> Response: + def delete( + self, request: Request, project: Project, alert_rule: AlertRule | Detector + ) -> Response: """ Delete a metric alert rule. @deprecated. Use OrganizationAlertRuleDetailsEndpoint instead. `````````````````` diff --git a/src/sentry/incidents/endpoints/utils.py b/src/sentry/incidents/endpoints/utils.py index 9038bb3d3c1012..f10e1c4314945d 100644 --- a/src/sentry/incidents/endpoints/utils.py +++ b/src/sentry/incidents/endpoints/utils.py @@ -41,14 +41,17 @@ def translate_threshold(alert_rule: AlertRule, threshold: float | None) -> float def translate_data_condition_type( - comparison_delta: int, condition_type: str, threshold: float | None + comparison_delta: int | None, condition_type: str, threshold: float | int | None ) -> float | None: """ Translates our internal percent representation into a delta percentage. For ABOVE: A percentage like 170% would become 70% increase For BELOW: A percentage like 40% would become 60% decrease. """ - if comparison_delta is None or threshold is None: + if threshold is None: return threshold + if comparison_delta is None: + return float(threshold) + return data_condition_type_translators[condition_type](threshold) diff --git a/src/sentry/integrations/api/endpoints/organization_repository_details.py b/src/sentry/integrations/api/endpoints/organization_repository_details.py index a704c48878287f..c843b7c2ae7666 100644 --- a/src/sentry/integrations/api/endpoints/organization_repository_details.py +++ b/src/sentry/integrations/api/endpoints/organization_repository_details.py @@ -23,7 +23,7 @@ from sentry.models.organization import Organization from sentry.models.repository import Repository from sentry.tasks.repository import repository_cascade_delete_on_hide -from sentry.tasks.seer import cleanup_seer_repository_preferences +from sentry.tasks.seer.cleanup import cleanup_seer_repository_preferences class RepositorySerializer(serializers.Serializer): diff --git a/src/sentry/integrations/gitlab/constants.py b/src/sentry/integrations/gitlab/constants.py index 81595083734d90..8635fc17a84eb5 100644 --- a/src/sentry/integrations/gitlab/constants.py +++ b/src/sentry/integrations/gitlab/constants.py @@ -1 +1,6 @@ GITLAB_CLOUD_BASE_URL = "https://gitlab.com" +# Webhook version tracking +# Increment this when webhook configuration changes (e.g., adding new event types) +# Version 1: Added issues_events support for assignment and comment sync +GITLAB_WEBHOOK_VERSION_KEY = "gitlab_webhook_version" +GITLAB_WEBHOOK_VERSION = 1 diff --git a/src/sentry/integrations/gitlab/integration.py b/src/sentry/integrations/gitlab/integration.py index a47c7e3adaa6e8..ae90f2dedd832a 100644 --- a/src/sentry/integrations/gitlab/integration.py +++ b/src/sentry/integrations/gitlab/integration.py @@ -20,6 +20,8 @@ IntegrationMetadata, IntegrationProvider, ) +from sentry.integrations.gitlab.constants import GITLAB_WEBHOOK_VERSION, GITLAB_WEBHOOK_VERSION_KEY +from sentry.integrations.gitlab.tasks import update_all_project_webhooks from sentry.integrations.pipeline import IntegrationPipeline from sentry.integrations.referrer_ids import GITLAB_PR_BOT_REFERRER from sentry.integrations.services.integration import integration_service @@ -257,7 +259,11 @@ def update_organization_config(self, data: MutableMapping[str, Any]) -> None: config = self.org_integration.config + # Check webhook version BEFORE updating config to determine if migration is needed + current_webhook_version = config.get(GITLAB_WEBHOOK_VERSION_KEY, 0) + config.update(data) + org_integration = integration_service.update_organization_integration( org_integration_id=self.org_integration.id, config=config, @@ -265,6 +271,15 @@ def update_organization_config(self, data: MutableMapping[str, Any]) -> None: if org_integration is not None: self.org_integration = org_integration + # Only update webhooks if: + # 1. A sync setting was enabled, AND + # 2. The webhook version is outdated + if current_webhook_version < GITLAB_WEBHOOK_VERSION: + update_all_project_webhooks.delay( + integration_id=self.model.id, + organization_id=self.organization_id, + ) + # CommitContextIntegration methods def on_create_or_update_comment_error(self, api_error: ApiError, metrics_base: str) -> bool: diff --git a/src/sentry/integrations/gitlab/metrics.py b/src/sentry/integrations/gitlab/metrics.py new file mode 100644 index 00000000000000..8b85525196be19 --- /dev/null +++ b/src/sentry/integrations/gitlab/metrics.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass +from enum import StrEnum + +from sentry.integrations.base import IntegrationDomain +from sentry.integrations.models import Integration +from sentry.integrations.services.integration import RpcIntegration +from sentry.integrations.utils.metrics import IntegrationEventLifecycleMetric + + +class GitLabTaskInteractionType(StrEnum): + """ + GitLab background task interaction types + """ + + UPDATE_ALL_PROJECT_WEBHOOKS = "update_all_project_webhooks" + UPDATE_PROJECT_WEBHOOK = "update_project_webhook" + + +class GitLabWebhookUpdateHaltReason(StrEnum): + """ + Reasons why a GitLab webhook update may halt without success/failure + """ + + INTEGRATION_NOT_FOUND = "integration_not_found" + ORG_INTEGRATION_NOT_FOUND = "org_integration_not_found" + NO_REPOSITORIES = "no_repositories" + REPOSITORY_NOT_FOUND = "repository_not_found" + MISSING_WEBHOOK_CONFIG = "missing_webhook_config" + + +@dataclass +class GitLabTaskEvent(IntegrationEventLifecycleMetric): + """ + An instance to be recorded of a GitLab background task execution + """ + + interaction_type: GitLabTaskInteractionType + integration: Integration | RpcIntegration + + def get_integration_name(self) -> str: + return self.integration.provider + + def get_integration_domain(self) -> IntegrationDomain: + return IntegrationDomain.PROJECT_MANAGEMENT + + def get_interaction_type(self) -> str: + return str(self.interaction_type) diff --git a/src/sentry/integrations/gitlab/tasks.py b/src/sentry/integrations/gitlab/tasks.py new file mode 100644 index 00000000000000..c605d5633f4557 --- /dev/null +++ b/src/sentry/integrations/gitlab/tasks.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +import logging + +from taskbroker_client.retry import Retry + +from sentry.constants import ObjectStatus +from sentry.integrations.gitlab.constants import GITLAB_WEBHOOK_VERSION, GITLAB_WEBHOOK_VERSION_KEY +from sentry.integrations.gitlab.metrics import ( + GitLabTaskEvent, + GitLabTaskInteractionType, + GitLabWebhookUpdateHaltReason, +) +from sentry.integrations.models.integration import Integration +from sentry.integrations.services.integration import integration_service +from sentry.integrations.services.repository import repository_service +from sentry.silo.base import SiloMode +from sentry.tasks.base import instrumented_task +from sentry.taskworker.namespaces import integrations_tasks + +logger = logging.getLogger(__name__) + + +@instrumented_task( + name="sentry.tasks.integrations.gitlab.update_project_webhook", + namespace=integrations_tasks, + silo_mode=SiloMode.CELL, + processing_deadline_duration=60, + retry=Retry(times=3, delay=60, on=(Exception,), ignore=(Integration.DoesNotExist,)), +) +def update_project_webhook(integration_id: int, organization_id: int, repository_id: int) -> None: + """ + Update a single project webhook for a GitLab integration. + This task is spawned by update_all_project_webhooks for each repository. + """ + integration = integration_service.get_integration( + integration_id=integration_id, status=ObjectStatus.ACTIVE + ) + if not integration: + logger.warning( + "update-project-webhook.integration-not-found", + extra={ + "integration_id": integration_id, + "organization_id": organization_id, + "repository_id": repository_id, + }, + ) + return + + with GitLabTaskEvent( + interaction_type=GitLabTaskInteractionType.UPDATE_PROJECT_WEBHOOK, + integration=integration, + ).capture() as lifecycle: + repo = repository_service.get_repository( + organization_id=organization_id, + id=repository_id, + ) + if not repo or repo.status != ObjectStatus.ACTIVE: + lifecycle.record_halt( + GitLabWebhookUpdateHaltReason.REPOSITORY_NOT_FOUND, + extra={ + "integration_id": integration_id, + "organization_id": organization_id, + "repository_id": repository_id, + }, + ) + return + + lifecycle.add_extra("repository_id", repository_id) + + webhook_id = repo.config.get("webhook_id") + project_id = repo.config.get("project_id") + + if not webhook_id or not project_id: + lifecycle.record_halt( + GitLabWebhookUpdateHaltReason.MISSING_WEBHOOK_CONFIG, + extra={ + "repository_id": repo.id, + "repository_name": repo.name, + "has_webhook_id": bool(webhook_id), + "has_project_id": bool(project_id), + }, + ) + return + + installation = integration.get_installation(organization_id=organization_id) + client = installation.get_client() + + client.update_project_webhook(project_id, webhook_id) + logger.info( + "update-project-webhook.webhook-updated", + extra={ + "repository_id": repo.id, + "repository_name": repo.name, + "project_id": project_id, + "webhook_id": webhook_id, + }, + ) + + +@instrumented_task( + name="sentry.tasks.integrations.gitlab.update_all_project_webhooks", + namespace=integrations_tasks, + silo_mode=SiloMode.CELL, + retry=Retry(times=3, delay=60, on=(Exception,), ignore=(Integration.DoesNotExist,)), +) +def update_all_project_webhooks(integration_id: int, organization_id: int) -> None: + """ + Spawn individual tasks to update all project webhooks for a GitLab integration. + This is triggered when sync settings are changed to ensure all webhooks have the correct permissions. + """ + integration = integration_service.get_integration( + integration_id=integration_id, status=ObjectStatus.ACTIVE + ) + if not integration: + logger.warning( + "update-all-project-webhooks.integration-not-found", + extra={"integration_id": integration_id, "organization_id": organization_id}, + ) + return + + with GitLabTaskEvent( + interaction_type=GitLabTaskInteractionType.UPDATE_ALL_PROJECT_WEBHOOKS, + integration=integration, + ).capture() as lifecycle: + # Get all active repositories linked to this integration + repositories = repository_service.get_repositories( + integration_id=integration_id, + organization_id=organization_id, + status=ObjectStatus.ACTIVE, + ) + + if not repositories: + logger.info( + "update-all-project-webhooks.no-repositories", + extra={"integration_id": integration_id, "organization_id": organization_id}, + ) + lifecycle.record_halt(GitLabWebhookUpdateHaltReason.NO_REPOSITORIES) + return + + lifecycle.add_extra("total_repositories", len(repositories)) + + # Verify org integration exists before spawning tasks + org_integration = integration_service.get_organization_integration( + integration_id=integration_id, organization_id=organization_id + ) + if not org_integration: + logger.warning( + "update-all-project-webhooks.org-integration-not-found", + extra={"integration_id": integration_id, "organization_id": organization_id}, + ) + lifecycle.record_halt(GitLabWebhookUpdateHaltReason.ORG_INTEGRATION_NOT_FOUND) + return + + # Spawn individual tasks for each repository webhook update + for repo in repositories: + update_project_webhook.delay(integration_id, organization_id, repo.id) + + logger.info( + "update-all-project-webhooks.tasks-spawned", + extra={ + "integration_id": integration_id, + "organization_id": organization_id, + "total_repositories": len(repositories), + "repository_ids": [repo.id for repo in repositories], + }, + ) + + # Update webhook version to prevent re-triggering on subsequent config changes + config = org_integration.config.copy() + config[GITLAB_WEBHOOK_VERSION_KEY] = GITLAB_WEBHOOK_VERSION + integration_service.update_organization_integration( + org_integration_id=org_integration.id, + config=config, + ) diff --git a/src/sentry/integrations/messaging/message_builder.py b/src/sentry/integrations/messaging/message_builder.py index ee389da594d60b..1516ff36a31599 100644 --- a/src/sentry/integrations/messaging/message_builder.py +++ b/src/sentry/integrations/messaging/message_builder.py @@ -273,9 +273,13 @@ def build_rule_url(rule: Any, group: Group, project: Project) -> str: project_slug = project.slug if should_fire_workflow_actions(group.organization, group.type): rule_id = get_key_from_rule_data(rule, "legacy_rule_id") - rule_url = f"/organizations/{org_slug}/alerts/rules/{project_slug}/{rule_id}/details/" + rule_url = ( + f"/organizations/{org_slug}/issues/alerts/rules/{project_slug}/{rule_id}/details/" + ) else: - rule_url = f"/organizations/{org_slug}/alerts/rules/{project_slug}/{rule.id}/details/" + rule_url = ( + f"/organizations/{org_slug}/issues/alerts/rules/{project_slug}/{rule.id}/details/" + ) return absolute_uri(rule_url) diff --git a/src/sentry/integrations/opsgenie/client.py b/src/sentry/integrations/opsgenie/client.py index 8dd34610e174e6..75b083cc34e610 100644 --- a/src/sentry/integrations/opsgenie/client.py +++ b/src/sentry/integrations/opsgenie/client.py @@ -61,7 +61,7 @@ def _get_rule_urls(self, group, rules): if should_fire_workflow_actions(organization, group.type): rule_id = get_key_from_rule_data(rule, "legacy_rule_id") - path = f"/organizations/{organization.slug}/alerts/rules/{group.project.slug}/{rule_id}/details/" + path = f"/organizations/{organization.slug}/issues/alerts/rules/{group.project.slug}/{rule_id}/details/" rule_urls.append(organization.absolute_url(path)) return rule_urls diff --git a/src/sentry/notifications/utils/__init__.py b/src/sentry/notifications/utils/__init__.py index 4f427867e6fd09..30b4abd24c4743 100644 --- a/src/sentry/notifications/utils/__init__.py +++ b/src/sentry/notifications/utils/__init__.py @@ -130,8 +130,8 @@ def get_rules( NotificationRuleDetails( rule.id, rule.label, - f"/organizations/{organization.slug}/alerts/rules/{project.slug}/{rule.id}/", - f"/organizations/{organization.slug}/alerts/rules/{project.slug}/{rule.id}/details/", + f"/organizations/{organization.slug}/issues/alerts/rules/{project.slug}/{rule.id}/", + f"/organizations/{organization.slug}/issues/alerts/rules/{project.slug}/{rule.id}/details/", ) for rule in rules ] diff --git a/src/sentry/notifications/utils/links.py b/src/sentry/notifications/utils/links.py index 014ffdc8e73e72..4c0172c63c80fb 100644 --- a/src/sentry/notifications/utils/links.py +++ b/src/sentry/notifications/utils/links.py @@ -135,8 +135,8 @@ def get_rules_with_legacy_ids( NotificationRuleDetails( rule_id, rule.label, - f"/organizations/{organization.slug}/alerts/rules/{project.slug}/{rule_id}/", - f"/organizations/{organization.slug}/alerts/rules/{project.slug}/{rule_id}/details/", + f"/organizations/{organization.slug}/issues/alerts/rules/{project.slug}/{rule_id}/", + f"/organizations/{organization.slug}/issues/alerts/rules/{project.slug}/{rule_id}/details/", ) ) return rules_with_legacy_ids @@ -171,4 +171,4 @@ def get_snooze_url( # should only be using rule if key == "workflow_id": rule_id = str(rule.id) - return f"/organizations/{organization.slug}/alerts/rules/{project.slug}/{rule_id}/details/{sentry_query_params}&{urlencode({'mute': '1'})}" + return f"/organizations/{organization.slug}/issues/alerts/rules/{project.slug}/{rule_id}/details/{sentry_query_params}&{urlencode({'mute': '1'})}" diff --git a/src/sentry/rules/actions/integrations/create_ticket/utils.py b/src/sentry/rules/actions/integrations/create_ticket/utils.py index 8b76e69062822a..f2e174ea989347 100644 --- a/src/sentry/rules/actions/integrations/create_ticket/utils.py +++ b/src/sentry/rules/actions/integrations/create_ticket/utils.py @@ -98,7 +98,9 @@ def build_description( Format the description of the ticket/work item """ project = event.group.project - rule_url = f"/organizations/{project.organization.slug}/alerts/rules/{project.slug}/{rule_id}/" + rule_url = ( + f"/organizations/{project.organization.slug}/issues/alerts/rules/{project.slug}/{rule_id}/" + ) description: str = installation.get_group_description(event.group, event) + generate_footer( rule_url diff --git a/src/sentry/seer/autofix/autofix.py b/src/sentry/seer/autofix/autofix.py index 087dd2cf7f03e1..302dedfad2d8ed 100644 --- a/src/sentry/seer/autofix/autofix.py +++ b/src/sentry/seer/autofix/autofix.py @@ -47,7 +47,7 @@ from sentry.services.eventstore.models import Event, GroupEvent from sentry.snuba.ourlogs import OurLogs from sentry.snuba.referrer import Referrer -from sentry.tasks.autofix import check_autofix_status +from sentry.tasks.seer.autofix import check_autofix_status from sentry.users.models.user import User from sentry.users.services.user.model import RpcUser from sentry.utils.event_frames import EventFrame diff --git a/src/sentry/seer/similarity/similar_issues.py b/src/sentry/seer/similarity/similar_issues.py index eb1b9231d62ceb..0949679f28c4d6 100644 --- a/src/sentry/seer/similarity/similar_issues.py +++ b/src/sentry/seer/similarity/similar_issues.py @@ -23,7 +23,7 @@ SimilarHashNotFoundError, SimilarIssuesEmbeddingsRequest, ) -from sentry.tasks.delete_seer_grouping_records import delete_seer_grouping_records_by_hash +from sentry.tasks.seer.delete_seer_grouping_records import delete_seer_grouping_records_by_hash from sentry.utils import json, metrics from sentry.utils.circuit_breaker2 import CircuitBreaker from sentry.utils.json import JSONDecodeError diff --git a/src/sentry/spans/buffer.py b/src/sentry/spans/buffer.py index d820dc1ad12057..c62fd4020c157f 100644 --- a/src/sentry/spans/buffer.py +++ b/src/sentry/spans/buffer.py @@ -93,7 +93,7 @@ from sentry.spans.consumers.process_segments.types import attribute_value from sentry.spans.debug_trace_logger import DebugTraceLogger from sentry.spans.segment_key import ( - DistributedPayloadKey, + PayloadKey, SegmentKey, parse_segment_key, segment_key_to_span_id, @@ -148,7 +148,7 @@ class FlushedSegment(NamedTuple): 0.0 # Queue score at flush time, used for conditional cleanup in done_flush_segments ) ingested_count: int = 0 # Ingested count at flush time, used for conditional data cleanup - distributed_payload_keys: list[DistributedPayloadKey] = [] # For cleanup + payload_keys: list[PayloadKey] = [] # For cleanup class SpansBuffer: @@ -166,7 +166,6 @@ def __init__(self, assigned_shards: list[int], slice_id: int | None = None): self._buffer_logger = BufferLogger() self._flusher_logger = FlusherLogger() self._debug_trace_logger: DebugTraceLogger | None = None - self._distributed_payload_keys_map: dict[SegmentKey, list[bytes]] = {} @cached_property def client(self) -> RedisCluster[bytes] | StrictRedis[bytes]: @@ -179,30 +178,13 @@ def __reduce__(self): def _get_span_key(self, project_and_trace: str, span_id: str) -> bytes: return f"span-buf:s:{{{project_and_trace}}}:{span_id}".encode("ascii") - def _get_distributed_payload_key( - self, project_and_trace: str, span_id: str - ) -> DistributedPayloadKey: + def _get_payload_key(self, project_and_trace: str, span_id: str) -> PayloadKey: return f"span-buf:s:{{{project_and_trace}:{span_id}}}:{span_id}".encode("ascii") def _get_payload_key_index(self, segment_key: SegmentKey) -> bytes: project_id, trace_id, span_id = parse_segment_key(segment_key) return b"span-buf:mk:{%s:%s}:%s" % (project_id, trace_id, span_id) - def _cleanup_distributed_keys(self, segment_keys: set[SegmentKey]) -> None: - """Delete member-keys tracking sets and distributed payload keys for the - given segments, and remove them from the payload keys map so - done_flush_segments doesn't try again.""" - with self.client.pipeline(transaction=False) as p: - for key in segment_keys: - payload_keys = self._distributed_payload_keys_map.get(key, []) - if payload_keys: - mk_key = self._get_payload_key_index(key) - p.delete(mk_key) - for distributed_key in payload_keys: - p.unlink(distributed_key) - self._distributed_payload_keys_map.pop(key, None) - p.execute() - @metrics.wraps("spans.buffer.process_spans") def process_spans(self, spans: Sequence[Span], now: int): """ @@ -255,10 +237,7 @@ def process_spans(self, spans: Sequence[Span], now: int): for (project_and_trace, parent_span_id), subsegment in batch: set_members = self._prepare_payloads(subsegment) if write_distributed_payloads: - # Write to distributed key. - dist_key = self._get_distributed_payload_key( - project_and_trace, parent_span_id - ) + dist_key = self._get_payload_key(project_and_trace, parent_span_id) p.sadd(dist_key, *set_members) p.expire(dist_key, redis_ttl) @@ -590,7 +569,7 @@ def flush_segments(self, now: int) -> dict[SegmentKey, FlushedSegment]: segment_to_queue = { segment_key: queue_key for _, queue_key, segment_key, _ in segment_keys } - segments, ingested_counts = self._load_segment_data( + segments, payload_keys_map, ingested_counts = self._load_segment_data( [k for _, _, k, _ in segment_keys], segment_to_queue, now, @@ -642,7 +621,7 @@ def flush_segments(self, now: int) -> dict[SegmentKey, FlushedSegment]: project_id=int(project_id.decode("ascii")), score=score, ingested_count=ingested_counts.get(segment_key, 0), - distributed_payload_keys=self._distributed_payload_keys_map.get(segment_key, []), + payload_keys=payload_keys_map.get(segment_key, []), ) num_has_root_spans += int(has_root_span) @@ -686,52 +665,14 @@ def flush_segments(self, now: int) -> dict[SegmentKey, FlushedSegment]: self.any_shard_at_limit = any_shard_at_limit return return_segments - def _apply_per_trace_limit( - self, - segment_keys: list[tuple[int, QueueKey, SegmentKey]], - max_per_trace: int, - now: int, - ) -> list[tuple[int, QueueKey, SegmentKey]]: - """ - Limits how many segments a single trace can be flushed in one cycle. - Prevents a trace from monopolizing the cycle and concentrating all - operations on one Redis node. - - Deferred segments have their score set to ``now`` so they no longer - sit at the front of the sorted set. This avoids head-of-line - blocking where a hot trace's overdue segments are re-fetched and - re-discarded every cycle. - """ - if max_per_trace <= 0: - return segment_keys - trace_counts: dict[bytes, int] = {} - accepted: list[tuple[int, QueueKey, SegmentKey]] = [] - deferred: list[tuple[int, QueueKey, SegmentKey]] = [] - for shard, queue_key, segment_key in segment_keys: - _, trace_id, _ = parse_segment_key(segment_key) - count = trace_counts.get(trace_id, 0) - if count < max_per_trace: - accepted.append((shard, queue_key, segment_key)) - trace_counts[trace_id] = count + 1 - else: - deferred.append((shard, queue_key, segment_key)) - - if deferred: - num_deferred = len(deferred) - metrics.incr("spans.buffer.flush_segments.deferred", amount=num_deferred) - with self.client.pipeline(transaction=False) as p: - for shard, queue_key, segment_key in deferred: - p.zadd(queue_key, {segment_key: now}) - p.execute() - - return accepted - def _load_segment_data( self, segment_keys: list[SegmentKey], segment_to_queue: dict[SegmentKey, QueueKey], now: int, - ) -> tuple[dict[SegmentKey, list[bytes]], dict[SegmentKey, int]]: + ) -> tuple[ + dict[SegmentKey, list[bytes]], dict[SegmentKey, list[PayloadKey]], dict[SegmentKey, int] + ]: """ Loads the segments from Redis, given a list of segment keys. Segments exceeding a certain size are skipped, and an error is logged. @@ -748,13 +689,14 @@ def _load_segment_data( write_distributed_payloads = options.get("spans.buffer.write-distributed-payloads") payloads: dict[SegmentKey, list[bytes]] = {key: [] for key in segment_keys} + payload_keys_map: dict[SegmentKey, list[PayloadKey]] = {key: [] for key in segment_keys} sizes: dict[SegmentKey, int] = {key: 0 for key in segment_keys} self._last_decompress_latency_ms = 0 decompress_latency_ms = 0.0 # Maps each scan key back to the segment it belongs to. For merged # keys these are the same; for distributed keys many map to one segment. - scan_key_to_segment: dict[SegmentKey | DistributedPayloadKey, SegmentKey] = {} + scan_key_to_segment: dict[SegmentKey | PayloadKey, SegmentKey] = {} # When read_distributed_payloads is off, scan merged segment keys directly. # When on, skip them — all data lives in distributed keys. @@ -764,27 +706,25 @@ def _load_segment_data( scan_key_to_segment[key] = key cursors[key] = 0 - self._distributed_payload_keys_map = {} - if write_distributed_payloads: with self.client.pipeline(transaction=False) as p: for key in segment_keys: p.smembers(self._get_payload_key_index(key)) mk_results = p.execute() - for key, sub_span_ids in zip(segment_keys, mk_results): + for key, payload_key_span_ids in zip(segment_keys, mk_results): project_id, trace_id, _ = parse_segment_key(key) - pat = f"{project_id.decode('ascii')}:{trace_id.decode('ascii')}" - distributed_keys: list[bytes] = [] - for sub_span_id in sub_span_ids: - distributed_key = self._get_distributed_payload_key( - pat, sub_span_id.decode("ascii") + project_and_trace = f"{project_id.decode('ascii')}:{trace_id.decode('ascii')}" + segment_payload_keys: list[PayloadKey] = [] + for payload_key_span_id in payload_key_span_ids: + payload_key = self._get_payload_key( + project_and_trace, payload_key_span_id.decode("ascii") ) - distributed_keys.append(distributed_key) + segment_payload_keys.append(payload_key) if read_distributed_payloads: - scan_key_to_segment[distributed_key] = key - cursors[distributed_key] = 0 - self._distributed_payload_keys_map[key] = distributed_keys + scan_key_to_segment[payload_key] = key + cursors[payload_key] = 0 + payload_keys_map[key] = segment_payload_keys dropped_segments: set[SegmentKey] = set() @@ -839,9 +779,6 @@ def _add_spans(key: SegmentKey, raw_data: bytes) -> bool: else: cursors[key] = cursor - if dropped_segments: - self._cleanup_distributed_keys(dropped_segments) - # Fetch ingested counts for all segments to calculate dropped spans with self.client.pipeline(transaction=False) as p: for key in segment_keys: @@ -930,7 +867,7 @@ def _add_spans(key: SegmentKey, raw_data: bytes) -> bool: self._last_decompress_latency_ms = int(decompress_latency_ms) - return payloads, ingested_counts + return payloads, payload_keys_map, ingested_counts def done_flush_segments(self, segment_keys: dict[SegmentKey, FlushedSegment]): metrics.timing("spans.buffer.done_flush_segments.num_segments", len(segment_keys)) @@ -1028,11 +965,11 @@ def done_flush_segments(self, segment_keys: dict[SegmentKey, FlushedSegment]): queue_removals.setdefault(flushed_segment.queue_key, []).append(segment_key) - if flushed_segment.distributed_payload_keys: + if flushed_segment.payload_keys: mk_key = self._get_payload_key_index(segment_key) p.delete(mk_key) - for distributed_key in flushed_segment.distributed_payload_keys: - p.unlink(distributed_key) + for payload_key in flushed_segment.payload_keys: + p.unlink(payload_key) for queue_key, keys in queue_removals.items(): for key_batch in itertools.batched(keys, 100): diff --git a/src/sentry/spans/segment_key.py b/src/sentry/spans/segment_key.py index 0d9b314afe851f..eae58585049e03 100644 --- a/src/sentry/spans/segment_key.py +++ b/src/sentry/spans/segment_key.py @@ -6,10 +6,10 @@ # The segment ID in the Kafka protocol is only the span ID. SegmentKey = bytes -# DistributedPayloadKey is a Redis key for a payload set that uses a per-span +# PayloadKey is a Redis key for a payload set that uses a per-span # hash tag: "span-buf:s:{project_id:trace_id:span_id}:span_id". This distributes # payloads across Redis cluster nodes instead of merging by trace in a single key. -DistributedPayloadKey = bytes +PayloadKey = bytes def parse_segment_key(segment_key: SegmentKey) -> tuple[bytes, bytes, bytes]: diff --git a/src/sentry/tasks/autofix.py b/src/sentry/tasks/autofix.py index 86107ce8a348ea..fe0a41217d3ba5 100644 --- a/src/sentry/tasks/autofix.py +++ b/src/sentry/tasks/autofix.py @@ -1,303 +1,5 @@ -import logging -from datetime import datetime, timedelta +# Shim for backwards compatibility with getsentry imports. +# Remove once getsentry is updated to import from sentry.tasks.seer.autofix. +from sentry.tasks.seer.autofix import configure_seer_for_existing_org -import sentry_sdk -from django.utils import timezone -from taskbroker_client.retry import Retry -from taskbroker_client.state import current_task - -from sentry import analytics, features -from sentry.analytics.events.autofix_automation_events import AiAutofixAutomationEvent -from sentry.constants import ObjectStatus -from sentry.models.group import Group -from sentry.models.organization import Organization -from sentry.models.project import Project -from sentry.seer.autofix.constants import ( - AutofixAutomationTuningSettings, - AutofixStatus, - SeerAutomationSource, -) -from sentry.seer.autofix.utils import ( - bulk_get_project_preferences, - bulk_set_project_preferences, - bulk_write_preferences_to_sentry_db, - deduplicate_repositories, - get_autofix_repos_from_project_code_mappings, - get_autofix_state, - get_seer_seat_based_tier_cache_key, - resolve_repository_ids, -) -from sentry.seer.models import SeerProjectPreference -from sentry.tasks.base import instrumented_task -from sentry.taskworker.namespaces import ingest_errors_tasks, issues_tasks -from sentry.utils import metrics -from sentry.utils.cache import cache - -logger = logging.getLogger(__name__) - - -def _get_group_or_log(group_id: int, task_name: str) -> Group | None: - """Fetch a Group by ID, returning None and logging a warning if it no longer exists.""" - try: - return Group.objects.get(id=group_id) - except Group.DoesNotExist: - logger.warning("%s.group_not_found", task_name, extra={"group_id": group_id}) - return None - - -@instrumented_task( - name="sentry.tasks.autofix.check_autofix_status", - namespace=issues_tasks, - retry=Retry(times=1), -) -def check_autofix_status(run_id: int, organization_id: int) -> None: - state = get_autofix_state(run_id=run_id, organization_id=organization_id) - - if ( - state - and state.status == AutofixStatus.PROCESSING - and state.updated_at < datetime.now() - timedelta(minutes=5) - ): - # This should log to sentry - logger.error( - "Autofix run has been processing for more than 5 minutes", extra={"run_id": run_id} - ) - - -@instrumented_task( - name="sentry.tasks.autofix.generate_summary_and_run_automation", - namespace=ingest_errors_tasks, - processing_deadline_duration=35, - retry=Retry(times=1), -) -def generate_summary_and_run_automation(group_id: int, **kwargs) -> None: - from sentry.seer.autofix.issue_summary import get_issue_summary - - trigger_path = kwargs.get("trigger_path", "unknown") - sentry_sdk.set_tag("trigger_path", trigger_path) - - group = _get_group_or_log(group_id, "generate_summary_and_run_automation") - if group is None: - return - organization = group.project.organization - - task_state = current_task() - if task_state is None or task_state.attempt == 0: - metrics.incr("sentry.tasks.autofix.generate_summary_and_run_automation", sample_rate=1.0) - analytics.record( - AiAutofixAutomationEvent( - organization_id=organization.id, - project_id=group.project_id, - group_id=group.id, - task_name="generate_summary_and_run_automation", - issue_event_count=group.times_seen, - fixability_score=group.seer_fixability_score, - ) - ) - - get_issue_summary(group=group, source=SeerAutomationSource.POST_PROCESS) - - -@instrumented_task( - name="sentry.tasks.autofix.generate_issue_summary_only", - namespace=ingest_errors_tasks, - processing_deadline_duration=35, - retry=Retry(times=3, delay=3, on=(Exception,)), -) -def generate_issue_summary_only(group_id: int) -> None: - """ - Generate issue summary WITHOUT triggering automation. - Used for triage signals flow when event count < 10 or when summary doesn't exist yet. - """ - from sentry.seer.autofix.issue_summary import ( - get_and_update_group_fixability_score, - get_issue_summary, - ) - - group = _get_group_or_log(group_id, "generate_issue_summary_only") - if group is None: - return - organization = group.project.organization - - task_state = current_task() - if task_state is None or task_state.attempt == 0: - metrics.incr("sentry.tasks.autofix.generate_issue_summary_only", sample_rate=1.0) - analytics.record( - AiAutofixAutomationEvent( - organization_id=organization.id, - project_id=group.project_id, - group_id=group.id, - task_name="generate_issue_summary_only", - issue_event_count=group.times_seen, - fixability_score=group.seer_fixability_score, - ) - ) - - # Generate and cache the summary - get_issue_summary( - group=group, source=SeerAutomationSource.POST_PROCESS, should_run_automation=False - ) - - get_and_update_group_fixability_score(group, force_generate=True) - - -@instrumented_task( - name="sentry.tasks.autofix.run_automation_only_task", - namespace=ingest_errors_tasks, - processing_deadline_duration=35, - retry=Retry(times=1), -) -def run_automation_only_task(group_id: int) -> None: - """ - Run automation directly for a group (assumes summary and fixability already exist). - Used for triage signals flow when event count >= 10 and summary exists. - """ - from django.contrib.auth.models import AnonymousUser - - from sentry.seer.autofix.issue_summary import run_automation - - group = _get_group_or_log(group_id, "run_automation_only_task") - if group is None: - return - organization = group.project.organization - - task_state = current_task() - if task_state is None or task_state.attempt == 0: - metrics.incr("sentry.tasks.autofix.run_automation_only_task", sample_rate=1.0) - analytics.record( - AiAutofixAutomationEvent( - organization_id=organization.id, - project_id=group.project_id, - group_id=group.id, - task_name="run_automation_only", - issue_event_count=group.times_seen, - fixability_score=group.seer_fixability_score, - ) - ) - - event = group.get_latest_event() - - if not event: - logger.warning("run_automation_only_task.no_event_found", extra={"group_id": group_id}) - return - - # Track issue age when running automation - issue_age_days = int((timezone.now() - group.first_seen).total_seconds() / (60 * 60 * 24)) - metrics.distribution( - "seer.automation.issue_age_since_first_seen", issue_age_days, unit="day", sample_rate=1.0 - ) - - run_automation( - group=group, user=AnonymousUser(), event=event, source=SeerAutomationSource.POST_PROCESS - ) - - -@instrumented_task( - name="sentry.tasks.autofix.configure_seer_for_existing_org", - namespace=issues_tasks, - processing_deadline_duration=90, - retry=Retry(times=3), -) -def configure_seer_for_existing_org(organization_id: int) -> None: - """ - Configure Seer settings for a new or existing organization migrating to new Seer pricing. - - Sets: - - Project-level (all projects): seer_scanner_automation=True, autofix_automation_tuning="medium" or "off" - - Seer API (all projects): automated_run_stopping_point="code_changes" or "open_pr" - - Ignores: - - Org-level: enable_seer_coding - """ - - organization = Organization.objects.get(id=organization_id) - - sentry_sdk.set_tag("organization_id", organization.id) - sentry_sdk.set_tag("organization_slug", organization.slug) - - # Set org-level options - organization.update_option( - "sentry:default_autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM - ) - - projects = list( - Project.objects.filter(organization_id=organization_id, status=ObjectStatus.ACTIVE) - ) - project_ids = [p.id for p in projects] - - if len(project_ids) == 0: - return - - # If seer is enabled for an org, every project must have project level settings - for project in projects: - project.update_option("sentry:seer_scanner_automation", True) - autofix_automation_tuning = project.get_option("sentry:autofix_automation_tuning") - if autofix_automation_tuning != AutofixAutomationTuningSettings.OFF: - project.update_option( - "sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM - ) - - preferences_by_id = bulk_get_project_preferences(organization_id, project_ids) - - # Determine which projects need updates - preferences_to_set = [] - projects_by_id = {p.id: p for p in projects} - for project_id in project_ids: - existing_pref = preferences_by_id.get(str(project_id)) - if not existing_pref: - # No existing preferences, get repositories from code mappings - repositories = get_autofix_repos_from_project_code_mappings(projects_by_id[project_id]) - else: - # Skip projects that already have an acceptable stopping point configured - if existing_pref.get("automated_run_stopping_point") in ("open_pr", "code_changes"): - continue - repositories = existing_pref.get("repositories") or [] - - repositories = deduplicate_repositories(repositories) - - # Preserve existing repositories and automation_handoff, only update the stopping point - preferences_to_set.append( - { - "organization_id": organization_id, - "project_id": project_id, - "repositories": repositories or [], - "automated_run_stopping_point": "code_changes", - "automation_handoff": ( - existing_pref.get("automation_handoff") if existing_pref else None - ), - } - ) - - if len(preferences_to_set) > 0: - bulk_set_project_preferences(organization_id, preferences_to_set) - - if features.has("organizations:seer-project-settings-dual-write", organization): - try: - validated_preferences = [ - SeerProjectPreference.validate(pref) for pref in preferences_to_set - ] - # Seer API responses don't include repository_id. - # Resolve before dual-writing so repos aren't skipped. - # This will not be necessary once we start keying by repo ID. - resolved_preferences = resolve_repository_ids( - organization_id, validated_preferences - ) - bulk_write_preferences_to_sentry_db(projects, resolved_preferences) - except Exception: - logger.exception( - "seer.write_preferences.failed", extra={"organization_id": organization_id} - ) - - # Invalidate existing cache entry and set cache to True to prevent race conditions where another - # request re-caches False before the billing flag has fully propagated - cache.set(get_seer_seat_based_tier_cache_key(organization_id), True, timeout=60 * 5) - - logger.info( - "Task: configure_seer_for_existing_org completed", - extra={ - "org_id": organization.id, - "org_slug": organization.slug, - "projects_configured": len(project_ids), - "preferences_set_via_api": len(preferences_to_set), - }, - ) +__all__ = ["configure_seer_for_existing_org"] diff --git a/src/sentry/tasks/context_engine_index.py b/src/sentry/tasks/context_engine_index.py index ce4a3386e89ff6..8d02a6a158d90c 100644 --- a/src/sentry/tasks/context_engine_index.py +++ b/src/sentry/tasks/context_engine_index.py @@ -1,299 +1,17 @@ -from __future__ import annotations - -import logging -from datetime import UTC, datetime, timedelta, timezone - -import sentry_sdk -from taskbroker_client.retry import Retry - -from sentry import features, options -from sentry.constants import ObjectStatus -from sentry.models.organization import Organization -from sentry.models.project import Project -from sentry.search.events.types import SnubaParams -from sentry.seer.explorer.context_engine_utils import ( - EVENT_COUNT_LOOKBACK_DAYS, - ProjectEventCounts, - get_event_counts_for_org_projects, - get_instrumentation_types, - get_sdk_names_for_org_projects, - get_top_span_ops_for_org_projects, - get_top_transactions_for_org_projects, -) -from sentry.seer.explorer.explorer_service_map_utils import ( - _build_nodes, - _query_service_dependencies, - _send_to_seer, -) -from sentry.seer.models import SeerApiError -from sentry.seer.signed_seer_api import ( - ExplorerIndexSentryKnowledgeRequest, - OrgProjectKnowledgeIndexRequest, - OrgProjectKnowledgeProjectData, - SeerViewerContext, - make_index_sentry_knowledge_request, - make_org_project_knowledge_index_request, -) -from sentry.tasks.base import instrumented_task -from sentry.taskworker.namespaces import seer_tasks -from sentry.utils.hashlib import md5_text -from sentry.utils.query import RangeQuerySetWrapper -from sentry.utils.snuba_rpc import SnubaRPCRateLimitExceeded - -logger = logging.getLogger(__name__) - - -@instrumented_task( - name="sentry.tasks.context_engine_index.index_org_project_knowledge", - namespace=seer_tasks, - processing_deadline_duration=10 * 60, -) -def index_org_project_knowledge(org_id: int) -> None: - """ - For a given org, list active projects, assemble project metadata and call - the Seer endpoint to generate LLM summaries and embeddings. - """ - if not options.get("explorer.context_engine_indexing.enable"): - logger.info("explorer.context_engine_indexing.enable flag is disabled") - return - - projects = list( - Project.objects.filter(organization_id=org_id, status=ObjectStatus.ACTIVE).select_related( - "organization" - ) - ) - if not projects: - logger.warning( - "No projects found for index_org_project_knowledge", - extra={"org_id": org_id}, - ) - return - - end = datetime.now(UTC) - start = end - timedelta(days=EVENT_COUNT_LOOKBACK_DAYS) - - project_ids = [p.id for p in projects] - event_counts = get_event_counts_for_org_projects(org_id, project_ids, start, end) - high_volume_projects = [p for p in projects if p.id in event_counts] - if not high_volume_projects: - logger.info( - "No high-volume projects found for index_org_project_knowledge", - extra={"org_id": org_id, "num_projects": len(projects)}, - ) - return - - with sentry_sdk.start_span(op="explorer.context_engine.get_top_transactions_for_org_projects"): - transactions_by_project = get_top_transactions_for_org_projects( - high_volume_projects, start, end - ) - with sentry_sdk.start_span(op="explorer.context_engine.get_top_span_ops_for_org_projects"): - span_ops_by_project = get_top_span_ops_for_org_projects(high_volume_projects, start, end) - with sentry_sdk.start_span(op="explorer.context_engine.get_sdk_names_for_org_projects"): - sdk_names_by_project = get_sdk_names_for_org_projects(high_volume_projects, start, end) - - project_data: list[OrgProjectKnowledgeProjectData] = [] - for project in high_volume_projects: - counts = event_counts.get(project.id, ProjectEventCounts()) - project_data.append( - OrgProjectKnowledgeProjectData( - project_id=project.id, - slug=project.slug, - sdk_name=sdk_names_by_project.get(project.id, ""), - error_count=counts.error_count, - transaction_count=counts.transaction_count, - instrumentation=get_instrumentation_types(project), - top_transactions=transactions_by_project.get(project.id, []), - top_span_operations=span_ops_by_project.get(project.id, []), - ) - ) - - payload = OrgProjectKnowledgeIndexRequest(org_id=org_id, projects=project_data) - - viewer_context = SeerViewerContext(organization_id=org_id) - - try: - response = make_org_project_knowledge_index_request( - payload, - timeout=30, - viewer_context=viewer_context, - ) - if response.status >= 400: - raise SeerApiError("Seer request failed", response.status) - except Exception: - logger.exception( - "Failed to call Seer org-project-knowledge endpoint", - extra={"org_id": org_id, "num_projects": len(project_data)}, - ) - raise - - logger.info( - "Successfully called Seer org-project-knowledge endpoint", - extra={"org_id": org_id, "num_projects": len(project_data)}, - ) - - -@instrumented_task( - name="sentry.tasks.context_engine_index.build_service_map", - namespace=seer_tasks, - processing_deadline_duration=10 * 60, # 10 minutes - retry=Retry(times=3, on=(SnubaRPCRateLimitExceeded,), delay=60), -) -def build_service_map(organization_id: int, *args, **kwargs) -> None: - """ - Build service map for a single organization and send to Seer. - - This task: - 1. Checks feature flags - 2. Queries Snuba for service dependencies - 3. Classifies service roles using graph analysis - 4. Sends data to Seer - - Args: - organization_id: Organization ID to build map for - """ - if not options.get("explorer.context_engine_indexing.enable"): - logger.info("explorer.context_engine_indexing.enable flag is disabled") - return - - logger.info( - "Starting service map build", - extra={"org_id": organization_id}, - ) - - try: - organization = Organization.objects.get(id=organization_id) - projects = list( - Project.objects.filter(organization_id=organization_id, status=ObjectStatus.ACTIVE) - ) - - if not projects: - logger.info("No projects found for organization", extra={"org_id": organization_id}) - return - - end = datetime.now(timezone.utc) - start = end - timedelta(hours=24) - - snuba_params = SnubaParams( - start=start, - end=end, - projects=projects, - organization=organization, - ) - - edges = _query_service_dependencies(snuba_params) - nodes = _build_nodes(edges, projects) - - if not nodes: - logger.info("No service map data found", extra={"org_id": organization_id}) - return - - _send_to_seer(organization_id, nodes, edges) - - logger.info( - "Successfully completed service map build", - extra={ - "org_id": organization_id, - "edge_count": len(edges), - "node_count": len(nodes), - }, - ) - - except Organization.DoesNotExist: - logger.error("Organization not found", extra={"org_id": organization_id}) - return - except Exception: - sentry_sdk.capture_exception() - logger.exception( - "Failed to build service map", - extra={"org_id": organization_id}, - ) - raise - - -def get_allowed_org_ids_context_engine_indexing() -> list[int]: - """ - Get the list of allowed organizations for context engine indexing. - - Divides all active orgs into 24 buckets via md5 hash of org ID. Only the bucket matching the current - hour is checked for the seer-explorer-context-engine feature flag, keeping feature check - volume at ~1/24th of total orgs. - """ - with sentry_sdk.start_span( - op="explorer.context_engine.get_allowed_org_ids_context_engine_indexing" - ): - now = datetime.now(UTC) - TOTAL_HOURLY_SLOTS = 24 - - eligible_org_ids: list[int] = [] - - for org in RangeQuerySetWrapper( - Organization.objects.filter(status=ObjectStatus.ACTIVE), - result_value_getter=lambda o: o.id, - ): - # Ordering of these if blocks is very crucial. We want to check the hour first as - # checking the feature flag is an expensive operation and we want to avoid it if possible. - if int(md5_text(str(org.id)).hexdigest(), 16) % TOTAL_HOURLY_SLOTS == now.hour: - with sentry_sdk.start_span(op="explorer.context_engine.has_feature"): - if features.has("organizations:seer-explorer-context-engine", org): - eligible_org_ids.append(org.id) - - return eligible_org_ids - - -@instrumented_task( - name="sentry.tasks.context_engine_index.schedule_context_engine_indexing_tasks", - namespace=seer_tasks, - processing_deadline_duration=30 * 60, -) -def schedule_context_engine_indexing_tasks() -> None: - """ - Schedule context engine indexing tasks for all allowed organizations. - - Dispatches index_org_project_knowledge and build_service_map for each org - with the seer-explorer-context-engine feature flag enabled. - """ - if not options.get("explorer.context_engine_indexing.enable"): - logger.info("explorer.context_engine_indexing.enable flag is disabled") - return - - allowed_org_ids = get_allowed_org_ids_context_engine_indexing() - - dispatched = 0 - for org_id in allowed_org_ids: - try: - index_org_project_knowledge.apply_async(args=[org_id]) - build_service_map.apply_async(args=[org_id]) - dispatched += 1 - except Exception: - logger.exception( - "Failed to dispatch context engine tasks for org", - extra={"org_id": org_id}, - ) - - logger.info( - "Scheduled context engine indexing tasks", - extra={ - "orgs": allowed_org_ids[:10], - "total_org_count": len(allowed_org_ids), - "dispatched": dispatched, - }, - ) - - -@instrumented_task( - name="sentry.tasks.context_engine_index.index_sentry_knowledge", - namespace=seer_tasks, - processing_deadline_duration=30, +# Shim for backwards compatibility with getsentry imports. +# Remove once getsentry is updated to import from sentry.tasks.seer.context_engine_index. +from sentry.tasks.seer.context_engine_index import ( + build_service_map, + get_allowed_org_ids_context_engine_indexing, + index_org_project_knowledge, + index_sentry_knowledge, + schedule_context_engine_indexing_tasks, ) -def index_sentry_knowledge() -> None: - response = make_index_sentry_knowledge_request( - body=ExplorerIndexSentryKnowledgeRequest(replace_existing=True) - ) - - if response.status >= 400: - raise Exception( - f"Seer sentry-knowledge endpoint returned {response.status}: {response.data.decode()}" - ) - logger.info("Successfully called Seer sentry-knowledge endpoint") - return None +__all__ = [ + "build_service_map", + "get_allowed_org_ids_context_engine_indexing", + "index_org_project_knowledge", + "index_sentry_knowledge", + "schedule_context_engine_indexing_tasks", +] diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index aece13cb91cff9..54656f5aaeb061 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -1529,7 +1529,7 @@ def kick_off_seer_automation(job: PostProcessJob) -> None: is_seer_scanner_rate_limited, is_seer_seat_based_tier_enabled, ) - from sentry.tasks.autofix import ( + from sentry.tasks.seer.autofix import ( generate_issue_summary_only, generate_summary_and_run_automation, run_automation_only_task, diff --git a/src/sentry/tasks/seer/__init__.py b/src/sentry/tasks/seer/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py new file mode 100644 index 00000000000000..86107ce8a348ea --- /dev/null +++ b/src/sentry/tasks/seer/autofix.py @@ -0,0 +1,303 @@ +import logging +from datetime import datetime, timedelta + +import sentry_sdk +from django.utils import timezone +from taskbroker_client.retry import Retry +from taskbroker_client.state import current_task + +from sentry import analytics, features +from sentry.analytics.events.autofix_automation_events import AiAutofixAutomationEvent +from sentry.constants import ObjectStatus +from sentry.models.group import Group +from sentry.models.organization import Organization +from sentry.models.project import Project +from sentry.seer.autofix.constants import ( + AutofixAutomationTuningSettings, + AutofixStatus, + SeerAutomationSource, +) +from sentry.seer.autofix.utils import ( + bulk_get_project_preferences, + bulk_set_project_preferences, + bulk_write_preferences_to_sentry_db, + deduplicate_repositories, + get_autofix_repos_from_project_code_mappings, + get_autofix_state, + get_seer_seat_based_tier_cache_key, + resolve_repository_ids, +) +from sentry.seer.models import SeerProjectPreference +from sentry.tasks.base import instrumented_task +from sentry.taskworker.namespaces import ingest_errors_tasks, issues_tasks +from sentry.utils import metrics +from sentry.utils.cache import cache + +logger = logging.getLogger(__name__) + + +def _get_group_or_log(group_id: int, task_name: str) -> Group | None: + """Fetch a Group by ID, returning None and logging a warning if it no longer exists.""" + try: + return Group.objects.get(id=group_id) + except Group.DoesNotExist: + logger.warning("%s.group_not_found", task_name, extra={"group_id": group_id}) + return None + + +@instrumented_task( + name="sentry.tasks.autofix.check_autofix_status", + namespace=issues_tasks, + retry=Retry(times=1), +) +def check_autofix_status(run_id: int, organization_id: int) -> None: + state = get_autofix_state(run_id=run_id, organization_id=organization_id) + + if ( + state + and state.status == AutofixStatus.PROCESSING + and state.updated_at < datetime.now() - timedelta(minutes=5) + ): + # This should log to sentry + logger.error( + "Autofix run has been processing for more than 5 minutes", extra={"run_id": run_id} + ) + + +@instrumented_task( + name="sentry.tasks.autofix.generate_summary_and_run_automation", + namespace=ingest_errors_tasks, + processing_deadline_duration=35, + retry=Retry(times=1), +) +def generate_summary_and_run_automation(group_id: int, **kwargs) -> None: + from sentry.seer.autofix.issue_summary import get_issue_summary + + trigger_path = kwargs.get("trigger_path", "unknown") + sentry_sdk.set_tag("trigger_path", trigger_path) + + group = _get_group_or_log(group_id, "generate_summary_and_run_automation") + if group is None: + return + organization = group.project.organization + + task_state = current_task() + if task_state is None or task_state.attempt == 0: + metrics.incr("sentry.tasks.autofix.generate_summary_and_run_automation", sample_rate=1.0) + analytics.record( + AiAutofixAutomationEvent( + organization_id=organization.id, + project_id=group.project_id, + group_id=group.id, + task_name="generate_summary_and_run_automation", + issue_event_count=group.times_seen, + fixability_score=group.seer_fixability_score, + ) + ) + + get_issue_summary(group=group, source=SeerAutomationSource.POST_PROCESS) + + +@instrumented_task( + name="sentry.tasks.autofix.generate_issue_summary_only", + namespace=ingest_errors_tasks, + processing_deadline_duration=35, + retry=Retry(times=3, delay=3, on=(Exception,)), +) +def generate_issue_summary_only(group_id: int) -> None: + """ + Generate issue summary WITHOUT triggering automation. + Used for triage signals flow when event count < 10 or when summary doesn't exist yet. + """ + from sentry.seer.autofix.issue_summary import ( + get_and_update_group_fixability_score, + get_issue_summary, + ) + + group = _get_group_or_log(group_id, "generate_issue_summary_only") + if group is None: + return + organization = group.project.organization + + task_state = current_task() + if task_state is None or task_state.attempt == 0: + metrics.incr("sentry.tasks.autofix.generate_issue_summary_only", sample_rate=1.0) + analytics.record( + AiAutofixAutomationEvent( + organization_id=organization.id, + project_id=group.project_id, + group_id=group.id, + task_name="generate_issue_summary_only", + issue_event_count=group.times_seen, + fixability_score=group.seer_fixability_score, + ) + ) + + # Generate and cache the summary + get_issue_summary( + group=group, source=SeerAutomationSource.POST_PROCESS, should_run_automation=False + ) + + get_and_update_group_fixability_score(group, force_generate=True) + + +@instrumented_task( + name="sentry.tasks.autofix.run_automation_only_task", + namespace=ingest_errors_tasks, + processing_deadline_duration=35, + retry=Retry(times=1), +) +def run_automation_only_task(group_id: int) -> None: + """ + Run automation directly for a group (assumes summary and fixability already exist). + Used for triage signals flow when event count >= 10 and summary exists. + """ + from django.contrib.auth.models import AnonymousUser + + from sentry.seer.autofix.issue_summary import run_automation + + group = _get_group_or_log(group_id, "run_automation_only_task") + if group is None: + return + organization = group.project.organization + + task_state = current_task() + if task_state is None or task_state.attempt == 0: + metrics.incr("sentry.tasks.autofix.run_automation_only_task", sample_rate=1.0) + analytics.record( + AiAutofixAutomationEvent( + organization_id=organization.id, + project_id=group.project_id, + group_id=group.id, + task_name="run_automation_only", + issue_event_count=group.times_seen, + fixability_score=group.seer_fixability_score, + ) + ) + + event = group.get_latest_event() + + if not event: + logger.warning("run_automation_only_task.no_event_found", extra={"group_id": group_id}) + return + + # Track issue age when running automation + issue_age_days = int((timezone.now() - group.first_seen).total_seconds() / (60 * 60 * 24)) + metrics.distribution( + "seer.automation.issue_age_since_first_seen", issue_age_days, unit="day", sample_rate=1.0 + ) + + run_automation( + group=group, user=AnonymousUser(), event=event, source=SeerAutomationSource.POST_PROCESS + ) + + +@instrumented_task( + name="sentry.tasks.autofix.configure_seer_for_existing_org", + namespace=issues_tasks, + processing_deadline_duration=90, + retry=Retry(times=3), +) +def configure_seer_for_existing_org(organization_id: int) -> None: + """ + Configure Seer settings for a new or existing organization migrating to new Seer pricing. + + Sets: + - Project-level (all projects): seer_scanner_automation=True, autofix_automation_tuning="medium" or "off" + - Seer API (all projects): automated_run_stopping_point="code_changes" or "open_pr" + + Ignores: + - Org-level: enable_seer_coding + """ + + organization = Organization.objects.get(id=organization_id) + + sentry_sdk.set_tag("organization_id", organization.id) + sentry_sdk.set_tag("organization_slug", organization.slug) + + # Set org-level options + organization.update_option( + "sentry:default_autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM + ) + + projects = list( + Project.objects.filter(organization_id=organization_id, status=ObjectStatus.ACTIVE) + ) + project_ids = [p.id for p in projects] + + if len(project_ids) == 0: + return + + # If seer is enabled for an org, every project must have project level settings + for project in projects: + project.update_option("sentry:seer_scanner_automation", True) + autofix_automation_tuning = project.get_option("sentry:autofix_automation_tuning") + if autofix_automation_tuning != AutofixAutomationTuningSettings.OFF: + project.update_option( + "sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM + ) + + preferences_by_id = bulk_get_project_preferences(organization_id, project_ids) + + # Determine which projects need updates + preferences_to_set = [] + projects_by_id = {p.id: p for p in projects} + for project_id in project_ids: + existing_pref = preferences_by_id.get(str(project_id)) + if not existing_pref: + # No existing preferences, get repositories from code mappings + repositories = get_autofix_repos_from_project_code_mappings(projects_by_id[project_id]) + else: + # Skip projects that already have an acceptable stopping point configured + if existing_pref.get("automated_run_stopping_point") in ("open_pr", "code_changes"): + continue + repositories = existing_pref.get("repositories") or [] + + repositories = deduplicate_repositories(repositories) + + # Preserve existing repositories and automation_handoff, only update the stopping point + preferences_to_set.append( + { + "organization_id": organization_id, + "project_id": project_id, + "repositories": repositories or [], + "automated_run_stopping_point": "code_changes", + "automation_handoff": ( + existing_pref.get("automation_handoff") if existing_pref else None + ), + } + ) + + if len(preferences_to_set) > 0: + bulk_set_project_preferences(organization_id, preferences_to_set) + + if features.has("organizations:seer-project-settings-dual-write", organization): + try: + validated_preferences = [ + SeerProjectPreference.validate(pref) for pref in preferences_to_set + ] + # Seer API responses don't include repository_id. + # Resolve before dual-writing so repos aren't skipped. + # This will not be necessary once we start keying by repo ID. + resolved_preferences = resolve_repository_ids( + organization_id, validated_preferences + ) + bulk_write_preferences_to_sentry_db(projects, resolved_preferences) + except Exception: + logger.exception( + "seer.write_preferences.failed", extra={"organization_id": organization_id} + ) + + # Invalidate existing cache entry and set cache to True to prevent race conditions where another + # request re-caches False before the billing flag has fully propagated + cache.set(get_seer_seat_based_tier_cache_key(organization_id), True, timeout=60 * 5) + + logger.info( + "Task: configure_seer_for_existing_org completed", + extra={ + "org_id": organization.id, + "org_slug": organization.slug, + "projects_configured": len(project_ids), + "preferences_set_via_api": len(preferences_to_set), + }, + ) diff --git a/src/sentry/tasks/seer.py b/src/sentry/tasks/seer/cleanup.py similarity index 100% rename from src/sentry/tasks/seer.py rename to src/sentry/tasks/seer/cleanup.py diff --git a/src/sentry/tasks/seer/context_engine_index.py b/src/sentry/tasks/seer/context_engine_index.py new file mode 100644 index 00000000000000..f47c37f3fd5ac3 --- /dev/null +++ b/src/sentry/tasks/seer/context_engine_index.py @@ -0,0 +1,298 @@ +from __future__ import annotations + +import logging +from datetime import UTC, datetime, timedelta, timezone + +import sentry_sdk +from taskbroker_client.retry import Retry + +from sentry import features, options +from sentry.constants import ObjectStatus +from sentry.models.organization import Organization +from sentry.models.project import Project +from sentry.search.events.types import SnubaParams +from sentry.seer.explorer.context_engine_utils import ( + EVENT_COUNT_LOOKBACK_DAYS, + ProjectEventCounts, + get_event_counts_for_org_projects, + get_instrumentation_types, + get_sdk_names_for_org_projects, + get_top_span_ops_for_org_projects, + get_top_transactions_for_org_projects, +) +from sentry.seer.explorer.explorer_service_map_utils import ( + _build_nodes, + _query_service_dependencies, + _send_to_seer, +) +from sentry.seer.models import SeerApiError +from sentry.seer.signed_seer_api import ( + ExplorerIndexSentryKnowledgeRequest, + OrgProjectKnowledgeIndexRequest, + OrgProjectKnowledgeProjectData, + SeerViewerContext, + make_index_sentry_knowledge_request, + make_org_project_knowledge_index_request, +) +from sentry.tasks.base import instrumented_task +from sentry.taskworker.namespaces import seer_tasks +from sentry.utils.hashlib import md5_text +from sentry.utils.query import RangeQuerySetWrapper +from sentry.utils.snuba_rpc import SnubaRPCRateLimitExceeded + +logger = logging.getLogger(__name__) + + +@instrumented_task( + name="sentry.tasks.context_engine_index.index_org_project_knowledge", + namespace=seer_tasks, + processing_deadline_duration=10 * 60, +) +def index_org_project_knowledge(org_id: int) -> None: + """ + For a given org, list active projects, assemble project metadata and call + the Seer endpoint to generate LLM summaries and embeddings. + """ + if not options.get("explorer.context_engine_indexing.enable"): + logger.info("explorer.context_engine_indexing.enable flag is disabled") + return + + projects = list( + Project.objects.filter(organization_id=org_id, status=ObjectStatus.ACTIVE).select_related( + "organization" + ) + ) + if not projects: + logger.warning( + "No projects found for index_org_project_knowledge", + extra={"org_id": org_id}, + ) + return + + end = datetime.now(UTC) + start = end - timedelta(days=EVENT_COUNT_LOOKBACK_DAYS) + + project_ids = [p.id for p in projects] + event_counts = get_event_counts_for_org_projects(org_id, project_ids, start, end) + high_volume_projects = [p for p in projects if p.id in event_counts] + if not high_volume_projects: + logger.info( + "No high-volume projects found for index_org_project_knowledge", + extra={"org_id": org_id, "num_projects": len(projects)}, + ) + return + + with sentry_sdk.start_span(op="explorer.context_engine.get_top_transactions_for_org_projects"): + transactions_by_project = get_top_transactions_for_org_projects( + high_volume_projects, start, end + ) + with sentry_sdk.start_span(op="explorer.context_engine.get_top_span_ops_for_org_projects"): + span_ops_by_project = get_top_span_ops_for_org_projects(high_volume_projects, start, end) + with sentry_sdk.start_span(op="explorer.context_engine.get_sdk_names_for_org_projects"): + sdk_names_by_project = get_sdk_names_for_org_projects(high_volume_projects, start, end) + + project_data: list[OrgProjectKnowledgeProjectData] = [] + for project in high_volume_projects: + counts = event_counts.get(project.id, ProjectEventCounts()) + project_data.append( + OrgProjectKnowledgeProjectData( + project_id=project.id, + slug=project.slug, + sdk_name=sdk_names_by_project.get(project.id, ""), + error_count=counts.error_count, + transaction_count=counts.transaction_count, + instrumentation=get_instrumentation_types(project), + top_transactions=transactions_by_project.get(project.id, []), + top_span_operations=span_ops_by_project.get(project.id, []), + ) + ) + + payload = OrgProjectKnowledgeIndexRequest(org_id=org_id, projects=project_data) + + viewer_context = SeerViewerContext(organization_id=org_id) + + try: + response = make_org_project_knowledge_index_request( + payload, + timeout=30, + viewer_context=viewer_context, + ) + if response.status >= 400: + raise SeerApiError("Seer request failed", response.status) + except Exception: + logger.exception( + "Failed to call Seer org-project-knowledge endpoint", + extra={"org_id": org_id, "num_projects": len(project_data)}, + ) + raise + + logger.info( + "Successfully called Seer org-project-knowledge endpoint", + extra={"org_id": org_id, "num_projects": len(project_data)}, + ) + + +@instrumented_task( + name="sentry.tasks.context_engine_index.build_service_map", + namespace=seer_tasks, + processing_deadline_duration=10 * 60, # 10 minutes + retry=Retry(times=3, on=(SnubaRPCRateLimitExceeded,), delay=60), +) +def build_service_map(organization_id: int, *args, **kwargs) -> None: + """ + Build service map for a single organization and send to Seer. + + This task: + 1. Checks feature flags + 2. Queries Snuba for service dependencies + 3. Classifies service roles using graph analysis + 4. Sends data to Seer + + Args: + organization_id: Organization ID to build map for + """ + if not options.get("explorer.context_engine_indexing.enable"): + logger.info("explorer.context_engine_indexing.enable flag is disabled") + return + + logger.info( + "Starting service map build", + extra={"org_id": organization_id}, + ) + + try: + organization = Organization.objects.get(id=organization_id) + projects = list( + Project.objects.filter(organization_id=organization_id, status=ObjectStatus.ACTIVE) + ) + + if not projects: + logger.info("No projects found for organization", extra={"org_id": organization_id}) + return + + end = datetime.now(timezone.utc) + start = end - timedelta(hours=24) + + snuba_params = SnubaParams( + start=start, + end=end, + projects=projects, + organization=organization, + ) + + edges = _query_service_dependencies(snuba_params) + nodes = _build_nodes(edges, projects) + + if not nodes: + logger.info("No service map data found", extra={"org_id": organization_id}) + return + + _send_to_seer(organization_id, nodes, edges) + + logger.info( + "Successfully completed service map build", + extra={ + "org_id": organization_id, + "edge_count": len(edges), + "node_count": len(nodes), + }, + ) + + except Organization.DoesNotExist: + logger.error("Organization not found", extra={"org_id": organization_id}) + return + except Exception: + sentry_sdk.capture_exception() + logger.exception( + "Failed to build service map", + extra={"org_id": organization_id}, + ) + raise + + +def get_allowed_org_ids_context_engine_indexing() -> list[int]: + """ + Get the list of allowed organizations for context engine indexing. + + Divides all active orgs into 24 buckets via md5 hash of org ID. Only the bucket matching the current + hour is checked for the seer-explorer-context-engine feature flag, keeping feature check + volume at ~1/24th of total orgs. + """ + with sentry_sdk.start_span( + op="explorer.context_engine.get_allowed_org_ids_context_engine_indexing" + ): + now = datetime.now(UTC) + TOTAL_HOURLY_SLOTS = 24 + + eligible_org_ids: list[int] = [] + + for org in RangeQuerySetWrapper( + Organization.objects.filter(status=ObjectStatus.ACTIVE), + result_value_getter=lambda o: o.id, + ): + # Ordering of these if blocks is very crucial. We want to check the hour first as + # checking the feature flag is an expensive operation and we want to avoid it if possible. + if int(md5_text(str(org.id)).hexdigest(), 16) % TOTAL_HOURLY_SLOTS == now.hour: + if features.has("organizations:seer-explorer-context-engine", org): + eligible_org_ids.append(org.id) + + return eligible_org_ids + + +@instrumented_task( + name="sentry.tasks.context_engine_index.schedule_context_engine_indexing_tasks", + namespace=seer_tasks, + processing_deadline_duration=30 * 60, +) +def schedule_context_engine_indexing_tasks() -> None: + """ + Schedule context engine indexing tasks for all allowed organizations. + + Dispatches index_org_project_knowledge and build_service_map for each org + with the seer-explorer-context-engine feature flag enabled. + """ + if not options.get("explorer.context_engine_indexing.enable"): + logger.info("explorer.context_engine_indexing.enable flag is disabled") + return + + allowed_org_ids = get_allowed_org_ids_context_engine_indexing() + + dispatched = 0 + for org_id in allowed_org_ids: + try: + index_org_project_knowledge.apply_async(args=[org_id]) + build_service_map.apply_async(args=[org_id]) + dispatched += 1 + except Exception: + logger.exception( + "Failed to dispatch context engine tasks for org", + extra={"org_id": org_id}, + ) + + logger.info( + "Scheduled context engine indexing tasks", + extra={ + "orgs": allowed_org_ids[:10], + "total_org_count": len(allowed_org_ids), + "dispatched": dispatched, + }, + ) + + +@instrumented_task( + name="sentry.tasks.context_engine_index.index_sentry_knowledge", + namespace=seer_tasks, + processing_deadline_duration=30, +) +def index_sentry_knowledge() -> None: + response = make_index_sentry_knowledge_request( + body=ExplorerIndexSentryKnowledgeRequest(replace_existing=True) + ) + + if response.status >= 400: + raise Exception( + f"Seer sentry-knowledge endpoint returned {response.status}: {response.data.decode()}" + ) + + logger.info("Successfully called Seer sentry-knowledge endpoint") + return None diff --git a/src/sentry/tasks/delete_seer_grouping_records.py b/src/sentry/tasks/seer/delete_seer_grouping_records.py similarity index 100% rename from src/sentry/tasks/delete_seer_grouping_records.py rename to src/sentry/tasks/seer/delete_seer_grouping_records.py diff --git a/src/sentry/tasks/seer_explorer_index.py b/src/sentry/tasks/seer/explorer_index.py similarity index 100% rename from src/sentry/tasks/seer_explorer_index.py rename to src/sentry/tasks/seer/explorer_index.py diff --git a/src/sentry/testutils/cell.py b/src/sentry/testutils/cell.py index 4130b6d014a426..40a8cc8e17d9a4 100644 --- a/src/sentry/testutils/cell.py +++ b/src/sentry/testutils/cell.py @@ -18,7 +18,13 @@ def __init__(self, cells: Collection[Cell]) -> None: def _apply_cells(self, cells: Collection[Cell]) -> None: localities = frozenset( - Locality(name=c.name, cells=frozenset([c.name]), category=c.category, visible=c.visible) + Locality( + name=c.name, + cells=frozenset([c.name]), + category=c.category, + visible=c.visible, + new_org_cell=c.name, + ) for c in cells ) self._cells = frozenset(cells) diff --git a/src/sentry/testutils/helpers/serializer_parity.py b/src/sentry/testutils/helpers/serializer_parity.py new file mode 100644 index 00000000000000..ad5074a665778c --- /dev/null +++ b/src/sentry/testutils/helpers/serializer_parity.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass, field +from typing import Any + + +def assert_serializer_parity( + *, + old: Mapping[str, Any], + new: Mapping[str, Any], + known_differences: set[str] | None = None, +) -> None: + """Assert that two serializer responses are equal, modulo known differences. + + ``known_differences`` is a set of dot-separated field paths to exclude: + + * ``"field"`` — skip ``field`` at the top level. + * ``"parent.field"`` — skip ``field`` inside ``parent``. If ``parent`` is a + list of dicts, the exclusion applies to every element. Callers must sort + any list fields to the same order before calling if order should be ignored. + + Raises if any listed known difference is not actually different — unnecessary + entries should be removed. + + Examples:: + + assert_serializer_parity(old=old, new=new) + + assert_serializer_parity( + old=old, + new=new, + known_differences={"resolveThreshold", "triggers.resolveThreshold"}, + ) + """ + known_diffs = frozenset(known_differences or ()) + checker = _ParityChecker() + checker.compare(old, new, known_diffs) + + assert not checker.mismatches, "Serializer differences:\n" + "\n".join(checker.mismatches) + + # known_diffs entries that were never confirmed as real differences are unnecessary. + unnecessary = known_diffs - checker.confirmed + assert not unnecessary, ( + "Unnecessary known_differences (no actual difference found):\n" + + "\n".join(sorted(unnecessary)) + ) + + +def _qualify(prefix: str, name: str) -> str: + return f"{prefix}.{name}" if prefix else name + + +@dataclass +class _ParityChecker: + mismatches: list[str] = field(default_factory=list) + + # known_diffs entries confirmed to be actual differences. + confirmed: set[str] = field(default_factory=set) + + def _nested_diffs(self, known_diffs: frozenset[str], key: str) -> frozenset[str]: + prefix = key + "." + return frozenset(e[len(prefix) :] for e in known_diffs if e.startswith(prefix)) + + def compare( + self, + old: Mapping[str, Any], + new: Mapping[str, Any], + known_diffs: frozenset[str], + path: str = "", + kd_path: str = "", + ) -> None: + for key in set(list(old.keys()) + list(new.keys())): + if key in known_diffs: + full_kd_key = _qualify(kd_path, key) + if key not in new or key not in old or old[key] != new[key]: + self.confirmed.add(full_kd_key) + continue + + full_path = _qualify(path, key) + + if key not in new: + self.mismatches.append(f"Missing from new: {full_path}") + continue + if key not in old: + self.mismatches.append(f"Extra in new: {full_path}") + continue + + old_val = old[key] + new_val = new[key] + nested = self._nested_diffs(known_diffs, key) + + if nested: + child_kd_path = _qualify(kd_path, key) + if isinstance(old_val, list) and isinstance(new_val, list): + if len(old_val) != len(new_val): + self.mismatches.append( + f"{full_path} count: old={len(old_val)}, new={len(new_val)}" + ) + for i, (old_item, new_item) in enumerate(zip(old_val, new_val)): + item_path = f"{full_path}[{i}]" + if isinstance(old_item, Mapping) and isinstance(new_item, Mapping): + self.compare(old_item, new_item, nested, item_path, child_kd_path) + elif old_item != new_item: + self.mismatches.append( + f"{item_path}: old={old_item!r}, new={new_item!r}" + ) + elif isinstance(old_val, Mapping) and isinstance(new_val, Mapping): + self.compare(old_val, new_val, nested, full_path, child_kd_path) + elif old_val != new_val: + self.mismatches.append(f"{full_path}: old={old_val!r}, new={new_val!r}") + elif old_val != new_val: + self.mismatches.append(f"{full_path}: old={old_val!r}, new={new_val!r}") diff --git a/src/sentry/types/cell.py b/src/sentry/types/cell.py index f6b668aa5d2eb4..25768901a9ac4d 100644 --- a/src/sentry/types/cell.py +++ b/src/sentry/types/cell.py @@ -36,6 +36,9 @@ class Locality: category: RegionCategory + new_org_cell: str + """The cell within this locality where new organizations are provisioned.""" + visible: bool = True """Whether the locality is visible in API responses.""" @@ -209,6 +212,13 @@ def validate_all(self) -> None: f"cell-only={defined_cells - assigned_cells!r}" ) + for loc in self.localities: + if loc.new_org_cell not in loc.cells: + raise CellConfigurationError( + f"Locality {loc.name!r} has new_org_cell={loc.new_org_cell!r} " + f"which is not in its cells={set(loc.cells)!r}" + ) + def _parse_raw_config(cell_config: list[CellConfig]) -> Iterable[Cell]: for config_value in cell_config: @@ -253,6 +263,7 @@ def _parse_locality_config( name=config_value["name"], category=RegionCategory(config_value["category"]), cells=frozenset(config_value["cells"]), + new_org_cell=config_value["new_org_cell"], visible=bool(config_value.get("visible", True)), ) @@ -276,6 +287,7 @@ def load_from_config( name=cell.name, category=cell.category, cells=frozenset([cell.name]), + new_org_cell=cell.name, visible=cell.visible, ) ) diff --git a/src/sentry/utils/concurrent.py b/src/sentry/utils/concurrent.py index e6c63e0ef19ab6..033fc1c166b624 100644 --- a/src/sentry/utils/concurrent.py +++ b/src/sentry/utils/concurrent.py @@ -1,10 +1,12 @@ from __future__ import annotations +import contextvars import functools import logging import threading from collections.abc import Callable from concurrent.futures import Future, InvalidStateError +from concurrent.futures import ThreadPoolExecutor as _ThreadPoolExecutor # noqa: S016 from concurrent.futures._base import FINISHED, RUNNING from contextlib import contextmanager from queue import Full, PriorityQueue @@ -259,6 +261,20 @@ def submit[T]( return future +class ContextPropagatingThreadPoolExecutor(_ThreadPoolExecutor): + """A ThreadPoolExecutor that automatically copies the caller's + ``contextvars.Context`` into each worker invocation. + + This ensures Sentry SDK scopes (isolation_scope, current_scope), + OpenTelemetry trace context, and any other context variables are + available in worker threads without manual propagation. + """ + + def submit(self, fn, /, *args, **kwargs): + ctx = contextvars.copy_context() + return super().submit(ctx.run, fn, *args, **kwargs) + + class FutureSet: """\ Coordinates a set of ``Future`` objects (either from diff --git a/static/app/actionCreators/modal.tsx b/static/app/actionCreators/modal.tsx index dbd04aef8f47fe..f8637acebbe97f 100644 --- a/static/app/actionCreators/modal.tsx +++ b/static/app/actionCreators/modal.tsx @@ -3,7 +3,6 @@ import type {CreateReleaseIntegrationModalOptions} from 'sentry/components/modal import type {DashboardWidgetQuerySelectorModalOptions} from 'sentry/components/modals/dashboardWidgetQuerySelectorModal'; import type {DataWidgetViewerModalOptions} from 'sentry/components/modals/dataWidgetViewerModal'; import type {SaveQueryModalProps} from 'sentry/components/modals/explore/saveQueryModal'; -import type {GenerateDashboardFromSeerModalProps} from 'sentry/components/modals/generateDashboardFromSeerModal'; import type {ImportDashboardFromFileModalProps} from 'sentry/components/modals/importDashboardFromFileModal'; import type {InsightChartModalOptions} from 'sentry/components/modals/insightChartModal'; import type {InviteRow} from 'sentry/components/modals/inviteMembersModal/types'; @@ -274,17 +273,6 @@ export async function openImportDashboardFromFileModal( }); } -export async function openGenerateDashboardFromSeerModal( - options: GenerateDashboardFromSeerModalProps -) { - const {default: Modal} = - await import('sentry/components/modals/generateDashboardFromSeerModal'); - - openModal(deps => , { - closeEvents: 'escape-key', - }); -} - export async function openReprocessEventModal({ onClose, ...options diff --git a/static/app/components/autoComplete.spec.tsx b/static/app/components/autoComplete.spec.tsx index d16ce81f457ea3..76e28fec795364 100644 --- a/static/app/components/autoComplete.spec.tsx +++ b/static/app/components/autoComplete.spec.tsx @@ -3,7 +3,7 @@ import {useEffect} from 'react'; import {act, fireEvent, render, screen, waitFor} from 'sentry-test/reactTestingLibrary'; import type {AutoCompleteProps} from 'sentry/components/autoComplete'; -import AutoComplete from 'sentry/components/autoComplete'; +import {AutoComplete} from 'sentry/components/autoComplete'; const items = [ { diff --git a/static/app/components/autoComplete.tsx b/static/app/components/autoComplete.tsx index 8485f3e87b5875..aa072ec9eae6f0 100644 --- a/static/app/components/autoComplete.tsx +++ b/static/app/components/autoComplete.tsx @@ -148,7 +148,10 @@ export interface AutoCompleteProps extends DefaultProps { resetInputOnClose?: boolean; } -class AutoComplete extends Component, State> { +export class AutoComplete extends Component< + AutoCompleteProps, + State +> { static defaultProps = defaultProps; state: State = this.getInitialState(); @@ -536,5 +539,3 @@ class AutoComplete extends Component, State ); } } - -export default AutoComplete; diff --git a/static/app/components/charts/eventsAreaChart.spec.tsx b/static/app/components/charts/eventsAreaChart.spec.tsx index 9afd7d0672bc68..63bb3572062396 100644 --- a/static/app/components/charts/eventsAreaChart.spec.tsx +++ b/static/app/components/charts/eventsAreaChart.spec.tsx @@ -3,7 +3,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg'; import {render, screen} from 'sentry-test/reactTestingLibrary'; import {BaseChart} from 'sentry/components/charts/baseChart'; -import EventsChart from 'sentry/components/charts/eventsChart'; +import {EventsChart} from 'sentry/components/charts/eventsChart'; jest.mock('sentry/components/charts/baseChart', () => ({ BaseChart: jest.fn().mockImplementation(() =>
), diff --git a/static/app/components/charts/eventsChart.tsx b/static/app/components/charts/eventsChart.tsx index b8e380d9bfcfa1..32d09cce811227 100644 --- a/static/app/components/charts/eventsChart.tsx +++ b/static/app/components/charts/eventsChart.tsx @@ -22,7 +22,7 @@ import {ErrorPanel} from 'sentry/components/charts/errorPanel'; import type {LineChartProps} from 'sentry/components/charts/lineChart'; import {LineChart} from 'sentry/components/charts/lineChart'; import ReleaseSeries from 'sentry/components/charts/releaseSeries'; -import TransitionChart from 'sentry/components/charts/transitionChart'; +import {TransitionChart} from 'sentry/components/charts/transitionChart'; import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask'; import {getInterval, RELEASE_LINES_THRESHOLD} from 'sentry/components/charts/utils'; import {IconWarning} from 'sentry/icons'; @@ -48,7 +48,7 @@ import type {DiscoverDatasets} from 'sentry/utils/discover/types'; import {decodeList, decodeScalar} from 'sentry/utils/queryString'; import {ellipsize} from 'sentry/utils/string/ellipsize'; -import EventsRequest from './eventsRequest'; +import {EventsRequest} from './eventsRequest'; type ChartComponent = | React.ComponentType @@ -488,7 +488,7 @@ type ChartDataProps = { topEvents?: number; }; -class EventsChart extends Component { +export class EventsChart extends Component { isStacked() { const {topEvents, yAxis} = this.props; return ( @@ -691,5 +691,3 @@ class EventsChart extends Component { ); } } - -export default EventsChart; diff --git a/static/app/components/charts/eventsRequest.spec.tsx b/static/app/components/charts/eventsRequest.spec.tsx index 45069888c5ea3c..9d48a4d0a2aa47 100644 --- a/static/app/components/charts/eventsRequest.spec.tsx +++ b/static/app/components/charts/eventsRequest.spec.tsx @@ -4,7 +4,7 @@ import {render, waitFor} from 'sentry-test/reactTestingLibrary'; import {doEventsRequest} from 'sentry/actionCreators/events'; import type {EventsRequestProps} from 'sentry/components/charts/eventsRequest'; -import EventsRequest from 'sentry/components/charts/eventsRequest'; +import {EventsRequest} from 'sentry/components/charts/eventsRequest'; const COUNT_OBJ = { count: 123, diff --git a/static/app/components/charts/eventsRequest.tsx b/static/app/components/charts/eventsRequest.tsx index f468558b716543..3ac812d9a48105 100644 --- a/static/app/components/charts/eventsRequest.tsx +++ b/static/app/components/charts/eventsRequest.tsx @@ -263,7 +263,7 @@ const propNamesToIgnore = [ const omitIgnoredProps = (props: EventsRequestProps) => omitBy(props, (_value, key) => propNamesToIgnore.includes(key)); -class EventsRequest extends PureComponent { +export class EventsRequest extends PureComponent { static defaultProps: DefaultProps = { period: undefined, start: null, @@ -658,7 +658,6 @@ class EventsRequest extends PureComponent { diff --git a/static/app/components/charts/intervalSelector.tsx b/static/app/components/charts/intervalSelector.tsx index a69f195fc54dad..2a5d803ecf4550 100644 --- a/static/app/components/charts/intervalSelector.tsx +++ b/static/app/components/charts/intervalSelector.tsx @@ -9,7 +9,7 @@ import { makeItem, } from 'sentry/components/timeRangeSelector/utils'; import {t, tn} from 'sentry/locale'; -import type EventView from 'sentry/utils/discover/eventView'; +import type {EventView} from 'sentry/utils/discover/eventView'; import {INTERVAL_DISPLAY_MODES} from 'sentry/utils/discover/types'; import {parsePeriodToHours} from 'sentry/utils/duration/parsePeriodToHours'; diff --git a/static/app/components/charts/onDemandMetricRequest.tsx b/static/app/components/charts/onDemandMetricRequest.tsx index 4e1465dc0d516c..66a6faa4038050 100644 --- a/static/app/components/charts/onDemandMetricRequest.tsx +++ b/static/app/components/charts/onDemandMetricRequest.tsx @@ -1,6 +1,6 @@ import {doEventsRequest} from 'sentry/actionCreators/events'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; -import EventsRequest from 'sentry/components/charts/eventsRequest'; +import {EventsRequest} from 'sentry/components/charts/eventsRequest'; import {t} from 'sentry/locale'; import type {EventsStats, MultiSeriesEventsStats} from 'sentry/types/organization'; diff --git a/static/app/components/charts/sessionsRequest.tsx b/static/app/components/charts/sessionsRequest.tsx index f4beb453138f68..7a55ec21659257 100644 --- a/static/app/components/charts/sessionsRequest.tsx +++ b/static/app/components/charts/sessionsRequest.tsx @@ -46,7 +46,7 @@ type State = { response: SessionApiResponse | null; }; -class SessionsRequest extends Component { +export class SessionsRequest extends Component { state: State = { reloading: false, errored: false, @@ -153,5 +153,3 @@ class SessionsRequest extends Component { }); } } - -export default SessionsRequest; diff --git a/static/app/components/charts/transitionChart.tsx b/static/app/components/charts/transitionChart.tsx index 3223fc359e2f4c..5a31f2f27c0d3c 100644 --- a/static/app/components/charts/transitionChart.tsx +++ b/static/app/components/charts/transitionChart.tsx @@ -18,7 +18,7 @@ type State = { prevReloading: boolean; }; -class TransitionChart extends Component { +export class TransitionChart extends Component { static defaultProps = defaultProps; state: State = { @@ -99,5 +99,3 @@ class TransitionChart extends Component { return {this.props.children}; } } - -export default TransitionChart; diff --git a/static/app/components/core/useScrollLock.tsx b/static/app/components/core/useScrollLock.tsx index 7f22648472a592..8c57da64a64c00 100644 --- a/static/app/components/core/useScrollLock.tsx +++ b/static/app/components/core/useScrollLock.tsx @@ -112,6 +112,7 @@ export function useScrollLock(container: HTMLElement) { return { acquire: () => lock.acquire(id), release: () => lock.release(id), + held: () => lock.held(), }; }, [lock, id]); } diff --git a/static/app/components/createAlertButton.spec.tsx b/static/app/components/createAlertButton.spec.tsx index 4b81088802600b..1ca0a78e05c538 100644 --- a/static/app/components/createAlertButton.spec.tsx +++ b/static/app/components/createAlertButton.spec.tsx @@ -8,7 +8,7 @@ import { CreateAlertFromViewButton, } from 'sentry/components/createAlertButton'; import {ProjectsStore} from 'sentry/stores/projectsStore'; -import EventView from 'sentry/utils/discover/eventView'; +import {EventView} from 'sentry/utils/discover/eventView'; import {DEFAULT_EVENT_VIEW} from 'sentry/views/discover/results/data'; const onClickMock = jest.fn(); diff --git a/static/app/components/createAlertButton.tsx b/static/app/components/createAlertButton.tsx index b2b58291c9c752..55d93f8c121539 100644 --- a/static/app/components/createAlertButton.tsx +++ b/static/app/components/createAlertButton.tsx @@ -12,7 +12,7 @@ import {t, tct} from 'sentry/locale'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {isDemoModeActive} from 'sentry/utils/demoMode'; -import type EventView from 'sentry/utils/discover/eventView'; +import type {EventView} from 'sentry/utils/discover/eventView'; import {decodeScalar} from 'sentry/utils/queryString'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; diff --git a/static/app/components/deprecatedAsyncComponent.spec.tsx b/static/app/components/deprecatedAsyncComponent.spec.tsx index 23d2f862dba009..2a580bccf81675 100644 --- a/static/app/components/deprecatedAsyncComponent.spec.tsx +++ b/static/app/components/deprecatedAsyncComponent.spec.tsx @@ -1,6 +1,6 @@ import {act, render, screen} from 'sentry-test/reactTestingLibrary'; -import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent'; +import {DeprecatedAsyncComponent} from 'sentry/components/deprecatedAsyncComponent'; describe('DeprecatedAsyncComponent', () => { class TestAsyncComponent extends DeprecatedAsyncComponent { diff --git a/static/app/components/deprecatedAsyncComponent.tsx b/static/app/components/deprecatedAsyncComponent.tsx index 1fce0ee920305a..dc86fbbe1f7448 100644 --- a/static/app/components/deprecatedAsyncComponent.tsx +++ b/static/app/components/deprecatedAsyncComponent.tsx @@ -55,7 +55,7 @@ function wrapErrorHandling( * * [1]: https://develop.sentry.dev/frontend/network-requests/ */ -class DeprecatedAsyncComponent< +export class DeprecatedAsyncComponent< P extends AsyncComponentProps = AsyncComponentProps, S extends AsyncComponentState = AsyncComponentState, > extends Component { @@ -389,5 +389,3 @@ class DeprecatedAsyncComponent< return this.renderComponent(); } } - -export default DeprecatedAsyncComponent; diff --git a/static/app/components/deprecatedforms/booleanField.spec.tsx b/static/app/components/deprecatedforms/booleanField.spec.tsx index 05478d7edef641..92f02921120071 100644 --- a/static/app/components/deprecatedforms/booleanField.spec.tsx +++ b/static/app/components/deprecatedforms/booleanField.spec.tsx @@ -1,7 +1,7 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import BooleanField from 'sentry/components/deprecatedforms/booleanField'; -import Form from 'sentry/components/deprecatedforms/form'; +import {Form} from 'sentry/components/deprecatedforms/form'; describe('BooleanField', () => { it('renders without form context', () => { diff --git a/static/app/components/deprecatedforms/booleanField.tsx b/static/app/components/deprecatedforms/booleanField.tsx index e9faf207257e13..05bda70075b576 100644 --- a/static/app/components/deprecatedforms/booleanField.tsx +++ b/static/app/components/deprecatedforms/booleanField.tsx @@ -1,7 +1,7 @@ import {Checkbox} from '@sentry/scraps/checkbox'; import {Tooltip} from '@sentry/scraps/tooltip'; -import InputField from 'sentry/components/deprecatedforms/inputField'; +import {InputField} from 'sentry/components/deprecatedforms/inputField'; import {withFormContext} from 'sentry/components/deprecatedforms/withFormContext'; import {IconQuestion} from 'sentry/icons'; import {defined} from 'sentry/utils'; diff --git a/static/app/components/deprecatedforms/dateTimeField.tsx b/static/app/components/deprecatedforms/dateTimeField.tsx index 7745c3956fb82a..0a5357c5cff011 100644 --- a/static/app/components/deprecatedforms/dateTimeField.tsx +++ b/static/app/components/deprecatedforms/dateTimeField.tsx @@ -1,4 +1,4 @@ -import InputField from 'sentry/components/deprecatedforms/inputField'; +import {InputField} from 'sentry/components/deprecatedforms/inputField'; /** * @deprecated Do not use this diff --git a/static/app/components/deprecatedforms/emailField.spec.tsx b/static/app/components/deprecatedforms/emailField.spec.tsx index ba74cda991fa52..3144fd69a9ad1a 100644 --- a/static/app/components/deprecatedforms/emailField.spec.tsx +++ b/static/app/components/deprecatedforms/emailField.spec.tsx @@ -1,7 +1,7 @@ import {render} from 'sentry-test/reactTestingLibrary'; import EmailField from 'sentry/components/deprecatedforms/emailField'; -import Form from 'sentry/components/deprecatedforms/form'; +import {Form} from 'sentry/components/deprecatedforms/form'; describe('EmailField', () => { describe('render()', () => { diff --git a/static/app/components/deprecatedforms/emailField.tsx b/static/app/components/deprecatedforms/emailField.tsx index 9f76766c16dc1f..6b9b0561298ddf 100644 --- a/static/app/components/deprecatedforms/emailField.tsx +++ b/static/app/components/deprecatedforms/emailField.tsx @@ -1,4 +1,4 @@ -import InputField from 'sentry/components/deprecatedforms/inputField'; +import {InputField} from 'sentry/components/deprecatedforms/inputField'; import {withFormContext} from 'sentry/components/deprecatedforms/withFormContext'; // XXX: This is ONLY used in GenericField. If we can delete that this can go. diff --git a/static/app/components/deprecatedforms/form.spec.tsx b/static/app/components/deprecatedforms/form.spec.tsx index 5f52d29f1639f2..be58ca853c7921 100644 --- a/static/app/components/deprecatedforms/form.spec.tsx +++ b/static/app/components/deprecatedforms/form.spec.tsx @@ -1,6 +1,6 @@ import {render} from 'sentry-test/reactTestingLibrary'; -import Form from 'sentry/components/deprecatedforms/form'; +import {Form} from 'sentry/components/deprecatedforms/form'; describe('Form', () => { describe('render()', () => { diff --git a/static/app/components/deprecatedforms/form.tsx b/static/app/components/deprecatedforms/form.tsx index 1bbbbe4603d895..81b02610bad6ed 100644 --- a/static/app/components/deprecatedforms/form.tsx +++ b/static/app/components/deprecatedforms/form.tsx @@ -6,7 +6,7 @@ import {Alert} from '@sentry/scraps/alert'; import {Button} from '@sentry/scraps/button'; import {FormContext} from 'sentry/components/deprecatedforms/formContext'; -import FormState from 'sentry/components/forms/state'; +import {FormState} from 'sentry/components/forms/state'; import {t} from 'sentry/locale'; type FormProps = { @@ -42,7 +42,7 @@ type FormClassState = { state: FormState; }; -class Form< +export class Form< Props extends FormProps = FormProps, State extends FormClassState = FormClassState, > extends Component { @@ -191,5 +191,3 @@ class Form< // Note: this is so we can use this as a selector for SelectField // We need to keep `Form` as a React Component because ApiForm extends it :/ export const StyledForm = styled('form')``; - -export default Form; diff --git a/static/app/components/deprecatedforms/genericField.spec.tsx b/static/app/components/deprecatedforms/genericField.spec.tsx index e161e11ccfd604..88912dfd6ab505 100644 --- a/static/app/components/deprecatedforms/genericField.spec.tsx +++ b/static/app/components/deprecatedforms/genericField.spec.tsx @@ -1,7 +1,7 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; import {GenericField} from 'sentry/components/deprecatedforms/genericField'; -import FormState from 'sentry/components/forms/state'; +import {FormState} from 'sentry/components/forms/state'; describe('GenericField', () => { it('renders text as TextInput', () => { diff --git a/static/app/components/deprecatedforms/genericField.tsx b/static/app/components/deprecatedforms/genericField.tsx index fedab19c273197..1885669ee71d68 100644 --- a/static/app/components/deprecatedforms/genericField.tsx +++ b/static/app/components/deprecatedforms/genericField.tsx @@ -8,7 +8,7 @@ import SelectCreatableField from 'sentry/components/deprecatedforms/selectCreata import SelectField from 'sentry/components/deprecatedforms/selectField'; import TextareaField from 'sentry/components/deprecatedforms/textareaField'; import TextField from 'sentry/components/deprecatedforms/textField'; -import type FormState from 'sentry/components/forms/state'; +import type {FormState} from 'sentry/components/forms/state'; import {defined} from 'sentry/utils'; type FieldType = diff --git a/static/app/components/deprecatedforms/inputField.tsx b/static/app/components/deprecatedforms/inputField.tsx index 06bb8b18876080..572c63580c235e 100644 --- a/static/app/components/deprecatedforms/inputField.tsx +++ b/static/app/components/deprecatedforms/inputField.tsx @@ -22,7 +22,7 @@ type InputFieldProps = FormFieldProps & { /** * @deprecated Do not use this */ -abstract class InputField< +export abstract class InputField< Props extends InputFieldProps = InputFieldProps, State extends FormField['state'] = FormField['state'], > extends FormField { @@ -56,5 +56,3 @@ abstract class InputField< abstract getType(): string; } - -export default InputField; diff --git a/static/app/components/deprecatedforms/numberField.spec.tsx b/static/app/components/deprecatedforms/numberField.spec.tsx index fcbefbd243bb2a..5c35b089248d50 100644 --- a/static/app/components/deprecatedforms/numberField.spec.tsx +++ b/static/app/components/deprecatedforms/numberField.spec.tsx @@ -1,6 +1,6 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; -import Form from 'sentry/components/deprecatedforms/form'; +import {Form} from 'sentry/components/deprecatedforms/form'; import NumberField from 'sentry/components/deprecatedforms/numberField'; describe('NumberField', () => { diff --git a/static/app/components/deprecatedforms/numberField.tsx b/static/app/components/deprecatedforms/numberField.tsx index 7cd5a10b3f4519..4931fa8182b0ec 100644 --- a/static/app/components/deprecatedforms/numberField.tsx +++ b/static/app/components/deprecatedforms/numberField.tsx @@ -1,4 +1,4 @@ -import InputField from 'sentry/components/deprecatedforms/inputField'; +import {InputField} from 'sentry/components/deprecatedforms/inputField'; import {withFormContext} from 'sentry/components/deprecatedforms/withFormContext'; type Props = { diff --git a/static/app/components/deprecatedforms/passwordField.spec.tsx b/static/app/components/deprecatedforms/passwordField.spec.tsx index 45448c9030d5f3..c8b5f4a47f1de0 100644 --- a/static/app/components/deprecatedforms/passwordField.spec.tsx +++ b/static/app/components/deprecatedforms/passwordField.spec.tsx @@ -1,6 +1,6 @@ import {render} from 'sentry-test/reactTestingLibrary'; -import Form from 'sentry/components/deprecatedforms/form'; +import {Form} from 'sentry/components/deprecatedforms/form'; import PasswordField from 'sentry/components/deprecatedforms/passwordField'; describe('PasswordField', () => { diff --git a/static/app/components/deprecatedforms/passwordField.tsx b/static/app/components/deprecatedforms/passwordField.tsx index addfe3b8cd3ae9..717e57df055945 100644 --- a/static/app/components/deprecatedforms/passwordField.tsx +++ b/static/app/components/deprecatedforms/passwordField.tsx @@ -1,6 +1,6 @@ -import InputField from 'sentry/components/deprecatedforms/inputField'; +import {InputField} from 'sentry/components/deprecatedforms/inputField'; import {withFormContext} from 'sentry/components/deprecatedforms/withFormContext'; -import FormState from 'sentry/components/forms/state'; +import {FormState} from 'sentry/components/forms/state'; type Props = InputField['props'] & { formState?: (typeof FormState)[keyof typeof FormState]; diff --git a/static/app/components/deprecatedforms/selectAsyncField.spec.tsx b/static/app/components/deprecatedforms/selectAsyncField.spec.tsx index 56dec9caaf768a..b3fe139d5c5682 100644 --- a/static/app/components/deprecatedforms/selectAsyncField.spec.tsx +++ b/static/app/components/deprecatedforms/selectAsyncField.spec.tsx @@ -1,7 +1,7 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import {selectEvent} from 'sentry-test/selectEvent'; -import Form from 'sentry/components/deprecatedforms/form'; +import {Form} from 'sentry/components/deprecatedforms/form'; import SelectAsyncField from 'sentry/components/deprecatedforms/selectAsyncField'; describe('SelectAsyncField', () => { diff --git a/static/app/components/deprecatedforms/selectCreatableField.spec.tsx b/static/app/components/deprecatedforms/selectCreatableField.spec.tsx index 5d32c5b44a13e0..385ffe0638f9cb 100644 --- a/static/app/components/deprecatedforms/selectCreatableField.spec.tsx +++ b/static/app/components/deprecatedforms/selectCreatableField.spec.tsx @@ -1,6 +1,6 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; -import Form from 'sentry/components/deprecatedforms/form'; +import {Form} from 'sentry/components/deprecatedforms/form'; import SelectCreatableField from 'sentry/components/deprecatedforms/selectCreatableField'; describe('SelectCreatableField', () => { diff --git a/static/app/components/deprecatedforms/selectField.spec.tsx b/static/app/components/deprecatedforms/selectField.spec.tsx index e78091ae497f20..5d678887e962f0 100644 --- a/static/app/components/deprecatedforms/selectField.spec.tsx +++ b/static/app/components/deprecatedforms/selectField.spec.tsx @@ -1,7 +1,7 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import {selectEvent} from 'sentry-test/selectEvent'; -import Form from 'sentry/components/deprecatedforms/form'; +import {Form} from 'sentry/components/deprecatedforms/form'; import SelectField from 'sentry/components/deprecatedforms/selectField'; describe('SelectField', () => { diff --git a/static/app/components/deprecatedforms/textField.spec.tsx b/static/app/components/deprecatedforms/textField.spec.tsx index b7e65e34c93b89..1444903a66acca 100644 --- a/static/app/components/deprecatedforms/textField.spec.tsx +++ b/static/app/components/deprecatedforms/textField.spec.tsx @@ -1,6 +1,6 @@ import {render} from 'sentry-test/reactTestingLibrary'; -import Form from 'sentry/components/deprecatedforms/form'; +import {Form} from 'sentry/components/deprecatedforms/form'; import TextField from 'sentry/components/deprecatedforms/textField'; describe('TextField', () => { diff --git a/static/app/components/deprecatedforms/textField.tsx b/static/app/components/deprecatedforms/textField.tsx index abf883dcad9d9b..d27685cfc5985b 100644 --- a/static/app/components/deprecatedforms/textField.tsx +++ b/static/app/components/deprecatedforms/textField.tsx @@ -1,4 +1,4 @@ -import InputField from 'sentry/components/deprecatedforms/inputField'; +import {InputField} from 'sentry/components/deprecatedforms/inputField'; import {withFormContext} from 'sentry/components/deprecatedforms/withFormContext'; type Props = InputField['props'] & { diff --git a/static/app/components/deprecatedforms/textareaField.tsx b/static/app/components/deprecatedforms/textareaField.tsx index 29c0d3e1b0bfcb..46476ce09df7c2 100644 --- a/static/app/components/deprecatedforms/textareaField.tsx +++ b/static/app/components/deprecatedforms/textareaField.tsx @@ -1,6 +1,6 @@ import {TextArea} from '@sentry/scraps/textarea'; -import InputField from 'sentry/components/deprecatedforms/inputField'; +import {InputField} from 'sentry/components/deprecatedforms/inputField'; import {withFormContext} from 'sentry/components/deprecatedforms/withFormContext'; type State = InputField['state'] & { diff --git a/static/app/components/discover/transactionsList.spec.tsx b/static/app/components/discover/transactionsList.spec.tsx index 31c6879ecbef5f..ba6bbca3017061 100644 --- a/static/app/components/discover/transactionsList.spec.tsx +++ b/static/app/components/discover/transactionsList.spec.tsx @@ -2,7 +2,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg'; import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; import {TransactionsList} from 'sentry/components/discover/transactionsList'; -import EventView from 'sentry/utils/discover/eventView'; +import {EventView} from 'sentry/utils/discover/eventView'; import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import {OrganizationContext} from 'sentry/views/organizationContext'; diff --git a/static/app/components/discover/transactionsList.tsx b/static/app/components/discover/transactionsList.tsx index 99984582644a05..fbd1b61a6acc5b 100644 --- a/static/app/components/discover/transactionsList.tsx +++ b/static/app/components/discover/transactionsList.tsx @@ -16,7 +16,7 @@ import {browserHistory} from 'sentry/utils/browserHistory'; import {DemoTourElement, DemoTourStep} from 'sentry/utils/demoMode/demoTours'; import type {TableDataRow} from 'sentry/utils/discover/discoverQuery'; import {DiscoverQuery} from 'sentry/utils/discover/discoverQuery'; -import type EventView from 'sentry/utils/discover/eventView'; +import type {EventView} from 'sentry/utils/discover/eventView'; import type {Sort} from 'sentry/utils/discover/fields'; import {SavedQueryDatasets} from 'sentry/utils/discover/types'; import {TrendsEventsDiscoverQuery} from 'sentry/utils/performance/trends/trendsDiscoverQuery'; diff --git a/static/app/components/discover/transactionsTable.tsx b/static/app/components/discover/transactionsTable.tsx index d9fa81caf7bcdb..967bf22fb576ba 100644 --- a/static/app/components/discover/transactionsTable.tsx +++ b/static/app/components/discover/transactionsTable.tsx @@ -15,8 +15,7 @@ import {t} from 'sentry/locale'; import type {Organization} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery'; -import type EventView from 'sentry/utils/discover/eventView'; -import type {MetaType} from 'sentry/utils/discover/eventView'; +import type {EventView, MetaType} from 'sentry/utils/discover/eventView'; import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers'; import {fieldAlignment, getAggregateAlias} from 'sentry/utils/discover/fields'; import {ViewReplayLink} from 'sentry/utils/discover/viewReplayLink'; diff --git a/static/app/components/errorBoundary.spec.tsx b/static/app/components/errorBoundary.spec.tsx index c0e64e06b046c7..56328a52119d96 100644 --- a/static/app/components/errorBoundary.spec.tsx +++ b/static/app/components/errorBoundary.spec.tsx @@ -1,6 +1,6 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; -import ErrorBoundary from './errorBoundary'; +import {ErrorBoundary} from './errorBoundary'; describe('ErrorBoundary', () => { it('renders components', () => { diff --git a/static/app/components/errorBoundary.tsx b/static/app/components/errorBoundary.tsx index eea4086e40c48b..25712440cc4206 100644 --- a/static/app/components/errorBoundary.tsx +++ b/static/app/components/errorBoundary.tsx @@ -47,7 +47,7 @@ function getExclamation() { return exclamation[Math.floor(Math.random() * exclamation.length)]; } -class ErrorBoundary extends Component { +export class ErrorBoundary extends Component { static defaultProps: DefaultProps = { mini: false, }; @@ -167,5 +167,3 @@ const StackTrace = styled('pre')` margin-left: 85px; margin-right: 18px; `; - -export default ErrorBoundary; diff --git a/static/app/components/events/autofix/v3/autofixCards.spec.tsx b/static/app/components/events/autofix/v3/autofixCards.spec.tsx index 25ae11120feec9..7cf3e2e5c4bc8f 100644 --- a/static/app/components/events/autofix/v3/autofixCards.spec.tsx +++ b/static/app/components/events/autofix/v3/autofixCards.spec.tsx @@ -198,7 +198,7 @@ describe('SolutionCard', () => { /> ); - expect(screen.getByText('Implementation Plan')).toBeInTheDocument(); + expect(screen.getByText('Plan')).toBeInTheDocument(); expect(screen.getByText('Add null check before accessing user')).toBeInTheDocument(); }); @@ -235,10 +235,10 @@ describe('SolutionCard', () => { /> ); - expect(screen.getByText('Implementation Plan')).toBeInTheDocument(); + expect(screen.getByText('Plan')).toBeInTheDocument(); expect( screen.getByText( - 'Seer failed to generate an implementation plan. This one is on us. Try running it again.' + 'Seer failed to generate a plan. This one is on us. Try running it again.' ) ).toBeInTheDocument(); expect(screen.getByRole('button', {name: 'Re-run'})).toBeInTheDocument(); @@ -351,8 +351,16 @@ describe('PullRequestsCard', () => { autofix={mockAutofix} section={makeSection('pull_request', 'completed', [ [ - makePR({repo_name: 'org/repo-a', pr_number: 10, pr_url: 'https://pr/10'}), - makePR({repo_name: 'org/repo-b', pr_number: 20, pr_url: 'https://pr/20'}), + makePR({ + repo_name: 'org/repo-a', + pr_number: 10, + pr_url: 'https://pr/10', + }), + makePR({ + repo_name: 'org/repo-b', + pr_number: 20, + pr_url: 'https://pr/20', + }), ], ])} /> @@ -468,7 +476,11 @@ describe('CodingAgentCard', () => { ); @@ -481,7 +493,11 @@ describe('CodingAgentCard', () => { ); @@ -494,7 +510,11 @@ describe('CodingAgentCard', () => { ); @@ -577,8 +597,16 @@ describe('CodingAgentCard', () => { autofix={mockAutofix} section={makeSection('coding_agents', 'completed', [ [ - makeCodingAgent({id: 'agent-1', name: 'Agent One', status: 'completed'}), - makeCodingAgent({id: 'agent-2', name: 'Agent Two', status: 'running'}), + makeCodingAgent({ + id: 'agent-1', + name: 'Agent One', + status: 'completed', + }), + makeCodingAgent({ + id: 'agent-2', + name: 'Agent Two', + status: 'running', + }), ], ])} /> diff --git a/static/app/components/events/autofix/v3/autofixCards.tsx b/static/app/components/events/autofix/v3/autofixCards.tsx index 096b9555496a16..fa4b42347aa0bc 100644 --- a/static/app/components/events/autofix/v3/autofixCards.tsx +++ b/static/app/components/events/autofix/v3/autofixCards.tsx @@ -115,7 +115,7 @@ export function SolutionCard({autofix, section}: AutofixCardProps) { const runId = runState?.run_id; return ( - } title={t('Implementation Plan')}> + } title={t('Plan')}> {section.status === 'processing' ? ( ) : artifact?.data ? ( @@ -143,7 +143,7 @@ export function SolutionCard({autofix, section}: AutofixCardProps) { {t( - 'Seer failed to generate an implementation plan. This one is on us. Try running it again.' + 'Seer failed to generate a plan. This one is on us. Try running it again.' )}
diff --git a/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx b/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx index b3e59b2b87e9ca..b7b1e87eab1a7c 100644 --- a/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx +++ b/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx @@ -74,7 +74,7 @@ describe('RootCausePreview', () => { }); describe('SolutionPreview', () => { - it('renders implementation plan title and summary', () => { + it('renders plan title and summary', () => { const artifact: Artifact = { key: 'solution', reason: 'Found solution', @@ -86,7 +86,7 @@ describe('SolutionPreview', () => { render(); - expect(screen.getByText('Implementation Plan')).toBeInTheDocument(); + expect(screen.getByText('Plan')).toBeInTheDocument(); expect(screen.getByText('Add null check before accessing user')).toBeInTheDocument(); }); @@ -107,10 +107,10 @@ describe('SolutionPreview', () => { render(); - expect(screen.getByText('Implementation Plan')).toBeInTheDocument(); + expect(screen.getByText('Plan')).toBeInTheDocument(); expect( screen.getByText( - 'Seer failed to generate an implementation plan. This one is on us. Try running it again.' + 'Seer failed to generate a plan. This one is on us. Try running it again.' ) ).toBeInTheDocument(); }); @@ -227,8 +227,16 @@ describe('PullRequestsPreview', () => { @@ -242,7 +250,13 @@ describe('PullRequestsPreview', () => { render( ); @@ -319,7 +333,11 @@ describe('CodingAgentPreview', () => { render( ); @@ -331,7 +349,11 @@ describe('CodingAgentPreview', () => { render( ); @@ -343,7 +365,11 @@ describe('CodingAgentPreview', () => { render( ); @@ -429,7 +455,11 @@ describe('CodingAgentPreview', () => { section={makeSection('coding_agents', [ [ makeCodingAgent({id: 'a1', name: 'Agent One', status: 'running'}), - makeCodingAgent({id: 'a2', name: 'Agent Two', status: 'completed'}), + makeCodingAgent({ + id: 'a2', + name: 'Agent Two', + status: 'completed', + }), ], ])} /> diff --git a/static/app/components/events/autofix/v3/autofixPreviews.tsx b/static/app/components/events/autofix/v3/autofixPreviews.tsx index ca73007c55c59a..1ab9f0499f1f9e 100644 --- a/static/app/components/events/autofix/v3/autofixPreviews.tsx +++ b/static/app/components/events/autofix/v3/autofixPreviews.tsx @@ -63,16 +63,14 @@ export function SolutionPreview({section}: ArtifactPreviewProps) { }, [section]); return ( - } title={t('Implementation Plan')}> + } title={t('Plan')}> {section.status === 'processing' ? ( ) : artifact?.data ? ( {artifact.data.one_line_summary} ) : ( - {t( - 'Seer failed to generate an implementation plan. This one is on us. Try running it again.' - )} + {t('Seer failed to generate a plan. This one is on us. Try running it again.')} )} diff --git a/static/app/components/events/autofix/v3/utils.spec.ts b/static/app/components/events/autofix/v3/utils.spec.ts index 310a2d47250a4b..81edf1a57bbf32 100644 --- a/static/app/components/events/autofix/v3/utils.spec.ts +++ b/static/app/components/events/autofix/v3/utils.spec.ts @@ -67,7 +67,10 @@ function makeSolutionArtifact( data: { one_line_summary: 'Add null check before accessing property', steps: [ - {title: 'Add guard clause', description: 'Check for null before accessing .name'}, + { + title: 'Add guard clause', + description: 'Check for null before accessing .name', + }, {title: 'Add test', description: 'Cover the null input case'}, ], }, @@ -144,7 +147,7 @@ describe('artifactToMarkdown', () => { it('renders full solution with steps', () => { expect(artifactToMarkdown(makeSolutionArtifact())).toBe( [ - '# Implementation Plan', + '# Plan', '', 'Add null check before accessing property', '', @@ -169,9 +172,7 @@ describe('artifactToMarkdown', () => { steps: [], }, }); - expect(artifactToMarkdown(artifact)).toBe( - ['# Implementation Plan', '', 'Quick fix'].join('\n') - ); + expect(artifactToMarkdown(artifact)).toBe(['# Plan', '', 'Quick fix'].join('\n')); }); }); diff --git a/static/app/components/events/autofix/v3/utils.ts b/static/app/components/events/autofix/v3/utils.ts index 93092f01796a1b..2a3d285537aca8 100644 --- a/static/app/components/events/autofix/v3/utils.ts +++ b/static/app/components/events/autofix/v3/utils.ts @@ -76,7 +76,7 @@ function solutionArtifactToMarkdown(artifact: Artifact): strin return null; } - const parts: string[] = ['# Implementation Plan', '', solution.one_line_summary]; + const parts: string[] = ['# Plan', '', solution.one_line_summary]; if (solution.steps.length) { parts.push(''); diff --git a/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx b/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx index 7dddb5c486bde3..531a692d94c16c 100644 --- a/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx +++ b/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx @@ -5,7 +5,7 @@ import styled from '@emotion/styled'; import {Button} from '@sentry/scraps/button'; import {Grid} from '@sentry/scraps/layout'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import { BreadcrumbControlOptions, BreadcrumbsDrawer, diff --git a/static/app/components/events/breadcrumbs/breadcrumbsTimeline.tsx b/static/app/components/events/breadcrumbs/breadcrumbsTimeline.tsx index 9954cf175b04e0..4cc825b78f2572 100644 --- a/static/app/components/events/breadcrumbs/breadcrumbsTimeline.tsx +++ b/static/app/components/events/breadcrumbs/breadcrumbsTimeline.tsx @@ -8,7 +8,7 @@ import {Tooltip} from '@sentry/scraps/tooltip'; import {DateTime} from 'sentry/components/dateTime'; import {Duration} from 'sentry/components/duration'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {BreadcrumbItemContent} from 'sentry/components/events/breadcrumbs/breadcrumbItemContent'; import type {EnhancedCrumb} from 'sentry/components/events/breadcrumbs/utils'; import {Timeline} from 'sentry/components/timeline'; diff --git a/static/app/components/events/contexts/contextBlock.tsx b/static/app/components/events/contexts/contextBlock.tsx index fc649fda8d7c60..ea2ddc6cee7dd5 100644 --- a/static/app/components/events/contexts/contextBlock.tsx +++ b/static/app/components/events/contexts/contextBlock.tsx @@ -1,4 +1,4 @@ -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {KeyValueList} from 'sentry/components/events/interfaces/keyValueList'; import type {KeyValueListData} from 'sentry/types/group'; diff --git a/static/app/components/events/contexts/contextCard.tsx b/static/app/components/events/contexts/contextCard.tsx index 457f3971d47bc5..371f404a852fae 100644 --- a/static/app/components/events/contexts/contextCard.tsx +++ b/static/app/components/events/contexts/contextCard.tsx @@ -3,7 +3,7 @@ import startCase from 'lodash/startCase'; import {Flex} from '@sentry/scraps/layout'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import type {ContextValue} from 'sentry/components/events/contexts'; import { getContextIcon, diff --git a/static/app/components/events/contexts/contextDataSection.tsx b/static/app/components/events/contexts/contextDataSection.tsx index 7df339d6b2f04a..70a8d04a0545f7 100644 --- a/static/app/components/events/contexts/contextDataSection.tsx +++ b/static/app/components/events/contexts/contextDataSection.tsx @@ -1,6 +1,6 @@ import {ExternalLink} from '@sentry/scraps/link'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {getOrderedContextItems} from 'sentry/components/events/contexts'; import {ContextCard} from 'sentry/components/events/contexts/contextCard'; import {CONTEXT_DOCS_LINK} from 'sentry/components/events/contexts/utils'; diff --git a/static/app/components/events/eventEntry.tsx b/static/app/components/events/eventEntry.tsx index f508e2847f8325..02d12ca926a9af 100644 --- a/static/app/components/events/eventEntry.tsx +++ b/static/app/components/events/eventEntry.tsx @@ -1,4 +1,4 @@ -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {EventBreadcrumbsSection} from 'sentry/components/events/eventBreadcrumbsSection'; import {t} from 'sentry/locale'; import type {Entry, Event, EventTransaction} from 'sentry/types/event'; diff --git a/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx b/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx index fc8e9a3ce7598c..7c1f125f64add6 100644 --- a/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx +++ b/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import {NegativeSpaceContainer} from 'sentry/components/container/negativeSpaceContainer'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {REPLAY_LOADING_HEIGHT} from 'sentry/components/events/eventReplay/constants'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {ArchivedReplayAlert} from 'sentry/components/replays/alerts/archivedReplayAlert'; diff --git a/static/app/components/events/eventHydrationDiff/replayDiffSection.tsx b/static/app/components/events/eventHydrationDiff/replayDiffSection.tsx index 6cd8818c7e6c17..1c3a239abaf2b8 100644 --- a/static/app/components/events/eventHydrationDiff/replayDiffSection.tsx +++ b/static/app/components/events/eventHydrationDiff/replayDiffSection.tsx @@ -3,7 +3,7 @@ import ReactLazyLoad from 'react-lazyload'; import styled from '@emotion/styled'; import {NegativeSpaceContainer} from 'sentry/components/container/negativeSpaceContainer'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {REPLAY_LOADING_HEIGHT} from 'sentry/components/events/eventReplay/constants'; import {LazyLoad} from 'sentry/components/lazyLoad'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; diff --git a/static/app/components/events/eventReplay/index.tsx b/static/app/components/events/eventReplay/index.tsx index 08c7c9c1d00a28..d356066fcb1b91 100644 --- a/static/app/components/events/eventReplay/index.tsx +++ b/static/app/components/events/eventReplay/index.tsx @@ -1,6 +1,6 @@ import {lazy} from 'react'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {ReplayClipSection} from 'sentry/components/events/eventReplay/replayClipSection'; import {LazyLoad} from 'sentry/components/lazyLoad'; import type {Event} from 'sentry/types/event'; diff --git a/static/app/components/events/eventReplay/replayClipSection.tsx b/static/app/components/events/eventReplay/replayClipSection.tsx index 11db578c89311a..f2f2c87b35e648 100644 --- a/static/app/components/events/eventReplay/replayClipSection.tsx +++ b/static/app/components/events/eventReplay/replayClipSection.tsx @@ -4,7 +4,7 @@ import styled from '@emotion/styled'; import {LinkButton} from '@sentry/scraps/button'; import {NegativeSpaceContainer} from 'sentry/components/container/negativeSpaceContainer'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {REPLAY_LOADING_HEIGHT} from 'sentry/components/events/eventReplay/constants'; import {LazyLoad} from 'sentry/components/lazyLoad'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; diff --git a/static/app/components/events/eventReplay/replayPreviewPlayer.tsx b/static/app/components/events/eventReplay/replayPreviewPlayer.tsx index b1bbdff68f7abe..ca903244f7f238 100644 --- a/static/app/components/events/eventReplay/replayPreviewPlayer.tsx +++ b/static/app/components/events/eventReplay/replayPreviewPlayer.tsx @@ -8,7 +8,7 @@ import {Button, LinkButton, type LinkButtonProps} from '@sentry/scraps/button'; import {Flex, Stack} from '@sentry/scraps/layout'; import {TooltipContext} from '@sentry/scraps/tooltip'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {useReplayContext} from 'sentry/components/replays/replayContext'; import {ReplayCurrentScreen} from 'sentry/components/replays/replayCurrentScreen'; import {ReplayCurrentUrl} from 'sentry/components/replays/replayCurrentUrl'; diff --git a/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx b/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx index ee89577994cb9c..ae5ea5e5d17399 100644 --- a/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx +++ b/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx @@ -1,14 +1,14 @@ import {LinkButton} from '@sentry/scraps/button'; import {ChartType} from 'sentry/chartcuterie/types'; -import TransitionChart from 'sentry/components/charts/transitionChart'; +import {TransitionChart} from 'sentry/components/charts/transitionChart'; import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask'; import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; import type {EventsStatsData} from 'sentry/types/organization'; import {toArray} from 'sentry/utils/array/toArray'; import type {MetaType} from 'sentry/utils/discover/eventView'; -import EventView from 'sentry/utils/discover/eventView'; +import {EventView} from 'sentry/utils/discover/eventView'; import type {DiscoverQueryProps} from 'sentry/utils/discover/genericDiscoverQuery'; import {useGenericDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; diff --git a/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx index 481191de5b8157..c52241a8b18cb6 100644 --- a/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx +++ b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx @@ -18,7 +18,7 @@ import { MINIMAP_HEIGHT, MinimapBackground, } from 'sentry/components/events/interfaces/spans/minimap'; -import WaterfallModel from 'sentry/components/events/interfaces/spans/waterfallModel'; +import {WaterfallModel} from 'sentry/components/events/interfaces/spans/waterfallModel'; import {OpsBreakdown} from 'sentry/components/events/opsBreakdown'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {TextOverflow} from 'sentry/components/textOverflow'; @@ -29,7 +29,7 @@ import type {Project} from 'sentry/types/project'; import {defined} from 'sentry/utils'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery'; -import EventView from 'sentry/utils/discover/eventView'; +import {EventView} from 'sentry/utils/discover/eventView'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import {getShortEventId} from 'sentry/utils/events'; @@ -40,7 +40,7 @@ import {useOrganization} from 'sentry/utils/useOrganization'; const BUTTON_ICON_SIZE = 'sm'; const BUTTON_SIZE = 'sm'; -export function getSampleEventQuery({ +function getSampleEventQuery({ transaction, durationBaseline, addUpperBound = true, diff --git a/static/app/components/events/eventTags/eventTagsTree.tsx b/static/app/components/events/eventTags/eventTagsTree.tsx index 1dff687343b31b..f328b7c0b2558b 100644 --- a/static/app/components/events/eventTags/eventTagsTree.tsx +++ b/static/app/components/events/eventTags/eventTagsTree.tsx @@ -1,7 +1,7 @@ import {Fragment, useMemo, useRef} from 'react'; import styled from '@emotion/styled'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import { EventTagsTreeRow, type EventTagsTreeRowProps, diff --git a/static/app/components/events/eventViewHierarchy.tsx b/static/app/components/events/eventViewHierarchy.tsx index 8204a915135795..f3ea25b585b5c3 100644 --- a/static/app/components/events/eventViewHierarchy.tsx +++ b/static/app/components/events/eventViewHierarchy.tsx @@ -2,7 +2,7 @@ import {useMemo} from 'react'; import * as Sentry from '@sentry/react'; import {useFetchEventAttachments} from 'sentry/actionCreators/events'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {getAttachmentUrl} from 'sentry/components/events/attachmentViewers/utils'; import { getPlatform, diff --git a/static/app/components/events/eventXrayDiff.tsx b/static/app/components/events/eventXrayDiff.tsx index 87a9c29ea9e3cd..ef81b4707fa93a 100644 --- a/static/app/components/events/eventXrayDiff.tsx +++ b/static/app/components/events/eventXrayDiff.tsx @@ -1,5 +1,5 @@ import {EmptyStateWarning} from 'sentry/components/emptyStateWarning'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {LoadingError} from 'sentry/components/loadingError'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {t} from 'sentry/locale'; diff --git a/static/app/components/events/highlights/highlightsDataSection.tsx b/static/app/components/events/highlights/highlightsDataSection.tsx index c28a6dd5cfbd7b..04ff34a4898181 100644 --- a/static/app/components/events/highlights/highlightsDataSection.tsx +++ b/static/app/components/events/highlights/highlightsDataSection.tsx @@ -7,7 +7,7 @@ import {ExternalLink} from '@sentry/scraps/link'; import {openModal} from 'sentry/actionCreators/modal'; import {hasEveryAccess} from 'sentry/components/acl/access'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {ContextCardContent} from 'sentry/components/events/contexts/contextCard'; import {getContextMeta} from 'sentry/components/events/contexts/utils'; import { diff --git a/static/app/components/events/interfaces/crashContent/exception/content.tsx b/static/app/components/events/interfaces/crashContent/exception/content.tsx index 0b3193ed253aed..66b0a979536a00 100644 --- a/static/app/components/events/interfaces/crashContent/exception/content.tsx +++ b/static/app/components/events/interfaces/crashContent/exception/content.tsx @@ -5,7 +5,7 @@ import {Button} from '@sentry/scraps/button'; import {Container} from '@sentry/scraps/layout'; import {Tooltip} from '@sentry/scraps/tooltip'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {StacktraceBanners} from 'sentry/components/events/interfaces/crashContent/exception/banners/stacktraceBanners'; import {useLineCoverageContext} from 'sentry/components/events/interfaces/crashContent/exception/lineCoverageContext'; import { diff --git a/static/app/components/events/interfaces/crashContent/exception/index.tsx b/static/app/components/events/interfaces/crashContent/exception/index.tsx index e55825c28e8805..08fbbc82a3b176 100644 --- a/static/app/components/events/interfaces/crashContent/exception/index.tsx +++ b/static/app/components/events/interfaces/crashContent/exception/index.tsx @@ -1,4 +1,4 @@ -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {useStacktraceContext} from 'sentry/components/events/interfaces/stackTraceContext'; import type {Event, ExceptionType} from 'sentry/types/event'; import type {Group} from 'sentry/types/group'; diff --git a/static/app/components/events/interfaces/crashContent/stackTrace/index.tsx b/static/app/components/events/interfaces/crashContent/stackTrace/index.tsx index 129fca8516099e..73a6c278e7dc6b 100644 --- a/static/app/components/events/interfaces/crashContent/stackTrace/index.tsx +++ b/static/app/components/events/interfaces/crashContent/stackTrace/index.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import type {Event} from 'sentry/types/event'; import type {PlatformKey} from 'sentry/types/project'; import type {StacktraceType} from 'sentry/types/stacktrace'; diff --git a/static/app/components/events/interfaces/debugMeta/debugImageDetails/candidates.tsx b/static/app/components/events/interfaces/debugMeta/debugImageDetails/candidates.tsx index ef7f4dd6629ffb..946de23e19bf16 100644 --- a/static/app/components/events/interfaces/debugMeta/debugImageDetails/candidates.tsx +++ b/static/app/components/events/interfaces/debugMeta/debugImageDetails/candidates.tsx @@ -49,7 +49,7 @@ type State = { searchTerm: string; }; -class Candidates extends Component { +export class Candidates extends Component { state: State = { searchTerm: '', filterOptions: [], @@ -370,8 +370,6 @@ class Candidates extends Component { } } -export default Candidates; - const Wrapper = styled('div')` display: grid; `; diff --git a/static/app/components/events/interfaces/debugMeta/debugImageDetails/index.tsx b/static/app/components/events/interfaces/debugMeta/debugImageDetails/index.tsx index 71f665fb567b70..20e013eda36216 100644 --- a/static/app/components/events/interfaces/debugMeta/debugImageDetails/index.tsx +++ b/static/app/components/events/interfaces/debugMeta/debugImageDetails/index.tsx @@ -26,7 +26,7 @@ import {useApi} from 'sentry/utils/useApi'; import {useOrganization} from 'sentry/utils/useOrganization'; import {getPrettyFileType} from 'sentry/views/settings/projectDebugFiles/utils'; -import Candidates from './candidates'; +import {Candidates} from './candidates'; import {GeneralInfo} from './generalInfo'; import {ReprocessAlert} from './reprocessAlert'; import {INTERNAL_SOURCE, INTERNAL_SOURCE_LOCATION} from './utils'; diff --git a/static/app/components/events/interfaces/exception.tsx b/static/app/components/events/interfaces/exception.tsx index 81525a3b54b67c..28cdd1305b1fd5 100644 --- a/static/app/components/events/interfaces/exception.tsx +++ b/static/app/components/events/interfaces/exception.tsx @@ -1,7 +1,7 @@ import {Fragment} from 'react'; import {CommitRow} from 'sentry/components/commitRow'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {StacktraceContext} from 'sentry/components/events/interfaces/stackTraceContext'; import {SuspectCommits} from 'sentry/components/events/suspectCommits'; import {TraceEventDataSection} from 'sentry/components/events/traceEventDataSection'; diff --git a/static/app/components/events/interfaces/frame/deprecatedLine.tsx b/static/app/components/events/interfaces/frame/deprecatedLine.tsx index 7cb66b96576f1f..8f666540d12f9b 100644 --- a/static/app/components/events/interfaces/frame/deprecatedLine.tsx +++ b/static/app/components/events/interfaces/frame/deprecatedLine.tsx @@ -8,7 +8,7 @@ import {Button} from '@sentry/scraps/button'; import InteractionStateLayer from '@sentry/scraps/interactionStateLayer'; import {openModal} from 'sentry/actionCreators/modal'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {analyzeFrameForRootCause} from 'sentry/components/events/interfaces/analyzeFrames'; import {LeadHint} from 'sentry/components/events/interfaces/frame/leadHint'; import {StacktraceLink} from 'sentry/components/events/interfaces/frame/stacktraceLink'; diff --git a/static/app/components/events/interfaces/nativeFrame.tsx b/static/app/components/events/interfaces/nativeFrame.tsx index 3d47927cbda43b..e356ebbfca0552 100644 --- a/static/app/components/events/interfaces/nativeFrame.tsx +++ b/static/app/components/events/interfaces/nativeFrame.tsx @@ -8,7 +8,7 @@ import InteractionStateLayer from '@sentry/scraps/interactionStateLayer'; import {Flex} from '@sentry/scraps/layout'; import {Tooltip} from '@sentry/scraps/tooltip'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {FRAME_TOOLTIP_MAX_WIDTH} from 'sentry/components/events/interfaces/frame/defaultTitle'; import {OpenInContextLine} from 'sentry/components/events/interfaces/frame/openInContextLine'; import {StacktraceLink} from 'sentry/components/events/interfaces/frame/stacktraceLink'; diff --git a/static/app/components/events/interfaces/request/index.tsx b/static/app/components/events/interfaces/request/index.tsx index 855760a97f1956..0803cab1dd2775 100644 --- a/static/app/components/events/interfaces/request/index.tsx +++ b/static/app/components/events/interfaces/request/index.tsx @@ -8,7 +8,7 @@ import {SegmentedControl} from '@sentry/scraps/segmentedControl'; import {Text} from '@sentry/scraps/text'; import {ClippedBox} from 'sentry/components/clippedBox'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {EventDataSection} from 'sentry/components/events/eventDataSection'; import {GraphQlRequestBody} from 'sentry/components/events/interfaces/request/graphQlRequestBody'; import {getCurlCommand, getFullUrl} from 'sentry/components/events/interfaces/utils'; diff --git a/static/app/components/events/interfaces/request/richHttpContentClippedBoxBodySection.tsx b/static/app/components/events/interfaces/request/richHttpContentClippedBoxBodySection.tsx index 06f332e1096281..3f2f40293f1835 100644 --- a/static/app/components/events/interfaces/request/richHttpContentClippedBoxBodySection.tsx +++ b/static/app/components/events/interfaces/request/richHttpContentClippedBoxBodySection.tsx @@ -1,5 +1,5 @@ import {ClippedBox} from 'sentry/components/clippedBox'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {KeyValueList} from 'sentry/components/events/interfaces/keyValueList'; import {StructuredEventData} from 'sentry/components/structuredEventData'; import {JsonEventData} from 'sentry/components/structuredEventData/jsonEventData'; diff --git a/static/app/components/events/interfaces/request/richHttpContentClippedBoxKeyValueList.tsx b/static/app/components/events/interfaces/request/richHttpContentClippedBoxKeyValueList.tsx index 646c6d1af9b5bf..56cfc23a7449ef 100644 --- a/static/app/components/events/interfaces/request/richHttpContentClippedBoxKeyValueList.tsx +++ b/static/app/components/events/interfaces/request/richHttpContentClippedBoxKeyValueList.tsx @@ -1,5 +1,5 @@ import {ClippedBox} from 'sentry/components/clippedBox'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {KeyValueList} from 'sentry/components/events/interfaces/keyValueList'; import type {EntryRequest} from 'sentry/types/event'; import type {Meta} from 'sentry/types/group'; diff --git a/static/app/components/events/interfaces/spans/spanTreeModel.spec.tsx b/static/app/components/events/interfaces/spans/spanTreeModel.spec.tsx index 2ee80d32006a81..1bf2c0d1ec2287 100644 --- a/static/app/components/events/interfaces/spans/spanTreeModel.spec.tsx +++ b/static/app/components/events/interfaces/spans/spanTreeModel.spec.tsx @@ -1,6 +1,6 @@ import {waitFor} from 'sentry-test/reactTestingLibrary'; -import SpanTreeModel from 'sentry/components/events/interfaces/spans/spanTreeModel'; +import {SpanTreeModel} from 'sentry/components/events/interfaces/spans/spanTreeModel'; import type { EnhancedProcessedSpanType, RawSpanType, @@ -607,7 +607,7 @@ describe('SpanTreeModel', () => { // If statement here is required to avoid TS linting issues if (spans[1]!.type === 'span_group_siblings') { - expect(spans[1]!.spanSiblingGrouping!).toHaveLength(5); + expect(spans[1]!.spanSiblingGrouping).toHaveLength(5); } }); diff --git a/static/app/components/events/interfaces/spans/spanTreeModel.tsx b/static/app/components/events/interfaces/spans/spanTreeModel.tsx index 40e6f5981b4fe2..1894714678ff56 100644 --- a/static/app/components/events/interfaces/spans/spanTreeModel.tsx +++ b/static/app/components/events/interfaces/spans/spanTreeModel.tsx @@ -34,7 +34,7 @@ import { const MIN_SIBLING_GROUP_SIZE = 5; -class SpanTreeModel { +export class SpanTreeModel { api: Client; // readonly state @@ -847,5 +847,3 @@ class SpanTreeModel { }; }; } - -export default SpanTreeModel; diff --git a/static/app/components/events/interfaces/spans/types.tsx b/static/app/components/events/interfaces/spans/types.tsx index 824cb7ba9e529d..1acc38d15d6685 100644 --- a/static/app/components/events/interfaces/spans/types.tsx +++ b/static/app/components/events/interfaces/spans/types.tsx @@ -1,6 +1,6 @@ import type {Fuse} from 'sentry/utils/fuzzySearch'; -import type SpanTreeModel from './spanTreeModel'; +import type {SpanTreeModel} from './spanTreeModel'; export type GapSpanType = { isOrphan: boolean; diff --git a/static/app/components/events/interfaces/spans/utils.tsx b/static/app/components/events/interfaces/spans/utils.tsx index e37285992d726f..34625c4a442603 100644 --- a/static/app/components/events/interfaces/spans/utils.tsx +++ b/static/app/components/events/interfaces/spans/utils.tsx @@ -13,7 +13,7 @@ import type { import {EntryType} from 'sentry/types/event'; import {assert} from 'sentry/types/utils'; -import type SpanTreeModel from './spanTreeModel'; +import type {SpanTreeModel} from './spanTreeModel'; import type { AggregateSpanType, GapSpanType, diff --git a/static/app/components/events/interfaces/spans/waterfallModel.spec.tsx b/static/app/components/events/interfaces/spans/waterfallModel.spec.tsx index 72bdb31a948427..06e213a28bc52a 100644 --- a/static/app/components/events/interfaces/spans/waterfallModel.spec.tsx +++ b/static/app/components/events/interfaces/spans/waterfallModel.spec.tsx @@ -1,7 +1,7 @@ import type {ActiveFilter} from 'sentry/components/events/interfaces/spans/filter'; import {noFilter} from 'sentry/components/events/interfaces/spans/filter'; import type {EnhancedProcessedSpanType} from 'sentry/components/events/interfaces/spans/types'; -import WaterfallModel from 'sentry/components/events/interfaces/spans/waterfallModel'; +import {WaterfallModel} from 'sentry/components/events/interfaces/spans/waterfallModel'; import type {EventTransaction} from 'sentry/types/event'; import {EntryType} from 'sentry/types/event'; import {assert} from 'sentry/types/utils'; diff --git a/static/app/components/events/interfaces/spans/waterfallModel.tsx b/static/app/components/events/interfaces/spans/waterfallModel.tsx index 309f8575865003..042ffb5ee5bbae 100644 --- a/static/app/components/events/interfaces/spans/waterfallModel.tsx +++ b/static/app/components/events/interfaces/spans/waterfallModel.tsx @@ -9,7 +9,7 @@ import {createFuzzySearch} from 'sentry/utils/fuzzySearch'; import type {ActiveOperationFilter} from './filter'; import {noFilter, toggleAllFilters, toggleFilter} from './filter'; -import SpanTreeModel from './spanTreeModel'; +import {SpanTreeModel} from './spanTreeModel'; import type { EnhancedProcessedSpanType, FilterSpans, @@ -21,7 +21,7 @@ import type { } from './types'; import {boundsGenerator, generateRootSpan, getSpanID, parseTrace} from './utils'; -class WaterfallModel { +export class WaterfallModel { api: Client = new Client(); // readonly state @@ -356,5 +356,3 @@ class WaterfallModel { }); }; } - -export default WaterfallModel; diff --git a/static/app/components/events/interfaces/threads.tsx b/static/app/components/events/interfaces/threads.tsx index 6ba512e40688c3..593aeb24b2a58d 100644 --- a/static/app/components/events/interfaces/threads.tsx +++ b/static/app/components/events/interfaces/threads.tsx @@ -5,7 +5,7 @@ import {Button, ButtonBar} from '@sentry/scraps/button'; import {Flex} from '@sentry/scraps/layout'; import {CommitRow} from 'sentry/components/commitRow'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import { StacktraceContext, useStacktraceContext, diff --git a/static/app/components/feedback/feedbackItem/feedbackActions.tsx b/static/app/components/feedback/feedbackItem/feedbackActions.tsx index e64feca2f18304..6289a92c5e3fac 100644 --- a/static/app/components/feedback/feedbackItem/feedbackActions.tsx +++ b/static/app/components/feedback/feedbackItem/feedbackActions.tsx @@ -6,7 +6,7 @@ import {Flex} from '@sentry/scraps/layout'; import {Tooltip} from '@sentry/scraps/tooltip'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {FeedbackAssignedTo} from 'sentry/components/feedback/feedbackItem/feedbackAssignedTo'; import {useFeedbackActions} from 'sentry/components/feedback/feedbackItem/useFeedbackActions'; import {IconCopy, IconEllipsis} from 'sentry/icons'; diff --git a/static/app/components/feedback/feedbackItem/feedbackItem.tsx b/static/app/components/feedback/feedbackItem/feedbackItem.tsx index b88ab29990baf3..8cb117b0886408 100644 --- a/static/app/components/feedback/feedbackItem/feedbackItem.tsx +++ b/static/app/components/feedback/feedbackItem/feedbackItem.tsx @@ -2,7 +2,7 @@ import {Fragment, useEffect, useMemo, useRef} from 'react'; import styled from '@emotion/styled'; import {AnalyticsArea} from 'sentry/components/analyticsArea'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {getOrderedContextItems} from 'sentry/components/events/contexts'; import {ContextCard} from 'sentry/components/events/contexts/contextCard'; import {EventTagsTree} from 'sentry/components/events/eventTags/eventTagsTree'; diff --git a/static/app/components/feedback/feedbackItem/feedbackItemHeader.tsx b/static/app/components/feedback/feedbackItem/feedbackItemHeader.tsx index cb32a3b88c0a9b..ab87b8a395250c 100644 --- a/static/app/components/feedback/feedbackItem/feedbackItemHeader.tsx +++ b/static/app/components/feedback/feedbackItem/feedbackItemHeader.tsx @@ -4,7 +4,7 @@ import styled from '@emotion/styled'; import {Button} from '@sentry/scraps/button'; import {Flex} from '@sentry/scraps/layout'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {FeedbackActions} from 'sentry/components/feedback/feedbackItem/feedbackActions'; import {FeedbackShortId} from 'sentry/components/feedback/feedbackItem/feedbackShortId'; import {FeedbackViewers} from 'sentry/components/feedback/feedbackItem/feedbackViewers'; diff --git a/static/app/components/feedback/feedbackItem/feedbackItemLoader.tsx b/static/app/components/feedback/feedbackItem/feedbackItemLoader.tsx index 51b51a907de38e..07ae5a5486978c 100644 --- a/static/app/components/feedback/feedbackItem/feedbackItemLoader.tsx +++ b/static/app/components/feedback/feedbackItem/feedbackItemLoader.tsx @@ -1,6 +1,6 @@ import {useEffect} from 'react'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {FeedbackEmptyDetails} from 'sentry/components/feedback/details/feedbackEmptyDetails'; import {FeedbackErrorDetails} from 'sentry/components/feedback/details/feedbackErrorDetails'; import {FeedbackItem} from 'sentry/components/feedback/feedbackItem/feedbackItem'; diff --git a/static/app/components/feedback/feedbackItem/feedbackReplay.tsx b/static/app/components/feedback/feedbackItem/feedbackReplay.tsx index 84aa2bb573fea7..632f57ebcef1f5 100644 --- a/static/app/components/feedback/feedbackItem/feedbackReplay.tsx +++ b/static/app/components/feedback/feedbackItem/feedbackReplay.tsx @@ -1,4 +1,4 @@ -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {FeedbackItemSection} from 'sentry/components/feedback/feedbackItem/feedbackItemSection'; import {ReplayInlineCTAPanel} from 'sentry/components/feedback/feedbackItem/replayInlineCTAPanel'; import {ReplaySection} from 'sentry/components/feedback/feedbackItem/replaySection'; diff --git a/static/app/components/feedback/list/feedbackList.tsx b/static/app/components/feedback/list/feedbackList.tsx index f7eff63c352788..d7500de50dd00f 100644 --- a/static/app/components/feedback/list/feedbackList.tsx +++ b/static/app/components/feedback/list/feedbackList.tsx @@ -8,7 +8,7 @@ import {Stack} from '@sentry/scraps/layout'; import {Tooltip} from '@sentry/scraps/tooltip'; import type {ApiResult} from 'sentry/api'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {FeedbackListHeader} from 'sentry/components/feedback/list/feedbackListHeader'; import {FeedbackListItem} from 'sentry/components/feedback/list/feedbackListItem'; import {useFeedbackQueryKeys} from 'sentry/components/feedback/useFeedbackQueryKeys'; diff --git a/static/app/components/feedback/list/feedbackListBulkSelection.tsx b/static/app/components/feedback/list/feedbackListBulkSelection.tsx index 3c8e5b338dcb6a..219694163da955 100644 --- a/static/app/components/feedback/list/feedbackListBulkSelection.tsx +++ b/static/app/components/feedback/list/feedbackListBulkSelection.tsx @@ -2,7 +2,7 @@ import {Button} from '@sentry/scraps/button'; import {Flex} from '@sentry/scraps/layout'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {useBulkEditFeedbacks} from 'sentry/components/feedback/list/useBulkEditFeedbacks'; import type {Mailbox} from 'sentry/components/feedback/useMailbox'; import {IconEllipsis} from 'sentry/icons/iconEllipsis'; diff --git a/static/app/components/feedback/useMutateActivity.tsx b/static/app/components/feedback/useMutateActivity.tsx index b53dff3cb1654e..6b456c902d2223 100644 --- a/static/app/components/feedback/useMutateActivity.tsx +++ b/static/app/components/feedback/useMutateActivity.tsx @@ -9,10 +9,10 @@ import type {RequestError} from 'sentry/utils/requestError/requestError'; type TPayload = {activity: GroupActivity[]; note?: NoteType; noteId?: string}; type TMethod = 'PUT' | 'POST' | 'DELETE'; -export type TData = GroupActivity; -export type TError = RequestError; -export type TVariables = [TPayload, TMethod]; -export type TContext = unknown; +type TData = GroupActivity; +type TError = RequestError; +type TVariables = [TPayload, TMethod]; +type TContext = unknown; type DeleteCommentCallback = ( noteId: string, diff --git a/static/app/components/forms/formField/controlState.tsx b/static/app/components/forms/formField/controlState.tsx index 10b6fe05bae464..ec9f231cc519aa 100644 --- a/static/app/components/forms/formField/controlState.tsx +++ b/static/app/components/forms/formField/controlState.tsx @@ -2,7 +2,7 @@ import {Observer} from 'mobx-react-lite'; import {ControlState} from 'sentry/components/forms/fieldGroup/controlState'; import type {FormModel} from 'sentry/components/forms/model'; -import FormState from 'sentry/components/forms/state'; +import {FormState} from 'sentry/components/forms/state'; type Props = { model: FormModel; diff --git a/static/app/components/forms/formField/index.tsx b/static/app/components/forms/formField/index.tsx index 4b8967e5598415..7a76ed94968131 100644 --- a/static/app/components/forms/formField/index.tsx +++ b/static/app/components/forms/formField/index.tsx @@ -17,7 +17,7 @@ import type {FieldGroupProps} from 'sentry/components/forms/fieldGroup/types'; import {FormContext} from 'sentry/components/forms/formContext'; import type {FormModel} from 'sentry/components/forms/model'; import {MockModel} from 'sentry/components/forms/model'; -import FormState from 'sentry/components/forms/state'; +import {FormState} from 'sentry/components/forms/state'; import type {FieldValue} from 'sentry/components/forms/types'; import {PanelAlert} from 'sentry/components/panels/panelAlert'; import {t} from 'sentry/locale'; diff --git a/static/app/components/forms/model.tsx b/static/app/components/forms/model.tsx index e92ff45f9733a1..9a6efbfbc13204 100644 --- a/static/app/components/forms/model.tsx +++ b/static/app/components/forms/model.tsx @@ -5,7 +5,7 @@ import {action, computed, makeObservable, observable} from 'mobx'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; import {Client} from 'sentry/api'; import {addUndoableFormChangeMessage} from 'sentry/components/forms/formIndicators'; -import FormState from 'sentry/components/forms/state'; +import {FormState} from 'sentry/components/forms/state'; import {t} from 'sentry/locale'; import type {Choice} from 'sentry/types/core'; import {defined} from 'sentry/utils'; diff --git a/static/app/components/forms/state.tsx b/static/app/components/forms/state.tsx index 4602f21746327e..2ad48b6acf9cbe 100644 --- a/static/app/components/forms/state.tsx +++ b/static/app/components/forms/state.tsx @@ -1,4 +1,4 @@ -enum FormState { +export enum FormState { HOVER = 'Hover', DISABLED = 'Disabled', LOADING = 'Loading', @@ -7,5 +7,3 @@ enum FormState { ERROR = 'Error', INCOMPLETE = 'Incomplete', } - -export default FormState; diff --git a/static/app/components/globalDrawer/index.tsx b/static/app/components/globalDrawer/index.tsx index 271d527d9b322e..cc0b5c354aed76 100644 --- a/static/app/components/globalDrawer/index.tsx +++ b/static/app/components/globalDrawer/index.tsx @@ -13,7 +13,7 @@ import type {Location} from 'history'; import {useScrollLock} from '@sentry/scraps/useScrollLock'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {DrawerComponents} from 'sentry/components/globalDrawer/components'; import {t} from 'sentry/locale'; import {defined} from 'sentry/utils'; diff --git a/static/app/components/group/externalIssuesList/index.tsx b/static/app/components/group/externalIssuesList/index.tsx index ac0d0108a11e73..e4d42b65a56583 100644 --- a/static/app/components/group/externalIssuesList/index.tsx +++ b/static/app/components/group/externalIssuesList/index.tsx @@ -7,7 +7,7 @@ import {Tooltip} from '@sentry/scraps/tooltip'; import {DropdownButton} from 'sentry/components/dropdownButton'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import type {ExternalIssueAction} from 'sentry/components/group/externalIssuesList/hooks/types'; import {useGroupExternalIssues} from 'sentry/components/group/externalIssuesList/hooks/useGroupExternalIssues'; import {Placeholder} from 'sentry/components/placeholder'; diff --git a/static/app/components/groupHeaderRow.tsx b/static/app/components/groupHeaderRow.tsx index 8dd17a27fd8c8e..b7d8174b14afa0 100644 --- a/static/app/components/groupHeaderRow.tsx +++ b/static/app/components/groupHeaderRow.tsx @@ -4,7 +4,7 @@ import {useHover} from '@react-aria/interactions'; import {Link} from '@sentry/scraps/link'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {EventMessage} from 'sentry/components/events/eventMessage'; import {GroupTitle} from 'sentry/components/groupTitle'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; diff --git a/static/app/components/idBadge/index.tsx b/static/app/components/idBadge/index.tsx index eea81d80d035e3..c4c273addd5d3f 100644 --- a/static/app/components/idBadge/index.tsx +++ b/static/app/components/idBadge/index.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; -import ErrorBoundary from 'sentry/components/errorBoundary'; +import {ErrorBoundary} from 'sentry/components/errorBoundary'; import type {GetBadgeProps} from './getBadge'; import {getBadge} from './getBadge'; diff --git a/static/app/components/layouts/thirds.tsx b/static/app/components/layouts/thirds.tsx index 39d78237619e95..906ccffcf23c5a 100644 --- a/static/app/components/layouts/thirds.tsx +++ b/static/app/components/layouts/thirds.tsx @@ -5,6 +5,7 @@ import styled from '@emotion/styled'; import {Container, Stack, type FlexProps} from '@sentry/scraps/layout'; import {Tabs} from '@sentry/scraps/tabs'; +import {usePrimaryNavigation} from 'sentry/views/navigation/primaryNavigationContext'; import {SecondaryNavigationContext} from 'sentry/views/navigation/secondaryNavigationContext'; import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFeature'; @@ -13,6 +14,7 @@ import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFea */ export function Page(props: FlexProps<'main'> & {withPadding?: boolean}) { const hasPageFrame = useHasPageFrameFeature(); + const primaryNavigation = usePrimaryNavigation(); const secondaryNavigation = useContext(SecondaryNavigationContext); const {withPadding, ...rest} = props; @@ -22,10 +24,29 @@ export function Page(props: FlexProps<'main'> & {withPadding?: boolean}) { @@ -37,9 +58,9 @@ export function Page(props: FlexProps<'main'> & {withPadding?: boolean}) { ); } -const StyledPageFrameStack = styled(Stack)` +const StyledPageFrameStack = styled(Stack)<{roundedCorner: boolean}>` > :first-child { - border-top-left-radius: ${p => p.theme.radius.lg}; + border-top-left-radius: ${p => (p.roundedCorner ? p.theme.radius.lg : undefined)}; } `; diff --git a/static/app/components/links/externalLink.tsx b/static/app/components/links/externalLink.tsx index 12e1ceb35f03f4..2a1b9bcf34c86b 100644 --- a/static/app/components/links/externalLink.tsx +++ b/static/app/components/links/externalLink.tsx @@ -1,6 +1,8 @@ import {ExternalLink} from '@sentry/scraps/link'; -/** - * @deprecated Use `ExternalLink` from `@sentry/scraps/link` instead. - */ -export default ExternalLink; +export { + /** + * @deprecated Use `ExternalLink` from `@sentry/scraps/link` instead. + */ + ExternalLink, +}; diff --git a/static/app/components/modals/dataWidgetViewerModal.spec.tsx b/static/app/components/modals/dataWidgetViewerModal.spec.tsx index 87a102ef2c42f9..9c6b9f841921a1 100644 --- a/static/app/components/modals/dataWidgetViewerModal.spec.tsx +++ b/static/app/components/modals/dataWidgetViewerModal.spec.tsx @@ -24,7 +24,7 @@ import {ProjectsStore} from 'sentry/stores/projectsStore'; import type {DashboardFilters, Widget, WidgetQuery} from 'sentry/views/dashboards/types'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import {performanceScoreTooltip} from 'sentry/views/dashboards/utils'; -import WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState'; +import {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState'; jest.mock('echarts-for-react/lib/core', () => { return jest.fn(({style}) => { diff --git a/static/app/components/modals/dataWidgetViewerModal.tsx b/static/app/components/modals/dataWidgetViewerModal.tsx index 13a1c9f18d41cc..d759c0c0f782aa 100644 --- a/static/app/components/modals/dataWidgetViewerModal.tsx +++ b/static/app/components/modals/dataWidgetViewerModal.tsx @@ -31,8 +31,7 @@ import {defined} from 'sentry/utils'; import {CAN_MARK, trackAnalytics} from 'sentry/utils/analytics'; import {getUtcDateString} from 'sentry/utils/dates'; import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery'; -import type EventView from 'sentry/utils/discover/eventView'; -import type {MetaType} from 'sentry/utils/discover/eventView'; +import type {EventView, MetaType} from 'sentry/utils/discover/eventView'; import type {RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers'; import type {Sort} from 'sentry/utils/discover/fields'; import { @@ -105,7 +104,7 @@ import {ReleaseWidgetQueries} from 'sentry/views/dashboards/widgetCard/releaseWi import {VisualizationWidget} from 'sentry/views/dashboards/widgetCard/visualizationWidget'; import {WidgetCardChartContainer} from 'sentry/views/dashboards/widgetCard/widgetCardChartContainer'; import {WidgetQueries} from 'sentry/views/dashboards/widgetCard/widgetQueries'; -import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState'; +import type {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState'; import {AgentsTracesTableWidgetVisualization} from 'sentry/views/dashboards/widgets/agentsTracesTableWidget/agentsTracesTableWidgetVisualization'; import {ALLOWED_CELL_ACTIONS} from 'sentry/views/dashboards/widgets/common/settings'; import {TableWidgetVisualization} from 'sentry/views/dashboards/widgets/tableWidget/tableWidgetVisualization'; diff --git a/static/app/components/modals/featureTourModal.spec.tsx b/static/app/components/modals/featureTourModal.spec.tsx index 862b279e1dee7e..f74651fe510eef 100644 --- a/static/app/components/modals/featureTourModal.spec.tsx +++ b/static/app/components/modals/featureTourModal.spec.tsx @@ -3,7 +3,7 @@ import {Fragment} from 'react'; import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import {GlobalModal} from 'sentry/components/globalModal'; -import FeatureTourModal from 'sentry/components/modals/featureTourModal'; +import {FeatureTourModal} from 'sentry/components/modals/featureTourModal'; const steps = [ { diff --git a/static/app/components/modals/featureTourModal.tsx b/static/app/components/modals/featureTourModal.tsx index 66518ef59f0e84..3394909dcc4ece 100644 --- a/static/app/components/modals/featureTourModal.tsx +++ b/static/app/components/modals/featureTourModal.tsx @@ -72,7 +72,7 @@ const defaultProps = { * trigger re-renders in the modal contents. This requires a bit of duplicate state * to be managed around the current step. */ -class FeatureTourModal extends Component { +export class FeatureTourModal extends Component { static defaultProps = defaultProps; state: State = { @@ -122,8 +122,6 @@ class FeatureTourModal extends Component { } } -export default FeatureTourModal; - type ContentsProps = ModalRenderProps & Pick & Pick; diff --git a/static/app/components/modals/generateDashboardFromSeerModal.tsx b/static/app/components/modals/generateDashboardFromSeerModal.tsx deleted file mode 100644 index 93d4d1da6b5fc4..00000000000000 --- a/static/app/components/modals/generateDashboardFromSeerModal.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import {Fragment, useCallback, useState} from 'react'; -import type {Location} from 'history'; - -import {Button} from '@sentry/scraps/button'; -import {Flex} from '@sentry/scraps/layout'; -import {TextArea} from '@sentry/scraps/textarea'; - -import {addErrorMessage} from 'sentry/actionCreators/indicator'; -import type {ModalRenderProps} from 'sentry/actionCreators/modal'; -import {IconSeer} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import type {Organization} from 'sentry/types/organization'; -import {getApiUrl} from 'sentry/utils/api/getApiUrl'; -import {fetchMutation} from 'sentry/utils/queryClient'; -import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; -import type {ReactRouter3Navigate} from 'sentry/utils/useNavigate'; - -export interface GenerateDashboardFromSeerModalProps { - location: Location; - navigate: ReactRouter3Navigate; - organization: Organization; -} - -function GenerateDashboardFromSeerModal({ - Header, - Body, - Footer, - closeModal, - organization, - location, - navigate, -}: ModalRenderProps & GenerateDashboardFromSeerModalProps) { - const [prompt, setPrompt] = useState(''); - const [isGenerating, setIsGenerating] = useState(false); - - const handleGenerate = useCallback(async () => { - if (!prompt.trim()) { - return; - } - - setIsGenerating(true); - - try { - const url = getApiUrl('/organizations/$organizationIdOrSlug/dashboards/generate/', { - path: { - organizationIdOrSlug: organization.slug, - }, - }); - const response = await fetchMutation<{run_id: string}>({ - url, - method: 'POST', - data: {prompt: prompt.trim()}, - }); - - const runId = response.run_id; - if (!runId) { - addErrorMessage(t('Failed to start dashboard generation')); - setIsGenerating(false); - return; - } - - closeModal(); - - navigate( - normalizeUrl({ - pathname: `/organizations/${organization.slug}/dashboards/new/from-seer/`, - query: {...location.query, seerRunId: String(runId)}, - }) - ); - } catch (error) { - setIsGenerating(false); - addErrorMessage(t('Failed to start dashboard generation')); - } - }, [prompt, organization.slug, location.query, closeModal, navigate]); - - return ( - -
-

{t('Create Dashboard with Agent')}

-
- -

{t('Describe the dashboard you would like to be generated for you.')}

-