diff --git a/jest.config.ts b/jest.config.ts index 81bfa61527463d..03c34f6c609b69 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -323,15 +323,7 @@ const config: Config.InitialOptions = { '/tests/js/setupFramework.ts', ], testMatch: testMatch || ['/(static|tests/js)/**/?(*.)+(spec|test).[jt]s?(x)'], - testPathIgnorePatterns: [ - '/tests/sentry/lang/javascript/', - // ESM-style helper scripts (e.g. scripts/genPlatformProductInfo.ts use - // `const __dirname = path.dirname(fileURLToPath(import.meta.url))`) that - // SWC's CJS transform redeclares — collides with Node's module wrapper. - // None of these are tests; keep them out of Jest's discovery entirely. - '/scripts/', - ], - modulePathIgnorePatterns: ['/scripts/'], + testPathIgnorePatterns: ['/tests/sentry/lang/javascript/'], unmockedModulePathPatterns: [ '/node_modules/react', diff --git a/pyproject.toml b/pyproject.toml index e062d8d96393cd..d134a0e40b7eef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,7 +93,7 @@ dependencies = [ "sentry-ophio>=1.1.3", # sentry-options is only used in getsentry for now "sentry-options>=1.0.13", - "sentry-protos>=0.16.1", + "sentry-protos>=0.17.0", "sentry-redis-tools>=0.5.0", "sentry-relay>=0.9.27", "sentry-scm==0.16.0", diff --git a/scripts/extractFormFields.ts b/scripts/extractFormFields.ts index 736b94314ff6b0..da0385aeb58363 100644 --- a/scripts/extractFormFields.ts +++ b/scripts/extractFormFields.ts @@ -11,8 +11,14 @@ import {fileURLToPath} from 'node:url'; import * as ts from 'typescript'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +// Named THIS_FILE / THIS_DIR rather than the conventional __filename / +// __dirname so SWC's CJS transform (used by Jest) doesn't emit a +// `const __dirname = ...` that collides with the wrapper-provided +// binding and crashes the frontend test run with a SyntaxError. +// Behavior-equivalent at runtime — the originals only shadowed the +// wrapper inside this module. +const THIS_FILE = fileURLToPath(import.meta.url); +const THIS_DIR = path.dirname(THIS_FILE); interface ExtractedField { formId: string; @@ -480,14 +486,14 @@ ${registryEntries} // Main execution try { - const configPath = path.join(__dirname, '../tsconfig.json'); + const configPath = path.join(THIS_DIR, '../tsconfig.json'); const extractor = new FormFieldExtractor(configPath); console.log('🔍 Extracting form fields from TypeScript files...'); const fields = extractor.extractAllFields(); const outputPath = path.join( - __dirname, + THIS_DIR, '../static/app/components/core/form/generatedFieldRegistry.ts' ); diff --git a/scripts/genPlatformProductInfo.ts b/scripts/genPlatformProductInfo.ts index 16b31ac937ca7f..b91cb215cb7b36 100644 --- a/scripts/genPlatformProductInfo.ts +++ b/scripts/genPlatformProductInfo.ts @@ -26,8 +26,13 @@ import {fileURLToPath} from 'node:url'; import * as ts from 'typescript'; import {parse as parseYaml} from 'yaml'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const SENTRY_ROOT = path.resolve(__dirname, '..'); +// Named THIS_DIR rather than __dirname so SWC's CJS transform (used by Jest) +// doesn't emit a `const __dirname = ...` that collides with the wrapper- +// provided binding and crashes the whole frontend test run with a +// SyntaxError. The original `__dirname` only shadowed the wrapper inside +// this module anyway, so the rename is behavior-equivalent at runtime. +const THIS_DIR = path.dirname(fileURLToPath(import.meta.url)); +const SENTRY_ROOT = path.resolve(THIS_DIR, '..'); const DOCS_ROOT = process.env.SENTRY_DOCS_PATH ? path.resolve(process.env.SENTRY_DOCS_PATH) : path.resolve(SENTRY_ROOT, '..', 'sentry-docs'); diff --git a/src/sentry/api/endpoints/organization_project_keys.py b/src/sentry/api/endpoints/organization_project_keys.py index 669c09d9d5aa6f..6f35048ed61282 100644 --- a/src/sentry/api/endpoints/organization_project_keys.py +++ b/src/sentry/api/endpoints/organization_project_keys.py @@ -80,7 +80,7 @@ class OrganizationProjectKeysEndpoint(OrganizationEndpoint): ), ], ) - def get(self, request: Request, organization) -> Response: + def get(self, request: Request, organization) -> Response[list[ProjectKeySerializerResponse]]: """ Return a list of client keys (DSNs) for all projects in an organization. diff --git a/src/sentry/api/endpoints/organization_sampling_effective_sample_rate.py b/src/sentry/api/endpoints/organization_sampling_effective_sample_rate.py index 8996d35187b4ae..701420aafd244c 100644 --- a/src/sentry/api/endpoints/organization_sampling_effective_sample_rate.py +++ b/src/sentry/api/endpoints/organization_sampling_effective_sample_rate.py @@ -52,7 +52,9 @@ class OrganizationSamplingEffectiveSampleRateEndpoint(OrganizationEndpoint): 404: RESPONSE_NOT_FOUND, }, ) - def get(self, request: Request, organization: Organization) -> Response: + def get( + self, request: Request, organization: Organization + ) -> Response[OrganizationSamplingEffectiveSampleRateResponse]: if not features.has("organizations:dynamic-sampling", organization, actor=request.user): raise ResourceDoesNotExist diff --git a/src/sentry/api/endpoints/organization_sessions.py b/src/sentry/api/endpoints/organization_sessions.py index 093d65a6628262..10a31e290cdc94 100644 --- a/src/sentry/api/endpoints/organization_sessions.py +++ b/src/sentry/api/endpoints/organization_sessions.py @@ -63,7 +63,7 @@ class OrganizationSessionsEndpoint(OrganizationEndpoint): }, examples=SessionExamples.QUERY_SESSIONS, ) - def get(self, request: Request, organization: Organization) -> Response: + def get(self, request: Request, organization: Organization) -> Response[SessionsQueryResult]: """ Returns a time series of release health session statistics for projects bound to an organization. diff --git a/src/sentry/api/endpoints/project_rules.py b/src/sentry/api/endpoints/project_rules.py index 5feb3453fd882b..05f1612e014ab3 100644 --- a/src/sentry/api/endpoints/project_rules.py +++ b/src/sentry/api/endpoints/project_rules.py @@ -851,7 +851,7 @@ class ProjectRulesEndpoint(ProjectEndpoint): @deprecated( ALERTS_API_DEPRECATION_DATE, suggested_api="sentry-api-0-organization-workflow-index" ) - def get(self, request: Request, project: Project) -> Response: + def get(self, request: Request, project: Project) -> Response[list[RuleSerializerResponse]]: """ ## Deprecated 🚧 Use [Fetch an Organization's Monitors](/api/monitors/fetch-an-organizations-monitors) and [Fetch Alerts](/api/monitors/fetch-alerts) instead. diff --git a/src/sentry/core/endpoints/organization_index.py b/src/sentry/core/endpoints/organization_index.py index 1983f32c9bf9bd..4b93ec21270c7c 100644 --- a/src/sentry/core/endpoints/organization_index.py +++ b/src/sentry/core/endpoints/organization_index.py @@ -138,7 +138,7 @@ class OrganizationIndexEndpoint(Endpoint): }, examples=UserExamples.LIST_ORGANIZATIONS, ) - def get(self, request: Request) -> Response: + def get(self, request: Request) -> Response[list[OrganizationSummarySerializerResponse]]: """ Return a list of organizations available to the authenticated session in a region. This is particularly useful for requests with a user bound context. For API key-based requests this will only return the organization that belongs to the key. diff --git a/src/sentry/core/endpoints/organization_member_index.py b/src/sentry/core/endpoints/organization_member_index.py index 700e8ca7d4e6bd..9d9909f51c5c31 100644 --- a/src/sentry/core/endpoints/organization_member_index.py +++ b/src/sentry/core/endpoints/organization_member_index.py @@ -203,7 +203,9 @@ class OrganizationMemberIndexEndpoint(OrganizationEndpoint): }, examples=OrganizationMemberExamples.LIST_ORG_MEMBERS, ) - def get(self, request: Request, organization: Organization) -> Response: + def get( + self, request: Request, organization: Organization + ) -> Response[list[OrganizationMemberResponse]]: """ List all organization members. diff --git a/src/sentry/core/endpoints/organization_teams.py b/src/sentry/core/endpoints/organization_teams.py index 667f919f3fe004..873ecff91aacd6 100644 --- a/src/sentry/core/endpoints/organization_teams.py +++ b/src/sentry/core/endpoints/organization_teams.py @@ -96,7 +96,9 @@ def team_serializer_for_post(self): }, examples=TeamExamples.LIST_ORG_TEAMS, ) - def get(self, request: Request, organization: Organization) -> Response: + def get( + self, request: Request, organization: Organization + ) -> Response[list[TeamSerializerResponse]]: """ Returns a list of teams bound to a organization. """ diff --git a/src/sentry/core/endpoints/project_keys.py b/src/sentry/core/endpoints/project_keys.py index 7b9b67ac400a9b..66a694918546f9 100644 --- a/src/sentry/core/endpoints/project_keys.py +++ b/src/sentry/core/endpoints/project_keys.py @@ -65,7 +65,7 @@ class ProjectKeysEndpoint(ProjectEndpoint): }, examples=ProjectExamples.LIST_CLIENT_KEYS, ) - def get(self, request: Request, project) -> Response: + def get(self, request: Request, project) -> Response[list[ProjectKeySerializerResponse]]: """ Return a list of client keys bound to a project. """ diff --git a/src/sentry/core/endpoints/project_teams.py b/src/sentry/core/endpoints/project_teams.py index febed18dd0748c..4a9f49bb41f125 100644 --- a/src/sentry/core/endpoints/project_teams.py +++ b/src/sentry/core/endpoints/project_teams.py @@ -37,7 +37,7 @@ class ProjectTeamsEndpoint(ProjectEndpoint): }, examples=TeamExamples.LIST_PROJECT_TEAMS, ) - def get(self, request: Request, project) -> Response: + def get(self, request: Request, project) -> Response[list[BaseTeamSerializerResponse]]: """ Return a list of teams that have access to this project. """ diff --git a/src/sentry/core/endpoints/scim/members.py b/src/sentry/core/endpoints/scim/members.py index ff6fa7552d7e9c..29fa1f0ac7fb5e 100644 --- a/src/sentry/core/endpoints/scim/members.py +++ b/src/sentry/core/endpoints/scim/members.py @@ -486,7 +486,9 @@ class OrganizationSCIMMemberIndex(SCIMEndpoint): }, examples=SCIMExamples.LIST_ORG_MEMBERS, ) - def get(self, request: Request, organization: Organization) -> Response: + def get( + self, request: Request, organization: Organization + ) -> Response[SCIMListMembersResponse]: """ Returns a paginated list of members bound to a organization with a SCIM Users GET Request. """ diff --git a/src/sentry/core/endpoints/scim/teams.py b/src/sentry/core/endpoints/scim/teams.py index 976aa8e8f6b844..b5bd8fb339cd18 100644 --- a/src/sentry/core/endpoints/scim/teams.py +++ b/src/sentry/core/endpoints/scim/teams.py @@ -198,7 +198,9 @@ class OrganizationSCIMTeamIndex(SCIMEndpoint): }, examples=SCIMExamples.LIST_ORG_PAGINATED_TEAMS, ) - def get(self, request: Request, organization: Organization, **kwds: Any) -> Response: + def get( + self, request: Request, organization: Organization, **kwds: Any + ) -> Response[SCIMListTeamsResponse]: """ Returns a paginated list of teams bound to a organization with a SCIM Groups GET Request. diff --git a/src/sentry/core/endpoints/team_members.py b/src/sentry/core/endpoints/team_members.py index 4656ad8ecf33a3..11ffb69b78b372 100644 --- a/src/sentry/core/endpoints/team_members.py +++ b/src/sentry/core/endpoints/team_members.py @@ -83,7 +83,7 @@ class TeamMembersEndpoint(TeamEndpoint): }, examples=TeamExamples.LIST_TEAM_MEMBERS, ) - def get(self, request: Request, team) -> Response: + def get(self, request: Request, team) -> Response[list[OrganizationMemberOnTeamResponse]]: """ List all members on a team. diff --git a/src/sentry/dashboards/endpoints/organization_dashboards.py b/src/sentry/dashboards/endpoints/organization_dashboards.py index e2a0d9a89ec5a9..eb77ef319c6572 100644 --- a/src/sentry/dashboards/endpoints/organization_dashboards.py +++ b/src/sentry/dashboards/endpoints/organization_dashboards.py @@ -359,7 +359,9 @@ class OrganizationDashboardsEndpoint(OrganizationEndpoint): }, examples=DashboardExamples.DASHBOARDS_QUERY_RESPONSE, ) - def get(self, request: Request, organization: Organization) -> Response: + def get( + self, request: Request, organization: Organization + ) -> Response[list[DashboardListResponse]]: """ Retrieve a list of custom dashboards that are associated with the given organization. """ diff --git a/src/sentry/explore/endpoints/explore_saved_queries.py b/src/sentry/explore/endpoints/explore_saved_queries.py index b3c239c988a76b..ca5f27eaa74c11 100644 --- a/src/sentry/explore/endpoints/explore_saved_queries.py +++ b/src/sentry/explore/endpoints/explore_saved_queries.py @@ -345,7 +345,9 @@ def has_feature(self, organization, request): }, examples=ExploreExamples.EXPLORE_SAVED_QUERIES_QUERY_RESPONSE, ) - def get(self, request: Request, organization: Organization) -> Response: + def get( + self, request: Request, organization: Organization + ) -> Response[list[ExploreSavedQueryResponse]]: """ Retrieve a list of saved queries that are associated with the given organization. """ diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index d3319439e0c888..c39c63008d1929 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -307,7 +307,6 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:search-query-attribute-validation", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable search query builder raw search replacement manager.add("organizations:search-query-builder-raw-search-replacement", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - manager.add("organizations:seer-agent-pr-consolidation", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Disables the enableSeerCoding setting, preventing orgs from changing code generation behavior manager.add("organizations:seer-disable-coding-setting", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable GitLab as a supported SCM provider for Seer diff --git a/src/sentry/integrations/api/endpoints/data_forwarding_details.py b/src/sentry/integrations/api/endpoints/data_forwarding_details.py index 19aa74d2fd2615..478e981a2b87d6 100644 --- a/src/sentry/integrations/api/endpoints/data_forwarding_details.py +++ b/src/sentry/integrations/api/endpoints/data_forwarding_details.py @@ -299,7 +299,7 @@ def _update_single_project_configuration( ) def put( self, request: Request, organization: Organization, data_forwarder: DataForwarder - ) -> Response: + ) -> Response[DataForwarderResponse]: """ Updates a data forwarder for an organization or update a project-specific override. Updates to the data forwarder's configuration require `org:write` permissions, and the entire diff --git a/src/sentry/integrations/api/endpoints/data_forwarding_index.py b/src/sentry/integrations/api/endpoints/data_forwarding_index.py index 6fc5d2501ef9aa..df3c673e8f2148 100644 --- a/src/sentry/integrations/api/endpoints/data_forwarding_index.py +++ b/src/sentry/integrations/api/endpoints/data_forwarding_index.py @@ -53,7 +53,7 @@ class DataForwardingIndexEndpoint(OrganizationEndpoint): ) @set_referrer_policy("strict-origin-when-cross-origin") @method_decorator(never_cache) - def get(self, request: Request, organization) -> Response: + def get(self, request: Request, organization) -> Response[list[DataForwarderResponse]]: """ Returns a list of data forwarders for an organization. """ @@ -79,7 +79,7 @@ def get(self, request: Request, organization) -> Response: ) @set_referrer_policy("strict-origin-when-cross-origin") @method_decorator(never_cache) - def post(self, request: Request, organization) -> Response: + def post(self, request: Request, organization) -> Response[DataForwarderResponse]: """ Creates a new data forwarder for an organization. Only one data forwarder can be created per provider for a given organization. diff --git a/src/sentry/integrations/api/endpoints/organization_integration_details.py b/src/sentry/integrations/api/endpoints/organization_integration_details.py index 8e07a8b309a496..9145d17c4e65d8 100644 --- a/src/sentry/integrations/api/endpoints/organization_integration_details.py +++ b/src/sentry/integrations/api/endpoints/organization_integration_details.py @@ -73,7 +73,7 @@ def get( organization_context: RpcUserOrganizationContext, integration_id: int, **kwds: Any, - ) -> Response: + ) -> Response[OrganizationIntegrationResponse]: org_integration = self.get_organization_integration( organization_context.organization.id, integration_id ) @@ -153,7 +153,7 @@ def post( installation.update_organization_config(request.data) except (IntegrationError, ApiError) as e: sentry_sdk.capture_exception(e) - return self.respond({"detail": [str(e)]}, status=400) + return self.respond({"detail": str(e)}, status=400) self.create_audit_entry( request=request, diff --git a/src/sentry/integrations/api/endpoints/organization_repository_commits.py b/src/sentry/integrations/api/endpoints/organization_repository_commits.py index bcf0e8338c9afb..17a7b34f166d4d 100644 --- a/src/sentry/integrations/api/endpoints/organization_repository_commits.py +++ b/src/sentry/integrations/api/endpoints/organization_repository_commits.py @@ -69,7 +69,9 @@ class OrganizationRepositoryCommitsEndpoint(OrganizationEndpoint): ), ], ) - def get(self, request: Request, organization, repo_id) -> Response: + def get( + self, request: Request, organization, repo_id + ) -> Response[list[CommitSerializerResponse]]: """ List a Repository's Commits """ diff --git a/src/sentry/issues/endpoints/group_tagkey_values.py b/src/sentry/issues/endpoints/group_tagkey_values.py index 623c3f05d8962c..0a5ad35d11a09d 100644 --- a/src/sentry/issues/endpoints/group_tagkey_values.py +++ b/src/sentry/issues/endpoints/group_tagkey_values.py @@ -70,7 +70,7 @@ class GroupTagKeyValuesEndpoint(GroupEndpoint): examples=[TagsExamples.GROUP_TAGKEY_VALUES], ) @deprecated(CELL_API_DEPRECATION_DATE, url_names=["sentry-api-0-group-tag-key-values"]) - def get(self, request: Request, group, key) -> Response: + def get(self, request: Request, group, key) -> Response[list[TagValueSerializerResponse]]: """ List a Tag's Values """ diff --git a/src/sentry/issues/endpoints/organization_shortid.py b/src/sentry/issues/endpoints/organization_shortid.py index 1c7ae5dca9d887..f6267068437d6c 100644 --- a/src/sentry/issues/endpoints/organization_shortid.py +++ b/src/sentry/issues/endpoints/organization_shortid.py @@ -118,7 +118,7 @@ class ShortIdLookupEndpoint(GroupEndpoint): ) ], ) - def get(self, request: Request, group: Group) -> Response: + def get(self, request: Request, group: Group) -> Response[ShortIdLookupResponse]: """ Resolve a short ID to the project slug and group details. """ diff --git a/src/sentry/issues/endpoints/project_events.py b/src/sentry/issues/endpoints/project_events.py index 8226c6e1c4d397..bef8fad012825d 100644 --- a/src/sentry/issues/endpoints/project_events.py +++ b/src/sentry/issues/endpoints/project_events.py @@ -66,7 +66,9 @@ class ProjectEventsEndpoint(ProjectEndpoint): }, examples=EventExamples.PROJECT_EVENTS_SIMPLE, ) - def get(self, request: Request, project: Project) -> Response: + def get( + self, request: Request, project: Project + ) -> Response[list[SimpleEventSerializerResponse]]: """ Return a list of events bound to a project. """ diff --git a/src/sentry/issues/endpoints/source_map_debug.py b/src/sentry/issues/endpoints/source_map_debug.py index 9d218d1fbc88c7..265b136838e122 100644 --- a/src/sentry/issues/endpoints/source_map_debug.py +++ b/src/sentry/issues/endpoints/source_map_debug.py @@ -52,7 +52,9 @@ class SourceMapDebugEndpoint(ProjectEndpoint): 404: RESPONSE_NOT_FOUND, }, ) - def get(self, request: Request, project: Project, event_id: str) -> Response: + def get( + self, request: Request, project: Project, event_id: str + ) -> Response[SourceMapProcessingResponse]: """ Return a list of source map errors for a given event. """ diff --git a/src/sentry/monitors/endpoints/organization_monitor_checkin_index.py b/src/sentry/monitors/endpoints/organization_monitor_checkin_index.py index 5faec913a87750..bbf57c6a8e7481 100644 --- a/src/sentry/monitors/endpoints/organization_monitor_checkin_index.py +++ b/src/sentry/monitors/endpoints/organization_monitor_checkin_index.py @@ -40,7 +40,9 @@ class OrganizationMonitorCheckInIndexEndpoint(MonitorEndpoint, MonitorCheckInMix 404: RESPONSE_NOT_FOUND, }, ) - def get(self, request: Request, organization, project, monitor) -> Response: + def get( + self, request: Request, organization, project, monitor + ) -> Response[list[MonitorCheckInSerializerResponse]]: """ Retrieve a list of check-ins for a monitor """ diff --git a/src/sentry/monitors/endpoints/organization_monitor_index.py b/src/sentry/monitors/endpoints/organization_monitor_index.py index 5e22408199994f..8ede95fe18e224 100644 --- a/src/sentry/monitors/endpoints/organization_monitor_index.py +++ b/src/sentry/monitors/endpoints/organization_monitor_index.py @@ -104,7 +104,9 @@ class OrganizationMonitorIndexEndpoint(OrganizationAlertRuleBaseEndpoint): 404: RESPONSE_NOT_FOUND, }, ) - def get(self, request: AuthenticatedHttpRequest, organization: Organization) -> Response: + def get( + self, request: AuthenticatedHttpRequest, organization: Organization + ) -> Response[list[MonitorSerializerResponse]]: """ Lists monitors, including nested monitor environments. May be filtered to a project or environment. """ @@ -309,7 +311,9 @@ def post(self, request: AuthenticatedHttpRequest, organization) -> Response: 404: RESPONSE_NOT_FOUND, }, ) - def put(self, request: AuthenticatedHttpRequest, organization) -> Response: + def put( + self, request: AuthenticatedHttpRequest, organization + ) -> Response[MonitorBulkEditResponse]: """ Bulk edit the muted and disabled status of a list of monitors determined by slug """ diff --git a/src/sentry/monitors/endpoints/organization_monitor_processing_errors_index.py b/src/sentry/monitors/endpoints/organization_monitor_processing_errors_index.py index 90a97f9d2d4326..c62a5aafa1087c 100644 --- a/src/sentry/monitors/endpoints/organization_monitor_processing_errors_index.py +++ b/src/sentry/monitors/endpoints/organization_monitor_processing_errors_index.py @@ -38,7 +38,9 @@ class OrganizationMonitorProcessingErrorsIndexEndpoint(OrganizationEndpoint): 404: RESPONSE_NOT_FOUND, }, ) - def get(self, request: AuthenticatedHttpRequest, organization: Organization) -> Response: + def get( + self, request: AuthenticatedHttpRequest, organization: Organization + ) -> Response[list[CheckinProcessingErrorData]]: """ Retrieves checkin processing errors for an organization """ diff --git a/src/sentry/monitors/endpoints/project_monitor_checkin_index.py b/src/sentry/monitors/endpoints/project_monitor_checkin_index.py index bfff9d3ff3cb92..0a457ff6e15f3d 100644 --- a/src/sentry/monitors/endpoints/project_monitor_checkin_index.py +++ b/src/sentry/monitors/endpoints/project_monitor_checkin_index.py @@ -40,7 +40,9 @@ class ProjectMonitorCheckInIndexEndpoint(ProjectMonitorEndpoint, MonitorCheckInM 404: RESPONSE_NOT_FOUND, }, ) - def get(self, request: Request, project, monitor) -> Response: + def get( + self, request: Request, project, monitor + ) -> Response[list[MonitorCheckInSerializerResponse]]: """ Retrieve a list of check-ins for a monitor """ diff --git a/src/sentry/monitors/endpoints/project_monitor_processing_errors_index.py b/src/sentry/monitors/endpoints/project_monitor_processing_errors_index.py index c892a0157b33e8..7d06dce19ef014 100644 --- a/src/sentry/monitors/endpoints/project_monitor_processing_errors_index.py +++ b/src/sentry/monitors/endpoints/project_monitor_processing_errors_index.py @@ -52,7 +52,9 @@ class ProjectMonitorProcessingErrorsIndexEndpoint(ProjectMonitorEndpoint): 404: RESPONSE_NOT_FOUND, }, ) - def get(self, request: AuthenticatedHttpRequest, project, monitor) -> Response: + def get( + self, request: AuthenticatedHttpRequest, project, monitor + ) -> Response[list[CheckinProcessingErrorData]]: """ Retrieves checkin processing errors for a monitor """ diff --git a/src/sentry/preprod/api/endpoints/public/project_preprod_build_distribution_latest.py b/src/sentry/preprod/api/endpoints/public/project_preprod_build_distribution_latest.py index 312dc1e8caa445..284859d61a1d65 100644 --- a/src/sentry/preprod/api/endpoints/public/project_preprod_build_distribution_latest.py +++ b/src/sentry/preprod/api/endpoints/public/project_preprod_build_distribution_latest.py @@ -124,7 +124,7 @@ def get( self, request: Request, project: Project, - ) -> Response: + ) -> Response[LatestInstallableBuildResponseDict]: """ Get the latest installable build for a project. diff --git a/src/sentry/preprod/api/endpoints/public/project_preprod_size_analysis_status_check_rules.py b/src/sentry/preprod/api/endpoints/public/project_preprod_size_analysis_status_check_rules.py index 0b5b69d70ce353..7d20792a8a5cd6 100644 --- a/src/sentry/preprod/api/endpoints/public/project_preprod_size_analysis_status_check_rules.py +++ b/src/sentry/preprod/api/endpoints/public/project_preprod_size_analysis_status_check_rules.py @@ -54,7 +54,9 @@ class ProjectPreprodSizeAnalysisStatusCheckRulesEndpoint(ProjectEndpoint): }, examples=PreprodExamples.GET_SIZE_STATUS_CHECK_RULES, ) - def get(self, request: Request, project: Project) -> Response: + def get( + self, request: Request, project: Project + ) -> Response[ProjectSizeStatusCheckRulesResponseDict]: r""" Retrieve the current Size Analysis status check rules configured for a project. diff --git a/src/sentry/preprod/api/endpoints/public/project_preprod_snapshot_status_check_rules.py b/src/sentry/preprod/api/endpoints/public/project_preprod_snapshot_status_check_rules.py index 1be8dba4765c4c..196fac265d79df 100644 --- a/src/sentry/preprod/api/endpoints/public/project_preprod_snapshot_status_check_rules.py +++ b/src/sentry/preprod/api/endpoints/public/project_preprod_snapshot_status_check_rules.py @@ -54,7 +54,9 @@ class ProjectPreprodSnapshotStatusCheckRulesEndpoint(ProjectEndpoint): }, examples=PreprodExamples.GET_SNAPSHOT_STATUS_CHECK_RULES, ) - def get(self, request: Request, project: Project) -> Response: + def get( + self, request: Request, project: Project + ) -> Response[ProjectSnapshotStatusCheckRulesResponseDict]: r""" Retrieve the current Snapshot status check rules configured for a project. diff --git a/src/sentry/replays/endpoints/organization_replay_selector_index.py b/src/sentry/replays/endpoints/organization_replay_selector_index.py index 0b95b6a6865292..5ca5c2d3ee70e4 100644 --- a/src/sentry/replays/endpoints/organization_replay_selector_index.py +++ b/src/sentry/replays/endpoints/organization_replay_selector_index.py @@ -108,7 +108,7 @@ def get_replay_filter_params(self, request, organization): }, examples=ReplayExamples.GET_SELECTORS, ) - def get(self, request: Request, organization: Organization) -> Response: + def get(self, request: Request, organization: Organization) -> Response[ReplaySelectorResponse]: """Return a list of selectors for a given organization.""" self.check_replay_access(request, organization) diff --git a/src/sentry/replays/endpoints/project_replay_clicks_index.py b/src/sentry/replays/endpoints/project_replay_clicks_index.py index af14b6908fdd32..8c78a970101bd7 100644 --- a/src/sentry/replays/endpoints/project_replay_clicks_index.py +++ b/src/sentry/replays/endpoints/project_replay_clicks_index.py @@ -80,7 +80,9 @@ class ProjectReplayClicksIndexEndpoint(ProjectReplayEndpoint): }, examples=ReplayExamples.GET_REPLAY_CLICKS, ) - def get(self, request: Request, project: Project, replay_id: str) -> Response: + def get( + self, request: Request, project: Project, replay_id: str + ) -> Response[ReplayClickResponse]: """Retrieve a collection of RRWeb DOM node-ids and the timestamp they were clicked.""" self.check_replay_access(request, project) diff --git a/src/sentry/replays/endpoints/project_replay_recording_segment_index.py b/src/sentry/replays/endpoints/project_replay_recording_segment_index.py index 98425aef25e0b7..edfbec3111b595 100644 --- a/src/sentry/replays/endpoints/project_replay_recording_segment_index.py +++ b/src/sentry/replays/endpoints/project_replay_recording_segment_index.py @@ -48,7 +48,9 @@ def __init__(self, **options) -> None: }, examples=ReplayExamples.GET_REPLAY_SEGMENTS, ) - def get(self, request: Request, project, replay_id: str) -> Response: + def get( + self, request: Request, project, replay_id: str + ) -> Response[list[list[dict[str, Any]]]]: """Return a collection of replay recording segments.""" self.check_replay_access(request, project) diff --git a/src/sentry/replays/endpoints/project_replay_viewed_by.py b/src/sentry/replays/endpoints/project_replay_viewed_by.py index 757f08d9565f51..f98e8a0d53ebad 100644 --- a/src/sentry/replays/endpoints/project_replay_viewed_by.py +++ b/src/sentry/replays/endpoints/project_replay_viewed_by.py @@ -51,7 +51,9 @@ class ProjectReplayViewedByEndpoint(ProjectReplayEndpoint): }, examples=ReplayExamples.GET_REPLAY_VIEWED_BY, ) - def get(self, request: Request, project: Project, replay_id: str) -> Response: + def get( + self, request: Request, project: Project, replay_id: str + ) -> Response[ReplayViewedByResponse]: """Return a list of users who have viewed a replay.""" self.check_replay_access(request, project) diff --git a/src/sentry/sentry_apps/api/endpoints/group_external_issues.py b/src/sentry/sentry_apps/api/endpoints/group_external_issues.py index 72aa123cfe810e..d0b454b1f8c03c 100644 --- a/src/sentry/sentry_apps/api/endpoints/group_external_issues.py +++ b/src/sentry/sentry_apps/api/endpoints/group_external_issues.py @@ -41,7 +41,9 @@ class GroupExternalIssuesEndpoint(GroupEndpoint): examples=SentryAppExamples.GET_PLATFORM_EXTERNAL_ISSUE, ) @deprecated(CELL_API_DEPRECATION_DATE, url_names=["sentry-api-0-group-external-issues"]) - def get(self, request: Request, group) -> Response: + def get( + self, request: Request, group + ) -> Response[list[PlatformExternalIssueSerializerResponse]]: """ Retrieve custom integration issue links for the given Sentry issue diff --git a/src/sentry/sentry_apps/api/endpoints/organization_sentry_apps.py b/src/sentry/sentry_apps/api/endpoints/organization_sentry_apps.py index dcd63a1d7fb4fa..9d70e2094f0bdf 100644 --- a/src/sentry/sentry_apps/api/endpoints/organization_sentry_apps.py +++ b/src/sentry/sentry_apps/api/endpoints/organization_sentry_apps.py @@ -46,7 +46,7 @@ def get( request: Request, organization_context: RpcUserOrganizationContext, organization: RpcOrganization, - ) -> Response: + ) -> Response[list[SentryAppSerializerResponse]]: """ Retrieve the custom integrations for an organization """ diff --git a/src/sentry/uptime/endpoints/organiation_uptime_alert_index.py b/src/sentry/uptime/endpoints/organiation_uptime_alert_index.py index 764d931081f25f..12195a80505b45 100644 --- a/src/sentry/uptime/endpoints/organiation_uptime_alert_index.py +++ b/src/sentry/uptime/endpoints/organiation_uptime_alert_index.py @@ -59,7 +59,9 @@ class OrganizationUptimeAlertIndexEndpoint(OrganizationEndpoint): 404: RESPONSE_NOT_FOUND, }, ) - def get(self, request: Request, organization: Organization) -> Response: + def get( + self, request: Request, organization: Organization + ) -> Response[list[UptimeDetectorSerializerResponse]]: """ Lists uptime alerts. May be filtered to a project or environment. """ diff --git a/src/sentry/workflow_engine/endpoints/organization_available_action_index.py b/src/sentry/workflow_engine/endpoints/organization_available_action_index.py index 3c71aaff2457ec..ecdaf0dc383319 100644 --- a/src/sentry/workflow_engine/endpoints/organization_available_action_index.py +++ b/src/sentry/workflow_engine/endpoints/organization_available_action_index.py @@ -66,7 +66,9 @@ class OrganizationAvailableActionIndexEndpoint(OrganizationEndpoint): 404: RESPONSE_NOT_FOUND, }, ) - def get(self, request: Request, organization: Organization) -> Response: + def get( + self, request: Request, organization: Organization + ) -> Response[list[ActionHandlerSerializerResponse]]: """ Returns a list of available actions for a given org """ diff --git a/src/sentry/workflow_engine/endpoints/organization_data_condition_index.py b/src/sentry/workflow_engine/endpoints/organization_data_condition_index.py index 7287dec3fd7e9b..147a95712bd33f 100644 --- a/src/sentry/workflow_engine/endpoints/organization_data_condition_index.py +++ b/src/sentry/workflow_engine/endpoints/organization_data_condition_index.py @@ -49,7 +49,9 @@ class OrganizationDataConditionIndexEndpoint(OrganizationEndpoint): 404: RESPONSE_NOT_FOUND, }, ) - def get(self, request: Request, organization: Organization) -> Response: + def get( + self, request: Request, organization: Organization + ) -> Response[list[DataConditionHandlerResponse]]: """ Returns a list of data conditions for a given org """ diff --git a/src/sentry/workflow_engine/endpoints/organization_detector_count.py b/src/sentry/workflow_engine/endpoints/organization_detector_count.py index 1ee678d06052fb..aad4207af64b91 100644 --- a/src/sentry/workflow_engine/endpoints/organization_detector_count.py +++ b/src/sentry/workflow_engine/endpoints/organization_detector_count.py @@ -46,7 +46,9 @@ class OrganizationDetectorCountEndpoint(OrganizationEndpoint): 403: RESPONSE_FORBIDDEN, }, ) - def get(self, request: AuthenticatedHttpRequest, organization: Organization) -> Response: + def get( + self, request: AuthenticatedHttpRequest, organization: Organization + ) -> Response[DetectorCountResponse]: """ Retrieves the count of detectors for an organization. """ diff --git a/src/sentry/workflow_engine/endpoints/organization_detector_types.py b/src/sentry/workflow_engine/endpoints/organization_detector_types.py index c4bf0b74d7891e..ca52deb896ce6c 100644 --- a/src/sentry/workflow_engine/endpoints/organization_detector_types.py +++ b/src/sentry/workflow_engine/endpoints/organization_detector_types.py @@ -39,7 +39,7 @@ class OrganizationDetectorTypeIndexEndpoint(OrganizationEndpoint): 404: RESPONSE_NOT_FOUND, }, ) - def get(self, request: Request, organization: Organization) -> Response: + def get(self, request: Request, organization: Organization) -> Response[list[str]]: """ Returns a list of detector types for a given org """ diff --git a/src/sentry/workflow_engine/endpoints/serializers/detector_serializer.py b/src/sentry/workflow_engine/endpoints/serializers/detector_serializer.py index 9923df83a01a2e..4b5e9806a987e6 100644 --- a/src/sentry/workflow_engine/endpoints/serializers/detector_serializer.py +++ b/src/sentry/workflow_engine/endpoints/serializers/detector_serializer.py @@ -3,6 +3,7 @@ from datetime import datetime from typing import Any, TypedDict +from django.db.models import OuterRef, Subquery from drf_spectacular.utils import extend_schema_serializer from sentry.api.serializers import Serializer, register, serialize @@ -98,14 +99,21 @@ def get_attrs( for mapping in alert_rule_mappings } - latest_detector_group_values = ( - DetectorGroup.objects.filter(detector__in=item_list) - .order_by("detector_id", "-date_added") - .distinct("detector_id") - .values_list("detector_id", "group_id") + # LIMIT 1 subquery, not DISTINCT ON: Postgres lacks skip scan, so + # DISTINCT ON reads all rows per detector before deduplicating. + # LIMIT 1 stops after one index probe per detector. + # Impact is _dramatic_ for high cardinality DetectorGroups like error Detectors. + latest_group_subquery = ( + DetectorGroup.objects.filter(detector_id=OuterRef("pk")) + .order_by("-date_added") + .values("group_id")[:1] ) latest_group_ids_by_detector_id = { - detector_id: group_id for detector_id, group_id in latest_detector_group_values + d.id: d.latest_group_id + for d in Detector.objects.filter(id__in=[item.id for item in item_list]).annotate( + latest_group_id=Subquery(latest_group_subquery) + ) + if d.latest_group_id is not None } project_ids = {item.project_id for item in item_list} latest_groups = list( diff --git a/static/gsAdmin/views/invoiceComparison.tsx b/static/gsAdmin/views/invoiceComparison.tsx index 4b7c55732a59c5..f4fbb264fe7428 100644 --- a/static/gsAdmin/views/invoiceComparison.tsx +++ b/static/gsAdmin/views/invoiceComparison.tsx @@ -44,9 +44,27 @@ type Summary = { row_count: number; start: string; truncated: boolean; + unmatched_invoice_count: number; + unmatched_invoice_pct: number; + unmatched_org_count: number; + unmatched_truncated: boolean; }; -type ComparisonResponse = {rows: Row[]; summary: Summary}; +type UnmatchedSide = 'legacy_only' | 'platform_only'; + +type UnmatchedRow = { + amount: number; + invoice_count: number; + organization_id: number; + organization_slug: string | null; + side: UnmatchedSide; +}; + +type ComparisonResponse = { + rows: Row[]; + summary: Summary; + unmatched: UnmatchedRow[]; +}; const STATUS_VARIANT: Record = { match: 'success', @@ -135,10 +153,12 @@ export function InvoiceComparison() { Per-org totals comparing legacy Invoice and shadow{' '} PlatformInvoice records generated in the selected window (filtered on date_added, your local time — converted to UTC on - submit). All invoices for an org are summed on each side and a count is shown in - parentheses. Orgs missing on one side appear as legacy_only or{' '} - platform_only. Sorted by absolute % delta (relative to legacy), - largest first — rows with no legacy baseline sort to the top as ∞. + submit). The Unmatched summary stat is the percent of invoices in + the window that belong to one-sided orgs (legacy-only + platform-only / total) — + zero means perfect parity. The first table compares orgs with invoices on{' '} + both sides, sorted by absolute % delta (relative to legacy), + largest first. The second table lists one-sided orgs by absolute amount so you can + spot-check the worst missing invoices.

@@ -203,7 +223,7 @@ export function InvoiceComparison() { Summary + + + Unmatched + + + {formatPercent(data.summary.unmatched_invoice_pct)} + + ({data.summary.unmatched_invoice_count} of{' '} + {data.summary.legacy_count + data.summary.platform_count}) + + + @@ -328,6 +360,57 @@ export function InvoiceComparison() { + + + + Unmatched orgs (one side only, sorted by |amount|) + {data.summary.unmatched_truncated && ( + + showing top {data.unmatched.length} of{' '} + {data.summary.unmatched_org_count} orgs + + )} + + + + + + + + Amount + Invoices + + + + {data.unmatched.length === 0 && ( + + + + )} + {data.unmatched.map(row => ( + + + + {formatDollars(row.amount)} + {row.invoice_count} + + ))} + +
OrganizationSide
+ No unmatched invoices in this range. +
+ {row.organization_slug ? ( + + {row.organization_slug} + + ) : ( + org#{row.organization_id} + )} + + {row.side} +
+
+
)} diff --git a/tests/sentry/integrations/api/endpoints/test_organization_integration_details.py b/tests/sentry/integrations/api/endpoints/test_organization_integration_details.py index e83060a37bf43f..69e4565d67c984 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_integration_details.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_integration_details.py @@ -89,7 +89,7 @@ def test_update_config_error(self) -> None: response = self.get_error_response( self.organization.slug, self.integration.id, **config, status_code=400 ) - assert response.data["detail"] == ["hello"] + assert response.data["detail"] == "hello" with patch.object( GitlabIntegration, @@ -99,7 +99,7 @@ def test_update_config_error(self) -> None: response = self.get_error_response( self.organization.slug, self.integration.id, **config, status_code=400 ) - assert response.data["detail"] == ["hi"] + assert response.data["detail"] == "hi" @control_silo_test diff --git a/uv.lock b/uv.lock index 5c0800cd5699cb..31e2079b6e9048 100644 --- a/uv.lock +++ b/uv.lock @@ -2412,7 +2412,7 @@ requires-dist = [ { name = "sentry-kafka-schemas", specifier = ">=2.1.27" }, { name = "sentry-ophio", specifier = ">=1.1.3" }, { name = "sentry-options", specifier = ">=1.0.13" }, - { name = "sentry-protos", specifier = ">=0.16.1" }, + { name = "sentry-protos", specifier = ">=0.17.0" }, { name = "sentry-redis-tools", specifier = ">=0.5.0" }, { name = "sentry-relay", specifier = ">=0.9.27" }, { name = "sentry-scm", specifier = "==0.16.0" }, @@ -2596,7 +2596,7 @@ wheels = [ [[package]] name = "sentry-protos" -version = "0.16.1" +version = "0.17.0" source = { registry = "https://pypi.devinfra.sentry.io/simple" } dependencies = [ { name = "grpc-stubs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2604,7 +2604,7 @@ dependencies = [ { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] wheels = [ - { url = "https://pypi.devinfra.sentry.io/wheels/sentry_protos-0.16.1-py3-none-any.whl", hash = "sha256:755a7cc71a0d8bef2a42a340cd1c35e2ee127e20dd71fed334d9fa88c0cb87a4" }, + { url = "https://pypi.devinfra.sentry.io/wheels/sentry_protos-0.17.0-py3-none-any.whl", hash = "sha256:e5a7c320678a6204cfa6e8a8981952e8dd1080131de076c5cec8702a24f62d7d" }, ] [[package]]