diff --git a/.agents/skills/hybrid-cloud-outboxes/SKILL.md b/.agents/skills/hybrid-cloud-outboxes/SKILL.md index 798034c0653b88..d564cb88638429 100644 --- a/.agents/skills/hybrid-cloud-outboxes/SKILL.md +++ b/.agents/skills/hybrid-cloud-outboxes/SKILL.md @@ -18,11 +18,11 @@ description: >- Sentry uses a **transactional outbox pattern** for eventually consistent operations. When a model changes, an outbox row is written inside the same database transaction. After the transaction commits, the outbox is drained — firing a signal that triggers side effects such as RPC calls, tombstone propagation, or audit logging. -The most common use case is **cross-silo data replication**: a model saved in the Region silo produces a `RegionOutbox` that, when processed, replicates data to the Control silo (or vice versa via `ControlOutbox`). But the pattern is general — outboxes work for any operation that should happen reliably after a transaction commits, even within a single silo. +The most common use case is **cross-silo data replication**: a model saved in the Cell silo produces a `CellOutbox` that, when processed, replicates data to the Control silo (or vice versa via `ControlOutbox`). But the pattern is general — outboxes work for any operation that should happen reliably after a transaction commits, even within a single silo. There are two outbox types corresponding to the two directions of flow: -- **`RegionOutbox`** — written in a Cell silo, processed in the Cell silo to push data toward Control (via RPC calls in signal receivers). +- **`CellOutbox`** — written in a Cell silo, processed in the Cell silo to push data toward Control (via RPC calls in signal receivers). - **`ControlOutbox`** — written in the Control silo, processed in the Control silo to push data toward one or more Cell silos. Each `ControlOutbox` row targets a specific `cell_name`. ## Critical Constraints @@ -66,7 +66,7 @@ There are two outbox types corresponding to the two directions of flow: | Data lives in... | Replicates toward... | Mixin | Outbox type | | ---------------- | -------------------- | ------------------------ | --------------- | -| Cell silo | Control silo | `ReplicatedCellModel` | `RegionOutbox` | +| Cell silo | Control silo | `ReplicatedCellModel` | `CellOutbox` | | Control silo | Cell silo(s) | `ReplicatedControlModel` | `ControlOutbox` | ### 2.2 `ReplicatedCellModel` Template @@ -148,7 +148,7 @@ class MyModel(ReplicatedCellModel): ### 2.3 `ReplicatedControlModel` Template -Use this when a Control model needs to replicate data to Region silo(s). The key difference: Control outboxes fan out to one or more cells, so the model must declare which cells to target. +Use this when a Control model needs to replicate data to Cell silo(s). The key difference: Control outboxes fan out to one or more cells, so the model must declare which cells to target. ```python from sentry.db.models import control_silo_model @@ -360,7 +360,7 @@ def test_idempotent_replication(self): ### 7.3 Silo Test Decorators -- Use **`@cell_silo_test`** for tests focused on `RegionOutbox` creation +- Use **`@cell_silo_test`** for tests focused on `CellOutbox` creation - Use **`@control_silo_test`** for tests focused on `ControlOutbox` creation - Use **`@all_silo_test`** for end-to-end replication tests that exercise both silos - Only use **`TransactionTestCase`** for threading/concurrency tests (e.g., `threading.Barrier`), not for standard outbox drain tests diff --git a/.agents/skills/hybrid-cloud-outboxes/references/backfill.md b/.agents/skills/hybrid-cloud-outboxes/references/backfill.md index a94063da37b44f..ce31ba30446d0a 100644 --- a/.agents/skills/hybrid-cloud-outboxes/references/backfill.md +++ b/.agents/skills/hybrid-cloud-outboxes/references/backfill.md @@ -110,7 +110,7 @@ Each batch (via `process_outbox_backfill_batch`): 1. Calls `_chunk_processing_batch` to determine the ID range `(low, up)` for this batch 2. For each instance in `model.objects.filter(id__gte=low, id__lte=up)`: - - Region models: `inst.outbox_for_update().save()` inside `outbox_context(flush=False)` + - Cell models: `inst.outbox_for_update().save()` inside `outbox_context(flush=False)` - Control models: saves all `inst.outboxes_for_update()` inside `outbox_context(flush=False)` 3. If no more rows: sets cursor to `(0, replication_version + 1)` (marks complete) 4. Otherwise: advances cursor to `(up + 1, version)` @@ -141,7 +141,7 @@ options.get("outbox_replication.sentry_mymodel.replication_version") ### Check Outbox Queue Depth ```sql --- Region outboxes for a specific category +-- Cell outboxes for a specific category SELECT count(*) FROM sentry_regionoutbox WHERE category = ; diff --git a/.agents/skills/hybrid-cloud-outboxes/references/debugging.md b/.agents/skills/hybrid-cloud-outboxes/references/debugging.md index bfeb512577fc90..8fe652244a23d3 100644 --- a/.agents/skills/hybrid-cloud-outboxes/references/debugging.md +++ b/.agents/skills/hybrid-cloud-outboxes/references/debugging.md @@ -45,7 +45,7 @@ When debugging stuck outboxes, you'll often need to generate SQL for a developer | Direction | Model class | Table name | | ------------------ | --------------- | ---------------------- | -| Cell -> Control | `RegionOutbox` | `sentry_regionoutbox` | +| Cell -> Control | `CellOutbox` | `sentry_regionoutbox` | | Control -> Cell(s) | `ControlOutbox` | `sentry_controloutbox` | **How to determine direction**: Look at the model that changed. diff --git a/.agents/skills/hybrid-cloud-rpc/references/service-template.md b/.agents/skills/hybrid-cloud-rpc/references/service-template.md index 7407178e5767c6..69c4c00e0c0b01 100644 --- a/.agents/skills/hybrid-cloud-rpc/references/service-template.md +++ b/.agents/skills/hybrid-cloud-rpc/references/service-template.md @@ -7,7 +7,7 @@ from .model import * # noqa from .service import * # noqa ``` -## `model.py` (REGION silo example) +## `model.py` (CELL silo example) ```python # Please do not use @@ -125,7 +125,7 @@ def serialize_my_thing(obj: MyThing) -> RpcMyThing: ) ``` -## `service.py` (REGION silo) +## `service.py` (CELL silo) ```python # Please do not use @@ -223,7 +223,7 @@ class MyMappingService(RpcService): my_mapping_service = MyMappingService.create_delegation() ``` -## `impl.py` (REGION silo example) +## `impl.py` (CELL silo example) ```python from __future__ import annotations diff --git a/.agents/skills/hybrid-cloud-test-gen/references/outbox-tests.md b/.agents/skills/hybrid-cloud-test-gen/references/outbox-tests.md index 93df3d09ae884b..f44ee6c8406e4e 100644 --- a/.agents/skills/hybrid-cloud-test-gen/references/outbox-tests.md +++ b/.agents/skills/hybrid-cloud-test-gen/references/outbox-tests.md @@ -12,7 +12,7 @@ import pytest from sentry.hybridcloud.models.outbox import ( ControlOutbox, - RegionOutbox, + CellOutbox, outbox_context, ) from sentry.hybridcloud.outbox.category import OutboxCategory, OutboxScope @@ -181,7 +181,7 @@ class Test{Feature}OutboxProcessing(TestCase): - **`assume_test_silo_mode_of(Model)`** is preferred for checking a specific model's state cross-silo. Auto-detects the model's silo. - **`assume_test_silo_mode(SiloMode.X)`** for blocks accessing multiple models or non-model resources. - **Factory calls** (`self.create_organization()`, etc.) must NEVER be wrapped in `assume_test_silo_mode`. Factories handle silo mode internally. -- **`@control_silo_test`** for tests focused on `ControlOutbox` records. **`@cell_silo_test`** for `RegionOutbox`. +- **`@control_silo_test`** for tests focused on `ControlOutbox` records. **`@cell_silo_test`** for `CellOutbox`. - Only use **`TransactionTestCase`** for threading/concurrency tests (e.g., `threading.Barrier`), not for standard outbox drain tests. - Outbox drain fixtures can clear state between tests: ```python diff --git a/.agents/skills/sentry-backend-bugs/references/missing-records.md b/.agents/skills/sentry-backend-bugs/references/missing-records.md index 170bc36ad7cab0..134be19a3acdf5 100644 --- a/.agents/skills/sentry-backend-bugs/references/missing-records.md +++ b/.agents/skills/sentry-backend-bugs/references/missing-records.md @@ -17,7 +17,7 @@ The most common sources of stale IDs: 1. **Snuba/ClickHouse query results** -- Snuba stores issue IDs, project IDs, and group IDs that may be deleted from Postgres before Snuba data expires 2. **Workflow engine references** -- Detectors, subscriptions, and alert rules reference objects deleted asynchronously 3. **Integration state** -- SentryAppInstallation, ServiceHook, or ExternalActor deleted while alert rules still reference them -4. **Cross-silo references** -- Region silo holds IDs that reference control silo objects (or vice versa) that may be deleted asynchronously +4. **Cross-silo references** -- Cell silo holds IDs that reference control silo objects (or vice versa) that may be deleted asynchronously 5. **Cached foreign keys** -- A ProjectKey cached in Redis still references a `project_id` for a deleted project 6. **Monitor/cron references** -- Environment objects referenced by monitors that may be deleted @@ -120,7 +120,7 @@ except Subscription.DoesNotExist: | Environment deleted while monitors reference it | High | `Environment.objects.get(id=monitor.env_id)` | | Integration uninstalled while rules active | High | Alert rules referencing deleted SentryApp | | Cached FK target deleted | Medium | `get_from_cache(id=fk_id)` after parent deleted | -| Cross-silo object deleted asynchronously | Medium | Region silo references control silo object | +| Cross-silo object deleted asynchronously | Medium | Cell silo references control silo object | ## Fix Patterns diff --git a/.agents/skills/sentry-security/references/endpoint-patterns.md b/.agents/skills/sentry-security/references/endpoint-patterns.md index 2cb29f78edd530..7c6adad5d56d09 100644 --- a/.agents/skills/sentry-security/references/endpoint-patterns.md +++ b/.agents/skills/sentry-security/references/endpoint-patterns.md @@ -174,7 +174,7 @@ projects = self.get_projects( | -------------- | ---------------------- | ------------------------------------ | ------------------------------------------- | | Org-scoped | `OrganizationEndpoint` | `organization` in kwargs | `OrganizationPermission` (org:read for GET) | | Project-scoped | `ProjectEndpoint` | `organization` + `project` in kwargs | `ProjectPermission` | -| Region silo | `RegionSiloEndpoint` | Nothing — must implement own auth | None | +| Cell silo | `CellSiloEndpoint` | Nothing — must implement own auth | None | | Control silo | `ControlSiloEndpoint` | Nothing — must implement own auth | None | -If an endpoint inherits `RegionSiloEndpoint` or `Endpoint` directly instead of `OrganizationEndpoint`/`ProjectEndpoint`, verify it has its own authorization logic. +If an endpoint inherits `CellSiloEndpoint` or `Endpoint` directly instead of `OrganizationEndpoint`/`ProjectEndpoint`, verify it has its own authorization logic. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ad6b4a2fd9239f..40f02ebc420fd4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -553,14 +553,14 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get ## APIs -/src/sentry/apidocs/ @getsentry/owners-api -/src/sentry/api/urls.py @getsentry/owners-api -/api-docs/ @getsentry/owners-api -/tests/apidocs/ @getsentry/owners-api -/.github/workflows/openapi.yml @getsentry/owners-api -/.github/workflows/openapi-diff.yml @getsentry/owners-api -/src/sentry/conf/api_pagination_allowlist_do_not_modify.py @getsentry/owners-api -/tests/sentry/api/test_path_params.py @getsentry/owners-api +/src/sentry/apidocs/ @getsentry/docs +/src/sentry/api/urls.py @getsentry/enterprise +/api-docs/ @getsentry/docs +/tests/apidocs/ @getsentry/docs +/.github/workflows/openapi.yml @getsentry/docs +/.github/workflows/openapi-diff.yml @getsentry/docs +/src/sentry/conf/api_pagination_allowlist_do_not_modify.py @getsentry/enterprise +/tests/sentry/api/test_path_params.py @getsentry/enterprise ## End of APIs @@ -622,6 +622,10 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get /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 +/src/sentry/viewer_context.py @getsentry/machine-learning-ai +/tests/sentry/test_viewer_context.py @getsentry/machine-learning-ai +/src/sentry/middleware/viewer_context.py @getsentry/machine-learning-ai +/tests/sentry/middleware/test_viewer_context.py @getsentry/machine-learning-ai ## End of ML & AI ## Issues @@ -897,8 +901,10 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get # End of Conduit # Cell architecture +/src/sentry/synapse/ +/tests/sentry/synapse/ /.agents/skills/cell-architecture @getsentry/sre-infrastructure-engineering -tests/sentry/core/endpoints/test_organization_cell.py @getsentry/sre-infrastructure-engineering +/tests/sentry/core/endpoints/test_organization_cell.py @getsentry/sre-infrastructure-engineering # End of cell architecture # Foundational Storage diff --git a/.github/workflows/scripts/selective-testing/compute-selected-tests.py b/.github/workflows/scripts/selective-testing/compute-selected-tests.py index 695adbbec8f811..fb452fd10c5bde 100644 --- a/.github/workflows/scripts/selective-testing/compute-selected-tests.py +++ b/.github/workflows/scripts/selective-testing/compute-selected-tests.py @@ -35,6 +35,11 @@ ".github/CODEOWNERS": ["tests/sentry/api/test_api_owners.py"], } +# Tests that should always be run even if not explicitly selected. +ALWAYS_RUN_TESTS: set[str] = { + "tests/sentry/taskworker/test_config.py", +} + def _matches_trigger(file_path: str, trigger: str | re.Pattern[str]) -> bool: if isinstance(trigger, re.Pattern): @@ -164,6 +169,9 @@ def main() -> int: print(f"Including {len(existing_changed_test_files)} directly changed test files") affected_test_files.update(existing_changed_test_files) + # Include tests that should always be run + affected_test_files.update(ALWAYS_RUN_TESTS) + # Filter out any test files found via coverage lookup that no longer exist # (e.g. a deleted test file that covered the same source as another changed file). existing_files = {f for f in affected_test_files if Path(f).exists()} diff --git a/eslint.config.ts b/eslint.config.ts index 55e81a0cff8237..824337252346ba 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -38,8 +38,6 @@ import react from 'eslint-plugin-react'; import reactHooks from 'eslint-plugin-react-hooks'; import reactYouMightNotNeedAnEffect from 'eslint-plugin-react-you-might-not-need-an-effect'; import regexp from 'eslint-plugin-regexp'; -// @ts-expect-error TS(7016): Could not find a declaration file -import sentry from 'eslint-plugin-sentry'; import testingLibrary from 'eslint-plugin-testing-library'; // @ts-expect-error TS (7016): Could not find a declaration file import typescriptSortKeys from 'eslint-plugin-typescript-sort-keys'; @@ -458,7 +456,10 @@ export default typescript.config([ name: 'plugin/@sentry/sentry', plugins: {'@sentry': sentryPlugin}, rules: { + '@sentry/no-digits-in-tn': 'error', + '@sentry/no-dynamic-translations': 'error', '@sentry/no-static-translations': 'error', + '@sentry/no-styled-shortcut': 'error', }, }, { @@ -747,16 +748,6 @@ export default typescript.config([ ], }, }, - { - name: 'plugin/sentry', - // https://github.com/getsentry/eslint-config-sentry/tree/master/packages/eslint-plugin-sentry/docs/rules - plugins: {sentry}, - rules: { - 'sentry/no-digits-in-tn': 'error', - 'sentry/no-dynamic-translations': 'error', // TODO(ryan953): There are no docs for this rule - 'sentry/no-styled-shortcut': 'error', - }, - }, { name: 'plugin/@emotion', // https://github.com/emotion-js/emotion/tree/main/packages/eslint-plugin/docs/rules @@ -1262,95 +1253,111 @@ export default typescript.config([ rules: [ // --- figma code connect --- { - from: ['figma-code-connect'], - allow: ['core*'], + from: [{type: 'figma-code-connect'}], + allow: [{to: {type: 'core*'}}], }, { - from: ['sentry*'], - allow: ['core*', 'sentry*'], + from: [{type: 'sentry*'}], + allow: [{to: {type: 'core*'}}, {to: {type: 'sentry*'}}], }, { - from: ['getsentry*'], - allow: ['core*', 'getsentry*', 'sentry*'], + from: [{type: 'getsentry*'}], + allow: [ + {to: {type: 'core*'}}, + {to: {type: 'getsentry*'}}, + {to: {type: 'sentry*'}}, + ], }, { - from: ['gsAdmin*'], - disallow: ['sentry-locale'], - allow: ['core*', 'gsAdmin*', 'sentry*', 'getsentry*'], + from: [{type: 'gsAdmin*'}], + disallow: [{to: {type: 'sentry-locale'}}], + allow: [ + {to: {type: 'core*'}}, + {to: {type: 'gsAdmin*'}}, + {to: {type: 'sentry*'}}, + {to: {type: 'getsentry*'}}, + ], }, { - from: ['test-sentry'], - allow: ['test-sentry', 'test', 'core*', 'sentry*'], + from: [{type: 'test-sentry'}], + allow: [ + {to: {type: 'test-sentry'}}, + {to: {type: 'test'}}, + {to: {type: 'core*'}}, + {to: {type: 'sentry*'}}, + ], }, { // todo does test-gesentry need test-sentry? - from: ['test-getsentry'], + from: [{type: 'test-getsentry'}], allow: [ - 'test-getsentry', - 'test-sentry', - 'test', - 'core*', - 'getsentry*', - 'sentry*', + {to: {type: 'test-getsentry'}}, + {to: {type: 'test-sentry'}}, + {to: {type: 'test'}}, + {to: {type: 'core*'}}, + {to: {type: 'getsentry*'}}, + {to: {type: 'sentry*'}}, ], }, { - from: ['test-gsAdmin'], + from: [{type: 'test-gsAdmin'}], allow: [ - 'test-gsAdmin', - 'test-getsentry', - 'test-sentry', - 'test', - 'core*', - 'gsAdmin*', - 'sentry*', - 'getsentry*', + {to: {type: 'test-gsAdmin'}}, + {to: {type: 'test-getsentry'}}, + {to: {type: 'test-sentry'}}, + {to: {type: 'test'}}, + {to: {type: 'core*'}}, + {to: {type: 'gsAdmin*'}}, + {to: {type: 'sentry*'}}, + {to: {type: 'getsentry*'}}, ], }, { - from: ['test'], - allow: ['test', 'test-sentry', 'sentry*'], + from: [{type: 'test'}], + allow: [ + {to: {type: 'test'}}, + {to: {type: 'test-sentry'}}, + {to: {type: 'sentry*'}}, + ], }, { - from: ['configs'], - allow: ['configs', 'build-utils'], + from: [{type: 'configs'}], + allow: [{to: {type: 'configs'}}, {to: {type: 'build-utils'}}], }, // --- stories --- { - from: ['story-files', 'story-book'], - allow: ['core*', 'sentry*', 'story-book'], + from: [{type: 'story-files'}, {type: 'story-book'}], + allow: [ + {to: {type: 'core*'}}, + {to: {type: 'sentry*'}}, + {to: {type: 'story-book'}}, + ], }, // --- debug tools (e.g. notifications) --- { - from: ['debug-tools'], - allow: ['core*', 'sentry*', 'debug-tools'], + from: [{type: 'debug-tools'}], + allow: [ + {to: {type: 'core*'}}, + {to: {type: 'sentry*'}}, + {to: {type: 'debug-tools'}}, + ], }, // --- core --- // todo: sentry* shouldn't be allowed { - from: ['core'], - allow: ['core*', 'sentry*'], - }, - ], - }, - ], - 'boundaries/entry-point': [ - 'error', - { - default: 'disallow', - rules: [ - { - target: ['core'], - allow: [ - '*.{ts,tsx}', // core/renderToString.tsx at the core root etc. - '*/index.{ts,tsx}', // core/form/index.tsx, core/alert/index.tsx etc. - '**/*.png', // needed for story-files - '**/__stories__/*.{ts,tsx}', // story demo helpers imported by .mdx files - ], + from: [{type: 'core'}], + allow: [{to: {type: 'core*'}}, {to: {type: 'sentry*'}}], }, + // --- core entry points (enforce isolation) --- { - target: ['!core'], - allow: '**/*', + to: { + type: 'core', + internalPath: + '!(*.{ts,tsx}|*/index.{ts,tsx}|**/*.png|**/__stories__/*.{ts,tsx})', + }, + disallow: { + from: {type: '*'}, + }, }, ], }, diff --git a/knip.config.ts b/knip.config.ts index b0b8657a864274..b9e2f2f3d82476 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -12,6 +12,8 @@ const productionEntryPoints = [ 'static/app/gettingStartedDocs/**/*.{js,ts,tsx}', // this is imported with require.context 'static/app/data/forms/*.tsx', + // frontend experiemnt framework may be unused when we have no experiemnets + 'static/app/utils/useExperiment.tsx', // --- we should be able to get rid of those: --- // Only used in stories (so far) 'static/app/components/core/quote/*.tsx', @@ -68,13 +70,12 @@ const config: KnipConfig = { ignoreExportsUsedInFile: isProductionMode, ignoreDependencies: [ 'core-js', + 'tslib', // subdependency of many packages, declare the latest version 'jest-environment-jsdom', // used as testEnvironment in jest config 'swc-plugin-component-annotate', // used in rspack config, needs better knip plugin '@swc/plugin-emotion', // used in rspack config, needs better knip plugin 'buffer', // rspack.ProvidePlugin, needs better knip plugin 'process', // rspack.ProvidePlugin, needs better knip plugin - '@types/webpack-env', // needed to make require.context work - '@types/gtag.js', // needed for global `gtag` namespace typings '@babel/preset-env', // Still used in jest '@babel/preset-react', // Still used in jest '@babel/preset-typescript', // Still used in jest diff --git a/package.json b/package.json index 66c00ae0a97422..368d4d7d9ef294 100644 --- a/package.json +++ b/package.json @@ -224,7 +224,6 @@ "ts-checker-rspack-plugin": "1.2.6", "tslib": "^2.8.1", "type-fest": "^5.2.0", - "typescript": "5.9.3", "zod": "^4.3.5", "zrender": "6.0.0", "zxcvbn": "^4.4.2" @@ -250,7 +249,7 @@ "@types/node": "^22.9.1", "@typescript-eslint/rule-tester": "8.58.0", "@typescript-eslint/utils": "8.58.0", - "@typescript/native-preview": "7.0.0-dev.20260112.1", + "@typescript/native-preview": "7.0.0-dev.20260401.1", "@volar/typescript": "^2.4.28", "babel-jest": "30.3.0", "eslint": "9.34.0", @@ -266,7 +265,6 @@ "eslint-plugin-react-hooks": "6.1.0", "eslint-plugin-react-you-might-not-need-an-effect": "0.5.3", "eslint-plugin-regexp": "^3.0.0", - "eslint-plugin-sentry": "^2.10.0", "eslint-plugin-testing-library": "^7.16.0", "eslint-plugin-typescript-sort-keys": "^3.3.0", "eslint-plugin-unicorn": "^57.0.0", @@ -286,6 +284,7 @@ "stylelint": "16.10.0", "stylelint-config-recommended": "^14.0.1", "terser": "5.40.0", + "typescript": "6.0.2", "typescript-eslint": "8.58.0" }, "optionalDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f58f5feb8ceef..926d36c93de1c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -470,7 +470,7 @@ importers: version: 1.4.0(date-fns@2.17.0)(react@19.2.3) react-docgen-typescript: specifier: 2.4.0 - version: 2.4.0(typescript@5.9.3) + version: 2.4.0(typescript@6.0.2) react-dom: specifier: 19.2.3 version: 19.2.3(react@19.2.3) @@ -527,16 +527,13 @@ importers: version: 1.13.0 ts-checker-rspack-plugin: specifier: 1.2.6 - version: 1.2.6(@rspack/core@1.7.6(@swc/helpers@0.5.15))(typescript@5.9.3) + version: 1.2.6(@rspack/core@1.7.6(@swc/helpers@0.5.15))(typescript@6.0.2) tslib: specifier: ^2.8.1 version: 2.8.1 type-fest: specifier: ^5.2.0 version: 5.2.0 - typescript: - specifier: 5.9.3 - version: 5.9.3 zod: specifier: ^4.3.5 version: 4.3.5 @@ -549,7 +546,7 @@ importers: devDependencies: '@emotion/eslint-plugin': specifier: ^11.12.0 - version: 11.12.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + version: 11.12.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) '@emotion/server': specifier: ^11.11.0 version: 11.11.0(@emotion/css@11.13.5) @@ -567,7 +564,7 @@ importers: version: 2.41.0 '@sentry/jest-environment': specifier: 6.1.0 - version: 6.1.0(@sentry/node@10.41.0-beta.0)(@sentry/profiling-node@10.41.0-beta.0)(jest@30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3))) + version: 6.1.0(@sentry/node@10.41.0-beta.0)(@sentry/profiling-node@10.41.0-beta.0)(jest@30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2))) '@sentry/profiling-node': specifier: 10.41.0-beta.0 version: 10.41.0-beta.0 @@ -576,7 +573,7 @@ importers: version: 1.0.1 '@tanstack/eslint-plugin-query': specifier: 5.96.0 - version: 5.96.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + version: 5.96.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -603,13 +600,13 @@ importers: version: 22.15.21 '@typescript-eslint/rule-tester': specifier: 8.58.0 - version: 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + version: 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) '@typescript-eslint/utils': specifier: 8.58.0 - version: 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + version: 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) '@typescript/native-preview': - specifier: 7.0.0-dev.20260112.1 - version: 7.0.0-dev.20260112.1 + specifier: 7.0.0-dev.20260401.1 + version: 7.0.0-dev.20260401.1 '@volar/typescript': specifier: ^2.4.28 version: 2.4.28 @@ -627,13 +624,13 @@ importers: version: 3.8.3(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-boundaries: specifier: 6.0.2 - version: 6.0.2(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) + version: 6.0.2(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-import: specifier: 2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jest: specifier: 29.15.0 - version: 29.15.0(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.34.0(jiti@2.6.1))(jest@30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.15.0(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint@9.34.0(jiti@2.6.1))(jest@30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)))(typescript@6.0.2) eslint-plugin-jest-dom: specifier: ^5.5.0 version: 5.5.0(@testing-library/dom@10.4.1)(eslint@9.34.0(jiti@2.6.1)) @@ -655,15 +652,12 @@ importers: eslint-plugin-regexp: specifier: ^3.0.0 version: 3.0.0(eslint@9.34.0(jiti@2.6.1)) - eslint-plugin-sentry: - specifier: ^2.10.0 - version: 2.10.0 eslint-plugin-testing-library: specifier: ^7.16.0 - version: 7.16.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + version: 7.16.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) eslint-plugin-typescript-sort-keys: specifier: ^3.3.0 - version: 3.3.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + version: 3.3.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) eslint-plugin-unicorn: specifier: ^57.0.0 version: 57.0.0(eslint@9.34.0(jiti@2.6.1)) @@ -675,7 +669,7 @@ importers: version: 16.3.0 jest: specifier: 30.3.0 - version: 30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)) + version: 30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)) jest-canvas-mock: specifier: ^2.5.2 version: 2.5.2 @@ -708,16 +702,19 @@ importers: version: 0.18.0 stylelint: specifier: 16.10.0 - version: 16.10.0(typescript@5.9.3) + version: 16.10.0(typescript@6.0.2) stylelint-config-recommended: specifier: ^14.0.1 - version: 14.0.1(stylelint@16.10.0(typescript@5.9.3)) + version: 14.0.1(stylelint@16.10.0(typescript@6.0.2)) terser: specifier: 5.40.0 version: 5.40.0 + typescript: + specifier: 6.0.2 + version: 6.0.2 typescript-eslint: specifier: 8.58.0 - version: 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + version: 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) optionalDependencies: fsevents: specifier: ^2.3.3 @@ -4173,6 +4170,12 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.56.1': + resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.58.0': resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4197,6 +4200,12 @@ packages: resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.56.1': + resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/tsconfig-utils@8.58.0': resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4231,6 +4240,12 @@ packages: typescript: optional: true + '@typescript-eslint/typescript-estree@8.56.1': + resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/typescript-estree@8.58.0': resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4243,6 +4258,13 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + '@typescript-eslint/utils@8.56.1': + resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.58.0': resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4262,43 +4284,43 @@ packages: resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260112.1': - resolution: {integrity: sha512-FUOOGN0/9LF+AOX07SOqfX1hBQfP3rezMFCwDlwAVW52leJ2Fur8efrQR5oUNL8hDt/NMGJwsg3wreZGdYSqJg==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-9PCc1D4/zLic30g1upOw6ZmUl98fnrXYRv5wIZ6fxm1zZAObieRKUX3Jbr8M9N8iQQFxPIZPniIScsxAbmbJvw==} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260112.1': - resolution: {integrity: sha512-SgiY7DcvhcyCCdrFRcgq5zPK1jdp7hxhnvo8YEEGvBsvyI+Y/q6phoR79nqMnz5W7a2HTPuaxLjCf7tR17cw4w==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-wwzca1KrjSVC6ApXfITsg/wF4GGbhVYebc7zChpuyi+phrHfw6ThTPB5XFUH4nA32vqw0Hn/6KACipMgzg8GPA==} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260112.1': - resolution: {integrity: sha512-N9Ukn5NUjO063F3YICBl/dSLEpJayTIMTAJbvbuLjEdlHmp9xn7ty3MCkE4FqP6x/CWLDiZttnu9jYppJ//Q5g==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-1hgKibGi4QZF1J0hKltgY4nj4yKDmI4Ang5ar80I+YeUdGxV/fP2kU3bJang7QtHuSso6W+a52SF62zgqbzdow==} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260112.1': - resolution: {integrity: sha512-7mtAOEKaNOqKFIIVUsFK2IYB09bd/i8oIkzPi1EcEN3V7dzuKtSieNak30pY+k6z/f6QpUheHXVFM4wXBGbGrQ==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-bbIkRZYjtyoyCJ3wFES7qn3EwYO5Go1hxArL5X5oWiBmUHq5gMIxTZDv5mpWxopVf9Eyh4ErHefXjf1s4J+6Ag==} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260112.1': - resolution: {integrity: sha512-K4/6TRu2MAwObOxZyUmUvAxi1B5fpwi/8OYb8xyf9VsLbkDCsBMbZxgwEMf9RBYhW74I187IIGEfw/g0oHDKEA==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-1ysZ4c/Wa3RYIlrwVceYlhb1m1hxQ4P2x92valZXH0bNWEPb+oiQ4Yf35O/vi5h8zDdX/ZQ59vivYl27cF1VVA==} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260112.1': - resolution: {integrity: sha512-ivjcYuqlCD91x6QOO+/xpjGUiWBLzBCgz0aAITkEfDTBHEwf3keTEQH2GsaVnMC8GKUFdoEF6QtSR+jZ75BhWA==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-fZYLCRe36y1BuzRFFpU2/RQ212l6Y1dccRMh8oTB8HlAVAAwtbkb6cjEn0Ablj4Dy16+Ih8R9uHsxPLNhtKw1Q==} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260112.1': - resolution: {integrity: sha512-gZ+72BdVDA4VUFKw9ZvEkEger/2SrXw62gstz25TriOHROvjxEgXLVHqiVKhE/XkWqYT0GJ7aM7WKoM/RG2XTw==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-I6ses4SjWvpbvSpm1BPFRrDeqrzu7JTchJG/a26iwwmTLv4fAGLc5/o6Kv9Naygozop1W3KOcVM5i3A9oxiIjQ==} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260112.1': - resolution: {integrity: sha512-DvUmkkJ5BePLmZbQ9OecQr6wRVUlWbNz/bNEYmTcl7+0qwF8KtgPGriWiyuIzs+TiIhjf3HM1tt5OC/uBHNJsw==} + '@typescript/native-preview@7.0.0-dev.20260401.1': + resolution: {integrity: sha512-xJcN9WlY/P6xKjCMH4+DTzZSj/EKR6H9avuqUKs4eKyPEvaQ4bX+9Ys3Vl2qhlUaD7IRWY7HN7db0LHAGlWRSA==} hasBin: true '@ungap/structured-clone@1.3.0': @@ -5797,10 +5819,6 @@ packages: peerDependencies: eslint: '>=9.38.0' - eslint-plugin-sentry@2.10.0: - resolution: {integrity: sha512-QuCUr2B78Onr2GdXM4ncPlS2IBAB+lL5QwSPNvX5LcAoBtV7iFuhEY7DS4WqwgmJcRnI6g3/dKARpFuOILR35A==} - engines: {node: '>=0.10.0'} - eslint-plugin-testing-library@7.16.0: resolution: {integrity: sha512-lHZI6/Olb2oZqxd1+s1nOLCtL2PXKrc1ERz6oDbUKS0xZAMFH3Fy6wJo75z3pXTop3BV6+loPi2MSjIYt3vpAg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -9107,6 +9125,12 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -9223,6 +9247,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} + engines: {node: '>=14.17'} + hasBin: true + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -10818,10 +10847,10 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@boundaries/elements@2.0.1(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1))': + '@boundaries/elements@2.0.1(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1))': dependencies: eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) handlebars: 4.7.9 is-core-module: 2.16.1 micromatch: 4.0.8 @@ -10964,9 +10993,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@emotion/eslint-plugin@11.12.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3)': + '@emotion/eslint-plugin@11.12.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) eslint: 9.34.0(jiti@2.6.1) transitivePeerDependencies: - supports-color @@ -11331,7 +11360,7 @@ snapshots: jest-util: 30.3.0 slash: 3.0.0 - '@jest/core@30.3.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3))': + '@jest/core@30.3.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2))': dependencies: '@jest/console': 30.3.0 '@jest/pattern': 30.0.1 @@ -11346,7 +11375,7 @@ snapshots: exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.3.0 - jest-config: 30.3.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)) + jest-config: 30.3.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)) jest-haste-map: 30.3.0 jest-message-util: 30.3.0 jest-regex-util: 30.0.1 @@ -13133,11 +13162,11 @@ snapshots: '@sentry/core@10.41.0-beta.0': {} - '@sentry/jest-environment@6.1.0(@sentry/node@10.41.0-beta.0)(@sentry/profiling-node@10.41.0-beta.0)(jest@30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)))': + '@sentry/jest-environment@6.1.0(@sentry/node@10.41.0-beta.0)(@sentry/profiling-node@10.41.0-beta.0)(jest@30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)))': dependencies: '@sentry/node': 10.41.0-beta.0 '@sentry/profiling-node': 10.41.0-beta.0 - jest: 30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)) + jest: 30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)) '@sentry/node-core@10.41.0-beta.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.211.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)': dependencies: @@ -13415,12 +13444,12 @@ snapshots: - csstype - utf-8-validate - '@tanstack/eslint-plugin-query@5.96.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.96.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) eslint: 9.34.0(jiti@2.6.1) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -13975,56 +14004,65 @@ snapshots: dependencies: '@types/yargs-parser': 15.0.0 - '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) '@typescript-eslint/scope-manager': 8.58.0 - '@typescript-eslint/type-utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) '@typescript-eslint/visitor-keys': 8.58.0 eslint: 9.34.0(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/experimental-utils@5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/experimental-utils@5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) eslint: 9.34.0(jiti@2.6.1) transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@typescript-eslint/scope-manager': 8.58.0 '@typescript-eslint/types': 8.58.0 - '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) '@typescript-eslint/visitor-keys': 8.58.0 debug: 4.4.3 eslint: 9.34.0(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.56.1(typescript@6.0.2)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@6.0.2) + '@typescript-eslint/types': 8.56.1 + debug: 4.4.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.58.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.58.0(typescript@6.0.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@6.0.2) '@typescript-eslint/types': 8.58.0 debug: 4.4.3 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/rule-tester@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/rule-tester@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: - '@typescript-eslint/parser': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) ajv: 6.12.6 eslint: 9.34.0(jiti@2.6.1) json-stable-stringify-without-jsonify: 1.0.1 @@ -14049,19 +14087,23 @@ snapshots: '@typescript-eslint/types': 8.58.0 '@typescript-eslint/visitor-keys': 8.58.0 - '@typescript-eslint/tsconfig-utils@8.58.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@6.0.2)': dependencies: - typescript: 5.9.3 + typescript: 6.0.2 + + '@typescript-eslint/tsconfig-utils@8.58.0(typescript@6.0.2)': + dependencies: + typescript: 6.0.2 - '@typescript-eslint/type-utils@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@typescript-eslint/types': 8.58.0 - '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) debug: 4.4.3 eslint: 9.34.0(jiti@2.6.1) - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -14071,7 +14113,7 @@ snapshots: '@typescript-eslint/types@8.58.0': {} - '@typescript-eslint/typescript-estree@5.62.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@5.62.0(typescript@6.0.2)': dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 @@ -14079,35 +14121,50 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.3 - tsutils: 3.21.0(typescript@5.9.3) + tsutils: 3.21.0(typescript@6.0.2) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.56.1(typescript@6.0.2)': + dependencies: + '@typescript-eslint/project-service': 8.56.1(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@6.0.2) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + minimatch: 10.2.3 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.58.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.58.0(typescript@6.0.2)': dependencies: - '@typescript-eslint/project-service': 8.58.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) + '@typescript-eslint/project-service': 8.58.0(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@6.0.2) '@typescript-eslint/types': 8.58.0 '@typescript-eslint/visitor-keys': 8.58.0 debug: 4.4.3 minimatch: 10.2.3 semver: 7.7.3 tinyglobby: 0.2.15 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.34.0(jiti@2.6.1)) '@types/json-schema': 7.0.15 '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@6.0.2) eslint: 9.34.0(jiti@2.6.1) eslint-scope: 5.1.1 semver: 7.7.2 @@ -14115,14 +14172,25 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.56.1(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.34.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@6.0.2) + eslint: 9.34.0(jiti@2.6.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.34.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.58.0 '@typescript-eslint/types': 8.58.0 - '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) eslint: 9.34.0(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -14141,36 +14209,36 @@ snapshots: '@typescript-eslint/types': 8.58.0 eslint-visitor-keys: 5.0.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260112.1': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260401.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260112.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260401.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260112.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260401.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260112.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260401.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260112.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260401.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260112.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260401.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260112.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260401.1': optional: true - '@typescript/native-preview@7.0.0-dev.20260112.1': + '@typescript/native-preview@7.0.0-dev.20260401.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260112.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260112.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260112.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260112.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260112.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260112.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260112.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260401.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260401.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260401.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260401.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260401.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260401.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260401.1 '@ungap/structured-clone@1.3.0': {} @@ -15027,14 +15095,14 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - cosmiconfig@9.0.0(typescript@5.9.3): + cosmiconfig@9.0.0(typescript@6.0.2): dependencies: env-paths: 2.2.1 import-fresh: 3.3.0 js-yaml: 4.1.1 parse-json: 5.2.0 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.2 create-require@1.1.1: optional: true @@ -15709,7 +15777,7 @@ snapshots: stable-hash: 0.0.4 tinyglobby: 0.2.12 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -15733,24 +15801,24 @@ snapshots: - bluebird - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) eslint: 9.34.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-boundaries@6.0.2(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)): + eslint-plugin-boundaries@6.0.2(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)): dependencies: - '@boundaries/elements': 2.0.1(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) + '@boundaries/elements': 2.0.1(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) chalk: 4.1.2 eslint: 9.34.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) handlebars: 4.7.9 micromatch: 4.0.8 transitivePeerDependencies: @@ -15759,7 +15827,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -15770,7 +15838,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.34.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.34.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -15782,7 +15850,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -15796,14 +15864,14 @@ snapshots: optionalDependencies: '@testing-library/dom': 10.4.1 - eslint-plugin-jest@29.15.0(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.34.0(jiti@2.6.1))(jest@30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)))(typescript@5.9.3): + eslint-plugin-jest@29.15.0(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint@9.34.0(jiti@2.6.1))(jest@30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)))(typescript@6.0.2): dependencies: - '@typescript-eslint/utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) eslint: 9.34.0(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) - jest: 30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)) - typescript: 5.9.3 + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) + jest: 30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)) + typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -15878,27 +15946,23 @@ snapshots: regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-sentry@2.10.0: - dependencies: - requireindex: 1.2.0 - - eslint-plugin-testing-library@7.16.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-testing-library@7.16.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2): dependencies: '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) eslint: 9.34.0(jiti@2.6.1) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-typescript-sort-keys@3.3.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-typescript-sort-keys@3.3.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@typescript-eslint/experimental-utils': 5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/experimental-utils': 5.62.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/parser': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) eslint: 9.34.0(jiti@2.6.1) json-schema: 0.4.0 natural-compare-lite: 1.4.0 - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -17131,15 +17195,15 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)): + jest-cli@30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)): dependencies: - '@jest/core': 30.3.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)) + '@jest/core': 30.3.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)) '@jest/test-result': 30.3.0 '@jest/types': 30.3.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)) + jest-config: 30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)) jest-util: 30.3.0 jest-validate: 30.3.0 yargs: 17.7.2 @@ -17150,7 +17214,7 @@ snapshots: - supports-color - ts-node - jest-config@30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)): + jest-config@30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)): dependencies: '@babel/core': 7.29.0 '@jest/get-type': 30.1.0 @@ -17177,12 +17241,12 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.15.21 - ts-node: 10.9.2(@types/node@22.15.21)(typescript@5.9.3) + ts-node: 10.9.2(@types/node@22.15.21)(typescript@6.0.2) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@30.3.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)): + jest-config@30.3.0(@types/node@22.19.7)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)): dependencies: '@babel/core': 7.29.0 '@jest/get-type': 30.1.0 @@ -17209,7 +17273,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.19.7 - ts-node: 10.9.2(@types/node@22.15.21)(typescript@5.9.3) + ts-node: 10.9.2(@types/node@22.15.21)(typescript@6.0.2) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -17506,12 +17570,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)): + jest@30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)): dependencies: - '@jest/core': 30.3.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)) + '@jest/core': 30.3.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)) '@jest/types': 30.3.0 import-local: 3.2.0 - jest-cli: 30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3)) + jest-cli: 30.3.0(@types/node@22.15.21)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -19006,9 +19070,9 @@ snapshots: react-list: 0.8.16(react@19.2.3) shallow-equal: 1.2.1 - react-docgen-typescript@2.4.0(typescript@5.9.3): + react-docgen-typescript@2.4.0(typescript@6.0.2): dependencies: - typescript: 5.9.3 + typescript: 6.0.2 react-dom@19.2.3(react@19.2.3): dependencies: @@ -19927,11 +19991,11 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - stylelint-config-recommended@14.0.1(stylelint@16.10.0(typescript@5.9.3)): + stylelint-config-recommended@14.0.1(stylelint@16.10.0(typescript@6.0.2)): dependencies: - stylelint: 16.10.0(typescript@5.9.3) + stylelint: 16.10.0(typescript@6.0.2) - stylelint@16.10.0(typescript@5.9.3): + stylelint@16.10.0(typescript@6.0.2): dependencies: '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) '@csstools/css-tokenizer': 3.0.3 @@ -19940,7 +20004,7 @@ snapshots: '@dual-bundle/import-meta-resolve': 4.1.0 balanced-match: 2.0.0 colord: 2.9.3 - cosmiconfig: 9.0.0(typescript@5.9.3) + cosmiconfig: 9.0.0(typescript@6.0.2) css-functions-list: 3.2.3 css-tree: 3.0.1 debug: 4.4.1 @@ -20127,11 +20191,15 @@ snapshots: trough@2.2.0: {} - ts-api-utils@2.5.0(typescript@5.9.3): + ts-api-utils@2.4.0(typescript@6.0.2): dependencies: - typescript: 5.9.3 + typescript: 6.0.2 + + ts-api-utils@2.5.0(typescript@6.0.2): + dependencies: + typescript: 6.0.2 - ts-checker-rspack-plugin@1.2.6(@rspack/core@1.7.6(@swc/helpers@0.5.15))(typescript@5.9.3): + ts-checker-rspack-plugin@1.2.6(@rspack/core@1.7.6(@swc/helpers@0.5.15))(typescript@6.0.2): dependencies: '@babel/code-frame': 7.27.1 '@rspack/lite-tapable': 1.1.0 @@ -20140,7 +20208,7 @@ snapshots: memfs: 4.51.1 minimatch: 9.0.5 picocolors: 1.1.1 - typescript: 5.9.3 + typescript: 6.0.2 optionalDependencies: '@rspack/core': 1.7.6(@swc/helpers@0.5.15) @@ -20149,7 +20217,7 @@ snapshots: '@ts-morph/common': 0.28.1 code-block-writer: 13.0.3 - ts-node@10.9.2(@types/node@22.15.21)(typescript@5.9.3): + ts-node@10.9.2(@types/node@22.15.21)(typescript@6.0.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 @@ -20163,7 +20231,7 @@ snapshots: create-require: 1.1.1 diff: 4.0.4 make-error: 1.3.6 - typescript: 5.9.3 + typescript: 6.0.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optional: true @@ -20181,10 +20249,10 @@ snapshots: tslib@2.8.1: {} - tsutils@3.21.0(typescript@5.9.3): + tsutils@3.21.0(typescript@6.0.2): dependencies: tslib: 1.14.1 - typescript: 5.9.3 + typescript: 6.0.2 type-check@0.4.0: dependencies: @@ -20244,14 +20312,14 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2))(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/parser': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.0(eslint@9.34.0(jiti@2.6.1))(typescript@6.0.2) eslint: 9.34.0(jiti@2.6.1) - typescript: 5.9.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -20259,6 +20327,8 @@ snapshots: typescript@5.9.3: {} + typescript@6.0.2: {} + uglify-js@3.19.3: optional: true diff --git a/pyproject.toml b/pyproject.toml index 704559ec8e75d8..46b1f3728e6103 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ dependencies = [ "rfc3339-validator>=0.1.2", "rfc3986-validator>=0.1.1", # [end] jsonschema format validators - "sentry-arroyo>=2.38.5", + "sentry-arroyo>=2.38.7", "sentry-conventions>=0.3.0", "sentry-forked-email-reply-parser>=0.5.12.post1", "sentry-kafka-schemas>=2.1.27", diff --git a/rspack.config.ts b/rspack.config.ts index 0621b035e0fe17..24f35a0950414d 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -846,15 +846,18 @@ if (IS_UI_DEV_ONLY || SENTRY_EXPERIMENTAL_SPA) { } if (IS_PRODUCTION) { - // This compression-webpack-plugin generates pre-compressed files - // ending in .gz, to be picked up and served by our internal static media - // server as well as nginx when paired with the gzip_static module. - appConfig.plugins?.push( - new CompressionPlugin({ - algorithm: 'gzip', - test: /\.(js|map|css|svg|html|txt|ico|eot|ttf)$/, - }) - ); + if (!IS_DEPLOY_PREVIEW) { + // This compression-webpack-plugin generates pre-compressed files + // ending in .gz, to be picked up and served by our internal static media + // server as well as nginx when paired with the gzip_static module. + // Skipped for deploy previews since Vercel handles compression itself. + appConfig.plugins?.push( + new CompressionPlugin({ + algorithm: 'gzip', + test: /\.(js|map|css|svg|html|txt|ico|eot|ttf)$/, + }) + ); + } // Enable sentry-webpack-plugin for production builds appConfig.plugins?.push( diff --git a/src/AGENTS.md b/src/AGENTS.md index b2bf3d720c27bc..2126133d132e47 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -149,7 +149,7 @@ from rest_framework.response import Response from sentry.api.base import cell_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.serializers import serialize -from sentry.api.serializers.models.organization import DetailedOrganizationSerializer +from sentry.api.serializers.models.organization import OrganizationSerializer @cell_silo_endpoint class OrganizationDetailsEndpoint(OrganizationEndpoint): @@ -164,7 +164,7 @@ class OrganizationDetailsEndpoint(OrganizationEndpoint): serialize( organization, request.user, - DetailedOrganizationSerializer() + OrganizationSerializer() ) ) diff --git a/src/sentry/api/endpoints/accept_project_transfer.py b/src/sentry/api/endpoints/accept_project_transfer.py index 5f4a4d69dd2d8b..15c59b1cb4ba99 100644 --- a/src/sentry/api/endpoints/accept_project_transfer.py +++ b/src/sentry/api/endpoints/accept_project_transfer.py @@ -15,7 +15,7 @@ from sentry.api.permissions import SentryIsAuthenticated from sentry.api.serializers import serialize from sentry.api.serializers.models.organization import ( - DetailedOrganizationSerializerWithProjectsAndTeams, + OrganizationWithProjectsAndTeamsSerializer, ) from sentry.constants import CELL_API_DEPRECATION_DATE from sentry.core.endpoints.project_transfer import SALT @@ -94,7 +94,7 @@ def get(self, request: Request) -> Response: "organizations": serialize( list(organizations), request.user, - DetailedOrganizationSerializerWithProjectsAndTeams(), + OrganizationWithProjectsAndTeamsSerializer(), access=request.access, ), "project": {"slug": project.slug, "id": project.id}, diff --git a/src/sentry/api/endpoints/project_rule_details.py b/src/sentry/api/endpoints/project_rule_details.py index 4209086556956f..af65a749ce74b9 100644 --- a/src/sentry/api/endpoints/project_rule_details.py +++ b/src/sentry/api/endpoints/project_rule_details.py @@ -104,7 +104,7 @@ class ProjectRuleDetailsPutSerializer(serializers.Serializer): @cell_silo_endpoint class ProjectRuleDetailsEndpoint(WorkflowEngineRuleEndpoint): workflow_engine_method_flags = { - "GET": "organizations:workflow-engine-projectruledetailsendpoint-get", + "GET": "organizations:workflow-engine-issue-alert-endpoints-get", } publish_status = { "DELETE": ApiPublishStatus.PUBLIC, diff --git a/src/sentry/api/endpoints/project_rules.py b/src/sentry/api/endpoints/project_rules.py index 7b882f5be412ae..67b3e40d29f395 100644 --- a/src/sentry/api/endpoints/project_rules.py +++ b/src/sentry/api/endpoints/project_rules.py @@ -864,7 +864,7 @@ def get(self, request: Request, project: Project) -> Response: queryset: BaseQuerySet[Workflow, Workflow] | BaseQuerySet[Rule, Rule] serializer: WorkflowEngineRuleSerializer | RuleSerializer if features.has( - "organizations:workflow-engine-projectrulesendpoint-get", project.organization + "organizations:workflow-engine-issue-alert-endpoints-get", project.organization ) or features.has("organizations:workflow-engine-rule-serializers", project.organization): queryset = Workflow.objects.filter( detectorworkflow__detector__project=project, diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index 1f68aa0fc4f336..18c1c1fa8f50af 100644 --- a/src/sentry/api/serializers/models/organization.py +++ b/src/sentry/api/serializers/models/organization.py @@ -84,6 +84,7 @@ from sentry.organizations.absolute_url import generate_organization_url from sentry.organizations.services.organization import RpcOrganizationSummary from sentry.replays.models import OrganizationMemberReplayAccess +from sentry.seer.autofix.utils import get_valid_automated_run_stopping_points from sentry.users.models.user import User from sentry.users.services.user.model import RpcUser from sentry.users.services.user.service import user_service @@ -130,7 +131,7 @@ class OnboardingTasksSerializerResponse(TypedDict): data: Any # JSON objec -class OrganizationSerializerResponseOptional(TypedDict, total=False): +class OrganizationSummarySerializerResponseOptional(TypedDict, total=False): features: list[str] # Only included if include_feature_flags is True extraOptions: dict[str, dict[str, Any]] access: frozenset[str] # Only if access=... is passed @@ -138,7 +139,7 @@ class OrganizationSerializerResponseOptional(TypedDict, total=False): @extend_schema_serializer(exclude_fields=["requireEmailVerification"]) -class OrganizationSerializerResponse(OrganizationSerializerResponseOptional): +class OrganizationSummarySerializerResponse(OrganizationSummarySerializerResponseOptional): id: str slug: str status: _Status @@ -285,7 +286,7 @@ def serialize( @register(Organization) -class OrganizationSerializer(Serializer): +class OrganizationSummarySerializer(Serializer): def get_attrs( self, item_list: Sequence[Organization], user: User | RpcUser | AnonymousUser, **kwargs: Any ) -> MutableMapping[Organization, MutableMapping[str, Any]]: @@ -409,7 +410,7 @@ def serialize( attrs: Mapping[str, Any], user: User | RpcUser | AnonymousUser, **kwargs: Any, - ) -> OrganizationSerializerResponse: + ) -> OrganizationSummarySerializerResponse: if attrs.get("avatar"): avatar: SerializedAvatarFields = { "avatarType": attrs["avatar"].get_avatar_type_display(), @@ -425,7 +426,7 @@ def serialize( has_auth_provider = attrs.get("auth_provider", None) is not None - context: OrganizationSerializerResponse = { + context: OrganizationSummarySerializerResponse = { "id": str(obj.id), "slug": obj.slug, "status": {"id": status.name.lower(), "name": status.label}, @@ -503,7 +504,7 @@ def serialize( } -class _DetailedOrganizationSerializerResponseOptional(OrganizationSerializerResponse, total=False): +class _OrganizationSerializerResponseOptional(OrganizationSummarySerializerResponse, total=False): role: Any # TODO: replace with enum/literal orgRole: str targetSampleRate: float @@ -517,7 +518,7 @@ class _DetailedOrganizationSerializerResponseOptional(OrganizationSerializerResp @extend_schema_serializer(exclude_fields=["availableRoles"]) -class DetailedOrganizationSerializerResponse(_DetailedOrganizationSerializerResponseOptional): +class OrganizationSerializerResponse(_OrganizationSerializerResponseOptional): experiments: dict[str, str] isDefault: bool defaultRole: str # TODO: replace with enum/literal @@ -569,7 +570,7 @@ class DetailedOrganizationSerializerResponse(_DetailedOrganizationSerializerResp replayAccessMembers: list[int] -class DetailedOrganizationSerializer(OrganizationSerializer): +class OrganizationSerializer(OrganizationSummarySerializer): def get_attrs( self, item_list: Sequence[Organization], user: User | RpcUser | AnonymousUser, **kwargs: Any ) -> MutableMapping[Organization, MutableMapping[str, Any]]: @@ -601,6 +602,15 @@ def get_attrs( return attrs + def _get_default_automated_run_stopping_point(self, obj: Organization) -> str: + stopping_point = obj.get_option( + "sentry:default_automated_run_stopping_point", + SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + ) + if stopping_point not in get_valid_automated_run_stopping_points(obj): + return SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + return stopping_point + def serialize( # type: ignore[override] self, obj: Organization, @@ -608,7 +618,7 @@ def serialize( # type: ignore[override] user: User | RpcUser | AnonymousUser, access: Access, **kwargs: Any, - ) -> DetailedOrganizationSerializerResponse: + ) -> OrganizationSerializerResponse: # TODO: rectify access argument overriding parent if we want to remove above type ignore include_feature_flags = kwargs.get("include_feature_flags", True) @@ -640,7 +650,7 @@ def serialize( # type: ignore[override] sample_rate = quotas.backend.get_blended_sample_rate(organization_id=obj.id) is_dynamically_sampled = sample_rate is not None and sample_rate < 1.0 - context: DetailedOrganizationSerializerResponse = { + context: OrganizationSerializerResponse = { **base, "experiments": features.get_experiment_assignments(obj, actor=user), "isDefault": obj.is_default, @@ -741,10 +751,7 @@ def serialize( # type: ignore[override] "defaultCodingAgentIntegrationId": obj.get_option( "sentry:seer_default_coding_agent_integration_id", None ), - "defaultAutomatedRunStoppingPoint": obj.get_option( - "sentry:default_automated_run_stopping_point", - SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, - ), + "defaultAutomatedRunStoppingPoint": self._get_default_automated_run_stopping_point(obj), "autoOpenPrs": bool( obj.get_option( "sentry:auto_open_prs", @@ -840,14 +847,12 @@ def serialize( # type: ignore[override] "replayAccessMembers", ] ) -class DetailedOrganizationSerializerWithProjectsAndTeamsResponse( - DetailedOrganizationSerializerResponse -): +class OrganizationWithProjectsAndTeamsSerializerResponse(OrganizationSerializerResponse): teams: list[TeamSerializerResponse] projects: list[OrganizationProjectResponse] -class DetailedOrganizationSerializerWithProjectsAndTeams(DetailedOrganizationSerializer): +class OrganizationWithProjectsAndTeamsSerializer(OrganizationSerializer): def get_attrs( self, item_list: Sequence[Organization], user: User | RpcUser | AnonymousUser, **kwargs: Any ) -> MutableMapping[Organization, MutableMapping[str, Any]]: @@ -884,7 +889,7 @@ def serialize( # type: ignore[override] user: User | RpcUser | AnonymousUser, access: Access, **kwargs: Any, - ) -> DetailedOrganizationSerializerWithProjectsAndTeamsResponse: + ) -> OrganizationWithProjectsAndTeamsSerializerResponse: from sentry.api.serializers.models.project import ( LATEST_DEPLOYS_KEY, ProjectSummarySerializer, @@ -892,7 +897,7 @@ def serialize( # type: ignore[override] from sentry.api.serializers.models.team import TeamSerializer context = cast( - DetailedOrganizationSerializerWithProjectsAndTeamsResponse, + OrganizationWithProjectsAndTeamsSerializerResponse, super().serialize(obj, attrs, user, access, **kwargs), ) @@ -915,3 +920,8 @@ def serialize( # type: ignore[override] ) return context + + +# Backwards-compatible aliases for getsentry +DetailedOrganizationSerializer = OrganizationSerializer +DetailedOrganizationSerializerWithProjectsAndTeams = OrganizationWithProjectsAndTeamsSerializer diff --git a/src/sentry/api/serializers/models/project.py b/src/sentry/api/serializers/models/project.py index db0ad6d49ecd0f..002100c0f18207 100644 --- a/src/sentry/api/serializers/models/project.py +++ b/src/sentry/api/serializers/models/project.py @@ -53,7 +53,7 @@ from sentry.users.services.user.model import RpcUser if TYPE_CHECKING: - from sentry.api.serializers.models.organization import OrganizationSerializerResponse + from sentry.api.serializers.models.organization import OrganizationSummarySerializerResponse STATUS_LABELS = { ObjectStatus.ACTIVE: "active", @@ -951,7 +951,7 @@ class DetailedProjectResponse(ProjectWithTeamResponseDict): secondaryGroupingExpiry: int secondaryGroupingConfig: str | None fingerprintingRules: str - organization: OrganizationSerializerResponse + organization: OrganizationSummarySerializerResponse plugins: list[Plugin] platforms: list[str] processingIssues: int diff --git a/src/sentry/api/serializers/models/team.py b/src/sentry/api/serializers/models/team.py index d4d3dfeccdc235..750394497ba4dd 100644 --- a/src/sentry/api/serializers/models/team.py +++ b/src/sentry/api/serializers/models/team.py @@ -34,7 +34,7 @@ if TYPE_CHECKING: from sentry.api.serializers import SCIMMeta - from sentry.api.serializers.models.organization import OrganizationSerializerResponse + from sentry.api.serializers.models.organization import OrganizationSummarySerializerResponse from sentry.api.serializers.models.project import ProjectSerializerResponse from sentry.integrations.api.serializers.models.external_actor import ExternalActorResponse @@ -124,7 +124,7 @@ def get_access_requests( class _TeamSerializerResponseOptional(TypedDict, total=False): externalTeams: list[ExternalActorResponse] - organization: OrganizationSerializerResponse + organization: OrganizationSummarySerializerResponse projects: list[ProjectSerializerResponse] diff --git a/src/sentry/api/urls.py b/src/sentry/api/urls.py index 30225f90c21639..d69b2353899f55 100644 --- a/src/sentry/api/urls.py +++ b/src/sentry/api/urls.py @@ -621,6 +621,7 @@ SentryInternalAppTokensEndpoint, ) from sentry.synapse.endpoints.org_cell_mappings import OrgCellMappingsEndpoint +from sentry.synapse.endpoints.project_key_cell_mappings import ProjectKeyCellMappingsEndpoint from sentry.tempest.endpoints.tempest_credentials import TempestCredentialsEndpoint from sentry.tempest.endpoints.tempest_credentials_details import TempestCredentialsDetailsEndpoint from sentry.tempest.endpoints.tempest_ips import TempestIpsEndpoint @@ -3660,6 +3661,11 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]: OrgCellMappingsEndpoint.as_view(), name="sentry-api-0-org-cell-mappings", ), + re_path( + r"^projectkey-cell-mappings/$", + ProjectKeyCellMappingsEndpoint.as_view(), + name="sentry-api-0-projectkey-cell-mappings", + ), *preprod_urls.preprod_internal_urlpatterns, *notification_platform_urls.internal_urlpatterns, ] diff --git a/src/sentry/api/utils.py b/src/sentry/api/utils.py index 38bdc91993be74..9e78e5a551a77d 100644 --- a/src/sentry/api/utils.py +++ b/src/sentry/api/utils.py @@ -310,7 +310,7 @@ def generate_locality_url(locality_name: str | None = None) -> str: If locality_name is not provided, it is inferred from the running silo: in CELL mode the local cell's locality is used; in MONOLITH mode with - SENTRY_REGION set the corresponding locality is resolved from that cell name. + SENTRY_LOCAL_CELL set the corresponding locality is resolved from that cell name. Falls back to system.url-prefix when no template or locality name is available. """ region_url_template: str | None = options.get("system.region-api-url-template") @@ -320,9 +320,9 @@ def generate_locality_url(locality_name: str | None = None) -> str: if ( locality_name is None and SiloMode.get_current_mode() == SiloMode.MONOLITH - and settings.SENTRY_REGION + and settings.SENTRY_LOCAL_CELL ): - locality_name = get_locality_name_for_cell(settings.SENTRY_REGION) + locality_name = get_locality_name_for_cell(settings.SENTRY_LOCAL_CELL) if not region_url_template or not locality_name: return options.get("system.url-prefix") return region_url_template.replace("{region}", locality_name) diff --git a/src/sentry/build/_integration_docs.py b/src/sentry/build/_integration_docs.py index 879dc0d36a5c93..f05aebfee99cdc 100644 --- a/src/sentry/build/_integration_docs.py +++ b/src/sentry/build/_integration_docs.py @@ -87,7 +87,7 @@ def _sync_docs(dest: str, *, quiet: bool = False) -> None: # This value is derived from https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor MAX_THREADS = 32 thread_count = min(len(data["platforms"]), multiprocessing.cpu_count() * 5, MAX_THREADS) - with concurrent.futures.ThreadPoolExecutor(thread_count) as exe: + with concurrent.futures.ThreadPoolExecutor(thread_count) as exe: # noqa: S016 - build script, no sentry_sdk available for future in concurrent.futures.as_completed( exe.submit( _sync_one, diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index 2f8c70b2005b52..37584bd50d4629 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -391,6 +391,7 @@ def env( "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "sentry.middleware.auth.AuthenticationMiddleware", + "sentry.middleware.viewer_context.ViewerContextMiddleware", "sentry.middleware.ai_agent.AIAgentMiddleware", "sentry.middleware.integrations.IntegrationControlMiddleware", APIGW_MIDDLEWARE, @@ -3262,7 +3263,7 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: # Addresses are hardcoded based on the defaults # we use in commands/devserver. region_port = os.environ.get("SENTRY_REGION_SILO_PORT", "8010") - SENTRY_REGION_CONFIG = [ + SENTRY_CELLS = [ { "name": "us", "snowflake_id": 1, @@ -3270,7 +3271,10 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: "address": f"http://127.0.0.1:{region_port}", } ] - SENTRY_MONOLITH_REGION = SENTRY_REGION_CONFIG[0]["name"] + SENTRY_MONOLITH_REGION = SENTRY_CELLS[0]["name"] + + # TODO(cells): remove after getsentry updated + SENTRY_REGION_CONFIG = SENTRY_CELLS # Cross region RPC authentication RPC_SHARED_SECRET = [ diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index 9f528e0c656145..a4c784f6e60ade 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -26,7 +26,7 @@ from sentry.api.serializers.models import organization as org_serializers from sentry.api.serializers.models.organization import ( BaseOrganizationSerializer, - DetailedOrganizationSerializerWithProjectsAndTeams, + OrganizationWithProjectsAndTeamsSerializer, TrustedRelaySerializer, ) from sentry.apidocs.constants import ( @@ -110,6 +110,7 @@ from sentry.relay.datascrubbing import validate_pii_config_update, validate_pii_selectors from sentry.replays.models import OrganizationMemberReplayAccess from sentry.seer.autofix.constants import AutofixAutomationTuningSettings +from sentry.seer.autofix.utils import get_valid_automated_run_stopping_points from sentry.services.organization.provisioning import organization_provisioning_service from sentry.tasks.console_platform_cleanup import remove_inaccessible_console_platform_sources from sentry.users.services.user.serial import serialize_generic_user @@ -384,9 +385,7 @@ class OrganizationSerializer(BaseOrganizationSerializer): allow_null=True, ) defaultCodingAgentIntegrationId = serializers.IntegerField(required=False, allow_null=True) - defaultAutomatedRunStoppingPoint = serializers.ChoiceField( - choices=["code_changes", "open_pr"], required=False - ) + defaultAutomatedRunStoppingPoint = serializers.CharField(required=False) autoOpenPrs = serializers.BooleanField(required=False) autoEnableCodeReview = serializers.BooleanField(required=False) defaultCodeReviewTriggers = serializers.ListField( @@ -434,6 +433,12 @@ def validate_defaultCodingAgentIntegrationId(self, value: int | None) -> int | N raise serializers.ValidationError("Integration does not exist.") return value + def validate_defaultAutomatedRunStoppingPoint(self, value: str) -> str: + organization = self.context["organization"] + if value not in get_valid_automated_run_stopping_points(organization): + raise serializers.ValidationError(f'"{value}" is not a valid choice.') + return value + def validate_sensitiveFields(self, value): if value and not all(value): raise serializers.ValidationError("Empty values are not allowed.") @@ -1133,7 +1138,7 @@ class OrganizationDetailsEndpoint(OrganizationEndpoint): parameters=[GlobalParams.ORG_ID_OR_SLUG, OrganizationParams.DETAILED], request=None, responses={ - 200: org_serializers.OrganizationSerializer, + 200: org_serializers.OrganizationSummarySerializer, 401: RESPONSE_UNAUTHORIZED, 403: RESPONSE_FORBIDDEN, 404: RESPONSE_NOT_FOUND, @@ -1148,14 +1153,14 @@ def get(self, request: Request, organization: Organization) -> Response: # This param will be used to determine if we should include feature flags in the response include_feature_flags = request.GET.get("include_feature_flags", "0") != "0" - serializer = org_serializers.OrganizationSerializer + serializer = org_serializers.OrganizationSummarySerializer if request.access.has_scope("org:read") or is_active_staff(request): is_detailed = request.GET.get("detailed", "1") != "0" - serializer = org_serializers.DetailedOrganizationSerializer + serializer = org_serializers.OrganizationSerializer if is_detailed: - serializer = org_serializers.DetailedOrganizationSerializerWithProjectsAndTeams + serializer = org_serializers.OrganizationWithProjectsAndTeamsSerializer context = serialize( organization, @@ -1174,7 +1179,7 @@ def get(self, request: Request, organization: Organization) -> Response: ], request=OrganizationDetailsPutSerializer, responses={ - 200: DetailedOrganizationSerializerWithProjectsAndTeams, + 200: OrganizationWithProjectsAndTeamsSerializer, 400: RESPONSE_BAD_REQUEST, 401: RESPONSE_UNAUTHORIZED, 403: RESPONSE_FORBIDDEN, @@ -1339,7 +1344,7 @@ def put(self, request: Request, organization: Organization) -> Response: context = serialize( organization, request.user, - org_serializers.DetailedOrganizationSerializerWithProjectsAndTeams(), + org_serializers.OrganizationWithProjectsAndTeamsSerializer(), access=request.access, include_feature_flags=include_feature_flags, ) @@ -1417,7 +1422,7 @@ def handle_delete(self, request: Request, organization: Organization): context = serialize( organization, request.user, - org_serializers.DetailedOrganizationSerializerWithProjectsAndTeams(), + org_serializers.OrganizationWithProjectsAndTeamsSerializer(), access=request.access, ) return self.respond(context, status=202) diff --git a/src/sentry/core/endpoints/organization_index.py b/src/sentry/core/endpoints/organization_index.py index 542e80e59ed549..1723b6c0274fbf 100644 --- a/src/sentry/core/endpoints/organization_index.py +++ b/src/sentry/core/endpoints/organization_index.py @@ -22,7 +22,7 @@ from sentry.api.serializers import serialize from sentry.api.serializers.models.organization import ( BaseOrganizationSerializer, - OrganizationSerializerResponse, + OrganizationSummarySerializerResponse, ) from sentry.apidocs.constants import RESPONSE_FORBIDDEN, RESPONSE_NOT_FOUND, RESPONSE_UNAUTHORIZED from sentry.apidocs.examples.user_examples import UserExamples @@ -88,7 +88,7 @@ class OrganizationIndexEndpoint(Endpoint): request=None, responses={ 200: inline_sentry_response_serializer( - "ListOrganizations", list[OrganizationSerializerResponse] + "ListOrganizations", list[OrganizationSummarySerializerResponse] ), 401: RESPONSE_UNAUTHORIZED, 403: RESPONSE_FORBIDDEN, @@ -269,7 +269,7 @@ def post(self, request: Request) -> Response: ) rpc_org = organization_provisioning_service.provision_organization_in_cell( - cell_name=settings.SENTRY_REGION or settings.SENTRY_MONOLITH_REGION, + cell_name=settings.SENTRY_LOCAL_CELL or settings.SENTRY_MONOLITH_REGION, provisioning_options=provision_args, ) org = Organization.objects.get(id=rpc_org.id) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index 865c7346a27f27..00f103ab2aebce 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -127,7 +127,7 @@ def register_temporary_features(manager: FeatureManager) -> None: # Enable GenAI consent manager.add("organizations:gen-ai-consent", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable increased issue_owners rate limit for auto-assignment - manager.add("organizations:increased-issue-owners-rate-limit", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False) + manager.add("organizations:increased-issue-owners-rate-limit", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Starfish: extract metrics from the spans manager.add("organizations:indexed-spans-extraction", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False) # These flags follow the pattern expected by IntegrationProvider.requires_feature_flag's usage on the config endpoint @@ -157,8 +157,8 @@ def register_temporary_features(manager: FeatureManager) -> None: # Enable Session Stats down to a minute resolution manager.add("organizations:minute-resolution-sessions", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, default=True, api_expose=True) # Enables higher limit for alert rules - manager.add("organizations:more-fast-alerts", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False) - manager.add("organizations:more-slow-alerts", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False) + manager.add("organizations:more-fast-alerts", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) + manager.add("organizations:more-slow-alerts", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable higher limit for workflows manager.add("organizations:more-workflows", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Normalize segment names during span enrichment @@ -189,6 +189,8 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:onboarding-new-welcome-ui", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable SCM-first onboarding flow with provider connection, platform detection, and feature selection steps manager.add("organizations:onboarding-scm", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) + # Experiment: SCM onboarding A/B test measuring conversion impact + manager.add("organizations:onboarding-scm-experiment", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable large ownership rule file size limit manager.add("organizations:ownership-size-limit-large", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable xlarge ownership rule file size limit @@ -284,7 +286,7 @@ def register_temporary_features(manager: FeatureManager) -> None: # Enable revocation of org auth keys when a user renames an org slug manager.add("organizations:revoke-org-auth-on-slug-rename", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable detecting SDK crashes during event processing - manager.add("organizations:sdk-crash-detection", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False) + manager.add("organizations:sdk-crash-detection", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable the Seer Config Reminder in the primary nav manager.add("organizations:seer-config-reminder", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable Seer Explorer panel for AI-powered data exploration @@ -301,8 +303,12 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:seer-explorer-context-engine-fe-override-ui-flag", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable code editing tools in Seer Explorer chat manager.add("organizations:seer-explorer-chat-coding", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) + # Enable structured LLM context (JSON snapshot) instead of ASCII DOM snapshot + manager.add("organizations:context-engine-structured-page-context", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable the Seer Overview sections for Seat-Based users manager.add("organizations:seer-overview", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) + # Allow root_cause as a valid automated run stopping point and org-level default + manager.add("organizations:root-cause-stopping-point", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable the Seer Wizard and related prompts/links/banners manager.add("organizations:seer-wizard", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable the Seer issues view @@ -344,6 +350,8 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:init-sentry-toolbar", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, default=False, api_expose=True) # Enable new stack trace component for issue details manager.add("organizations:issue-details-new-stack-trace", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) + # Remove trace and breadcrumbs from issue summary input + manager.add("organizations:issue-summary-experimental", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable suspect feature tags endpoint. manager.add("organizations:issues-suspect-tags", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Lets organizations manage grouping configs @@ -436,18 +444,9 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:workflow-engine-redirect-opt-out", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Use workflow engine serializers to return data for old rule / incident endpoints manager.add("organizations:workflow-engine-rule-serializers", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Use workflow engine exclusively for ProjectRulesEndpoint.get results. - # See src/sentry/workflow_engine/docs/legacy_backport.md for context. - manager.add("organizations:workflow-engine-projectrulesendpoint-get", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) - # Use workflow engine exclusively for ProjectRuleDetailsEndpoint.get results. - # See src/sentry/workflow_engine/docs/legacy_backport.md for context. - manager.add("organizations:workflow-engine-projectruledetailsendpoint-get", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Use workflow engine exclusively for OrganizationCombinedRuleIndexEndpoint.get results. # See src/sentry/workflow_engine/docs/legacy_backport.md for context. manager.add("organizations:workflow-engine-combinedruleindex-get", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) - # Use workflow engine exclusively for ProjectRuleGroupHistoryIndexEndpoint.get and ProjectRuleStatsIndexEndpoint.get results. - # See src/sentry/workflow_engine/docs/legacy_backport.md for context. - manager.add("organizations:workflow-engine-projectrulegroupstats-get", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Use workflow engine exclusively for legacy issue alert rule.get results. # See src/sentry/workflow_engine/docs/legacy_backport.md for context. manager.add("organizations:workflow-engine-issue-alert-endpoints-get", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) @@ -523,7 +522,7 @@ def register_temporary_features(manager: FeatureManager) -> None: # Enable error upsampling manager.add("projects:error-upsampling", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, default=False, api_expose=True) # Enable calculating a severity score for events which create a new group - manager.add("projects:first-event-severity-calculation", ProjectFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False) + manager.add("projects:first-event-severity-calculation", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable similarity embeddings API call # This feature is only available on the frontend using project details since the handler gets # project options and this is slow in the project index endpoint feature flag serialization diff --git a/src/sentry/hybridcloud/models/outbox.py b/src/sentry/hybridcloud/models/outbox.py index be03c109ef43e4..99301a2b4d567c 100644 --- a/src/sentry/hybridcloud/models/outbox.py +++ b/src/sentry/hybridcloud/models/outbox.py @@ -34,7 +34,7 @@ ) from sentry.hybridcloud.outbox.category import OutboxCategory, OutboxScope from sentry.hybridcloud.outbox.signals import process_cell_outbox, process_control_outbox -from sentry.hybridcloud.rpc import REGION_NAME_LENGTH +from sentry.hybridcloud.rpc import CELL_NAME_LENGTH from sentry.silo.base import SiloMode from sentry.silo.safety import unguarded_write from sentry.utils import metrics @@ -468,7 +468,7 @@ class ControlOutboxBase(OutboxBase): "object_identifier", ) - cell_name = models.CharField(max_length=REGION_NAME_LENGTH, db_column="region_name") + cell_name = models.CharField(max_length=CELL_NAME_LENGTH, db_column="region_name") def send_signal(self) -> None: process_control_outbox.send( diff --git a/src/sentry/hybridcloud/models/webhookpayload.py b/src/sentry/hybridcloud/models/webhookpayload.py index 45a4faac75b6ef..db90d0bb0377d8 100644 --- a/src/sentry/hybridcloud/models/webhookpayload.py +++ b/src/sentry/hybridcloud/models/webhookpayload.py @@ -20,7 +20,7 @@ class DestinationType(TextChoices): - SENTRY_REGION = "sentry_region" + SENTRY_CELL = "sentry_region" CODECOV = "codecov" @@ -34,7 +34,7 @@ class WebhookPayload(Model): # Destination attributes # Table is constantly being deleted from so let's make this non-nullable with a default value, since the table should be small at any given point in time. destination_type = models.CharField( - choices=DestinationType.choices, null=False, db_default=DestinationType.SENTRY_REGION + choices=DestinationType.choices, null=False, db_default=DestinationType.SENTRY_CELL ) cell_name = models.CharField(null=True, db_column="region_name") @@ -81,7 +81,7 @@ class Meta: constraints = [ models.CheckConstraint( - condition=~Q(destination_type=DestinationType.SENTRY_REGION) + condition=~Q(destination_type=DestinationType.SENTRY_CELL) | Q(cell_name__isnull=False), name="webhookpayload_region_name_not_null", ), diff --git a/src/sentry/hybridcloud/outbox/base.py b/src/sentry/hybridcloud/outbox/base.py index 94b3106c5f0acb..0d0267be939abe 100644 --- a/src/sentry/hybridcloud/outbox/base.py +++ b/src/sentry/hybridcloud/outbox/base.py @@ -182,7 +182,7 @@ def outbox_for_update(self, shard_identifier: int | None = None) -> CellOutboxBa Subclasses generally should override payload_for_update to customize this behavior. """ - return self.category.as_region_outbox( + return self.category.as_cell_outbox( model=self, payload=self.payload_for_update(), shard_identifier=shard_identifier, diff --git a/src/sentry/hybridcloud/outbox/category.py b/src/sentry/hybridcloud/outbox/category.py index 8bc9baf0d14471..3d8137ebfd16ca 100644 --- a/src/sentry/hybridcloud/outbox/category.py +++ b/src/sentry/hybridcloud/outbox/category.py @@ -130,7 +130,7 @@ def get_scope(self) -> OutboxScope: raise KeyError return OutboxScope(scope_int) - def as_region_outbox( + def as_cell_outbox( self, model: Any | None = None, payload: dict[str, Any] | None = None, diff --git a/src/sentry/hybridcloud/rpc/__init__.py b/src/sentry/hybridcloud/rpc/__init__.py index 35a62e187c3ca3..46fa54612fa8c8 100644 --- a/src/sentry/hybridcloud/rpc/__init__.py +++ b/src/sentry/hybridcloud/rpc/__init__.py @@ -23,7 +23,7 @@ OptionValue = Any IDEMPOTENCY_KEY_LENGTH = 48 -REGION_NAME_LENGTH = 48 +CELL_NAME_LENGTH = 48 DEFAULT_DATE = datetime.datetime(2000, 1, 1, tzinfo=datetime.UTC) diff --git a/src/sentry/hybridcloud/tasks/deliver_webhooks.py b/src/sentry/hybridcloud/tasks/deliver_webhooks.py index e8d9d3411e47d9..e2649daa77d584 100644 --- a/src/sentry/hybridcloud/tasks/deliver_webhooks.py +++ b/src/sentry/hybridcloud/tasks/deliver_webhooks.py @@ -436,9 +436,7 @@ def _record_delivery_time_metrics(payload: WebhookPayload) -> None: """Record delivery time metrics for a successfully delivered webhook payload.""" duration = timezone.now() - payload.date_added cell_sent_to = ( - payload.cell_name - if payload.destination_type == DestinationType.SENTRY_REGION - else "codecov" + payload.cell_name if payload.destination_type == DestinationType.SENTRY_CELL else "codecov" ) tags = {"region_sent_to": cell_sent_to} | _get_github_delivery_time_tags(payload) metrics.distribution( @@ -622,7 +620,7 @@ def perform_request(payload: WebhookPayload) -> None: destination_type = payload.destination_type match destination_type: - case DestinationType.SENTRY_REGION: + case DestinationType.SENTRY_CELL: assert payload.cell_name is not None cell = get_cell_by_name(name=payload.cell_name) perform_cell_request(cell, payload) diff --git a/src/sentry/incidents/endpoints/serializers/workflow_engine_detector.py b/src/sentry/incidents/endpoints/serializers/workflow_engine_detector.py index 66bc79545609ed..050a016f30cf0d 100644 --- a/src/sentry/incidents/endpoints/serializers/workflow_engine_detector.py +++ b/src/sentry/incidents/endpoints/serializers/workflow_engine_detector.py @@ -88,17 +88,16 @@ def add_triggers_and_actions( detectors: dict[int, Detector], sentry_app_installations_by_sentry_app_id: Mapping[str, RpcSentryAppComponentContext], serialized_data_conditions: list[dict[str, Any]], + detector_ids_by_alert_rule_id: dict[int, int], ) -> None: for serialized in serialized_data_conditions: errors = [] alert_rule_id = serialized.get("alertRuleId") assert alert_rule_id - try: - detector_id = AlertRuleDetector.objects.values_list("detector_id", flat=True).get( - alert_rule_id=alert_rule_id - ) - except AlertRuleDetector.DoesNotExist: - detector_id = get_object_id_from_fake_id(int(alert_rule_id)) + detector_id = detector_ids_by_alert_rule_id.get( + int(alert_rule_id), + get_object_id_from_fake_id(int(alert_rule_id)), + ) detector = detectors[int(detector_id)] alert_rule_triggers = result[detector].setdefault("triggers", []) @@ -273,6 +272,16 @@ def get_attrs( self.add_sentry_app_installations_by_sentry_app_id(actions, organization_id) ) + # Batch-fetch AlertRuleDetector mappings (used by add_triggers_and_actions and serialize) + alert_rule_ids_by_detector_id = dict( + AlertRuleDetector.objects.filter(detector_id__in=detector_ids).values_list( + "detector_id", "alert_rule_id" + ) + ) + detector_ids_by_alert_rule_id: dict[int, int] = { + v: k for k, v in alert_rule_ids_by_detector_id.items() if v is not None + } + # add trigger and action data # Evaluate queryset once and reuse for both serialization and lookup dict detector_trigger_data_conditions_list = list(detector_trigger_data_conditions) @@ -287,6 +296,7 @@ def get_attrs( detectors, sentry_app_installations_by_sentry_app_id, serialized_data_conditions, + detector_ids_by_alert_rule_id, ) # derive thresholdType and sensitivity/seasonality from trigger data conditions # Build a dict to avoid N queries when looking up by condition_group_id @@ -317,11 +327,6 @@ def get_attrs( self.add_created_by(result, list(detectors.values())) self.add_owner(result, list(detectors.values())) - alert_rule_ids_by_detector_id = dict( - AlertRuleDetector.objects.filter(detector_id__in=detector_ids).values_list( - "detector_id", "alert_rule_id" - ) - ) for detector in detectors.values(): result[detector]["alert_rule_id"] = alert_rule_ids_by_detector_id.get(detector.id) diff --git a/src/sentry/incidents/subscription_processor.py b/src/sentry/incidents/subscription_processor.py index b031d24f8e7c8b..6276ab2701ef71 100644 --- a/src/sentry/incidents/subscription_processor.py +++ b/src/sentry/incidents/subscription_processor.py @@ -53,6 +53,46 @@ class MetricIssueDetectorConfig(TypedDict): detection_type: Literal["static", "percent", "dynamic"] +def has_downgraded(dataset: str, organization: Organization) -> bool: + """ + Check if the organization has downgraded since the subscription was created. + """ + supports_metrics_issues = features.has("organizations:incidents", organization) + if dataset == Dataset.Events.value and not supports_metrics_issues: + metrics.incr("incidents.alert_rules.ignore_update_missing_incidents") + return True + + supports_performance_view = features.has("organizations:performance-view", organization) + if dataset == Dataset.Transactions.value and not ( + supports_metrics_issues and supports_performance_view + ): + metrics.incr("incidents.alert_rules.ignore_update_missing_incidents_performance") + return True + + supports_explore_view = features.has("organizations:visibility-explore-view", organization) + if dataset == Dataset.EventsAnalyticsPlatform.value and not ( + supports_metrics_issues and supports_explore_view + ): + metrics.incr("incidents.alert_rules.ignore_update_missing_incidents_eap") + return True + + if dataset == Dataset.PerformanceMetrics.value and not features.has( + "organizations:on-demand-metrics-extraction", organization + ): + metrics.incr("incidents.alert_rules.ignore_update_missing_on_demand") + return True + + if not supports_metrics_issues: + # These should probably be downgraded, but we should know the impact first. + metrics.incr( + "incidents.alert_rules.no_incidents_not_downgraded", + sample_rate=1.0, + tags={"dataset": dataset}, + ) + + return False + + class SubscriptionProcessor: """ Class for processing subscription updates for workflow engine. @@ -220,28 +260,6 @@ def process_results_workflow_engine( ) return results - def has_downgraded(self, dataset: str, organization: Organization) -> bool: - """ - Check if the organization has downgraded since the subscription was created, return early if True - """ - if dataset == "events" and not features.has("organizations:incidents", organization): - metrics.incr("incidents.alert_rules.ignore_update_missing_incidents") - return True - - elif dataset == "transactions" and not features.has( - "organizations:performance-view", organization - ): - metrics.incr("incidents.alert_rules.ignore_update_missing_incidents_performance") - return True - - elif dataset == "generic_metrics" and not features.has( - "organizations:on-demand-metrics-extraction", organization - ): - metrics.incr("incidents.alert_rules.ignore_update_missing_on_demand") - return True - - return False - def process_update(self, subscription_update: QuerySubscriptionUpdate) -> bool: """ Core processing method. Assumes subscription has cached project/organization @@ -250,7 +268,7 @@ def process_update(self, subscription_update: QuerySubscriptionUpdate) -> bool: dataset = self.subscription.snuba_query.dataset organization = self.subscription.project.organization - if self.has_downgraded(dataset, organization): + if has_downgraded(dataset, organization): return False if subscription_update["timestamp"] <= self.last_update: diff --git a/src/sentry/integrations/api/endpoints/organization_integration_repos.py b/src/sentry/integrations/api/endpoints/organization_integration_repos.py index b317b166affb3f..a633deca9c809f 100644 --- a/src/sentry/integrations/api/endpoints/organization_integration_repos.py +++ b/src/sentry/integrations/api/endpoints/organization_integration_repos.py @@ -49,6 +49,10 @@ def get( :qparam string search: Name fragment to search repositories by. :qparam bool installableOnly: If true, return only repositories that can be installed. If false or not provided, return all repositories. + :qparam bool accessibleOnly: If true, only return repositories that the integration + installation has access to, filtering locally instead of + using the provider's search API which may return results + beyond the installation's scope. """ integration = self.get_integration(organization.id, integration_id) @@ -63,8 +67,11 @@ def get( install = integration.get_installation(organization_id=organization.id) if isinstance(install, RepositoryIntegration): + search = request.GET.get("search") + accessible_only = request.GET.get("accessibleOnly", "false").lower() == "true" + try: - repositories = install.get_repositories(request.GET.get("search")) + repositories = install.get_repositories(search, accessible_only=accessible_only) except (IntegrationError, IdentityNotValid) as e: return self.respond({"detail": str(e)}, status=400) diff --git a/src/sentry/integrations/bitbucket/integration.py b/src/sentry/integrations/bitbucket/integration.py index 2d91a5118b5e0d..757f717cab3593 100644 --- a/src/sentry/integrations/bitbucket/integration.py +++ b/src/sentry/integrations/bitbucket/integration.py @@ -121,7 +121,10 @@ def error_message_from_json(self, data): # RepositoryIntegration methods def get_repositories( - self, query: str | None = None, page_number_limit: int | None = None + self, + query: str | None = None, + page_number_limit: int | None = None, + accessible_only: bool = False, ) -> list[dict[str, Any]]: username = self.model.metadata.get("uuid", self.username) if not query: diff --git a/src/sentry/integrations/bitbucket_server/integration.py b/src/sentry/integrations/bitbucket_server/integration.py index 0c704a39782cdd..8dbf0fa288471c 100644 --- a/src/sentry/integrations/bitbucket_server/integration.py +++ b/src/sentry/integrations/bitbucket_server/integration.py @@ -281,7 +281,10 @@ def error_message_from_json(self, data): # RepositoryIntegration methods def get_repositories( - self, query: str | None = None, page_number_limit: int | None = None + self, + query: str | None = None, + page_number_limit: int | None = None, + accessible_only: bool = False, ) -> list[dict[str, Any]]: if not query: resp = self.get_client().get_repos() diff --git a/src/sentry/integrations/example/integration.py b/src/sentry/integrations/example/integration.py index cbe2e508b42848..2dc18b17334ec7 100644 --- a/src/sentry/integrations/example/integration.py +++ b/src/sentry/integrations/example/integration.py @@ -147,7 +147,10 @@ def get_issue(self, issue_id, **kwargs): } def get_repositories( - self, query: str | None = None, page_number_limit: int | None = None + self, + query: str | None = None, + page_number_limit: int | None = None, + accessible_only: bool = False, ) -> list[dict[str, Any]]: return [{"name": "repo", "identifier": "user/repo"}] diff --git a/src/sentry/integrations/github/integration.py b/src/sentry/integrations/github/integration.py index 67b6a9b3a193f2..88086dee4c34c8 100644 --- a/src/sentry/integrations/github/integration.py +++ b/src/sentry/integrations/github/integration.py @@ -318,18 +318,24 @@ def extract_source_path_from_source_url(self, repo: Repository, url: str) -> str return source_path def get_repositories( - self, query: str | None = None, page_number_limit: int | None = None + self, + query: str | None = None, + page_number_limit: int | None = None, + accessible_only: bool = False, ) -> list[dict[str, Any]]: """ args: * query - a query to filter the repositories by + * accessible_only - when True with a query, fetch only installation- + accessible repos and filter locally instead of using the Search API + (which may return repos outside the installation's scope) This fetches all repositories accessible to the Github App https://docs.github.com/en/rest/apps/installations#list-repositories-accessible-to-the-app-installation """ - if not query: + if not query or accessible_only: all_repos = self.get_client().get_repos(page_number_limit=page_number_limit) - return [ + repos = [ { "name": i["name"], "identifier": i["full_name"], @@ -338,6 +344,10 @@ def get_repositories( for i in all_repos if not i.get("archived") ] + if query: + query_lower = query.lower() + repos = [r for r in repos if query_lower in r["identifier"].lower()] + return repos full_query = build_repository_query(self.model.metadata, self.model.name, query) response = self.get_client().search_repositories(full_query) diff --git a/src/sentry/integrations/github_enterprise/integration.py b/src/sentry/integrations/github_enterprise/integration.py index db7bb7f941691c..5981801345dcd7 100644 --- a/src/sentry/integrations/github_enterprise/integration.py +++ b/src/sentry/integrations/github_enterprise/integration.py @@ -216,7 +216,10 @@ def message_from_error(self, exc: Exception) -> str: # RepositoryIntegration methods def get_repositories( - self, query: str | None = None, page_number_limit: int | None = None + self, + query: str | None = None, + page_number_limit: int | None = None, + accessible_only: bool = False, ) -> list[dict[str, Any]]: if not query: all_repos = self.get_client().get_repos(page_number_limit=page_number_limit) diff --git a/src/sentry/integrations/gitlab/integration.py b/src/sentry/integrations/gitlab/integration.py index 5febeeeb7d10ab..2054eb28b1436c 100644 --- a/src/sentry/integrations/gitlab/integration.py +++ b/src/sentry/integrations/gitlab/integration.py @@ -165,7 +165,10 @@ def has_repo_access(self, repo: RpcRepository) -> bool: return False def get_repositories( - self, query: str | None = None, page_number_limit: int | None = None + self, + query: str | None = None, + page_number_limit: int | None = None, + accessible_only: bool = False, ) -> list[dict[str, Any]]: try: # Note: gitlab projects are the same things as repos everywhere else diff --git a/src/sentry/integrations/middleware/hybrid_cloud/parser.py b/src/sentry/integrations/middleware/hybrid_cloud/parser.py index 76520dea5e03e8..60ce50f6bcce49 100644 --- a/src/sentry/integrations/middleware/hybrid_cloud/parser.py +++ b/src/sentry/integrations/middleware/hybrid_cloud/parser.py @@ -181,7 +181,7 @@ def get_response_from_webhookpayload( # this loop. Create all payloads first, then trigger a single drain. payloads = [ WebhookPayload.create_from_request( - destination_type=DestinationType.SENTRY_REGION, + destination_type=DestinationType.SENTRY_CELL, cell=cell.name, provider=self.provider, identifier=shard_identifier, diff --git a/src/sentry/integrations/perforce/integration.py b/src/sentry/integrations/perforce/integration.py index 6ef4c3e655823d..921ec2ba9ad48b 100644 --- a/src/sentry/integrations/perforce/integration.py +++ b/src/sentry/integrations/perforce/integration.py @@ -349,7 +349,10 @@ def extract_source_path_from_source_url(self, repo: Repository, url: str) -> str return url def get_repositories( - self, query: str | None = None, page_number_limit: int | None = None + self, + query: str | None = None, + page_number_limit: int | None = None, + accessible_only: bool = False, ) -> list[dict[str, Any]]: """ Get list of depots/streams from Perforce server. diff --git a/src/sentry/integrations/source_code_management/repository.py b/src/sentry/integrations/source_code_management/repository.py index 307241e059913d..da5e9ecc362ff4 100644 --- a/src/sentry/integrations/source_code_management/repository.py +++ b/src/sentry/integrations/source_code_management/repository.py @@ -32,7 +32,10 @@ class BaseRepositoryIntegration(ABC): @abstractmethod def get_repositories( - self, query: str | None = None, page_number_limit: int | None = None + self, + query: str | None = None, + page_number_limit: int | None = None, + accessible_only: bool = False, ) -> list[dict[str, Any]]: """ Get a list of available repositories for an installation @@ -50,6 +53,10 @@ def get_repositories( IntegrationRepositoryProvider.repository_external_slug() You can use the `query` argument to filter repositories. + When `accessible_only` is True and a query is provided, + only repositories the installation has access to are + returned, filtering locally instead of using the provider's + search API. """ raise NotImplementedError diff --git a/src/sentry/integrations/vsts/integration.py b/src/sentry/integrations/vsts/integration.py index 552ba1ca2abda0..c701945cac60ec 100644 --- a/src/sentry/integrations/vsts/integration.py +++ b/src/sentry/integrations/vsts/integration.py @@ -308,7 +308,10 @@ def get_config_data(self) -> Mapping[str, Any]: # RepositoryIntegration methods def get_repositories( - self, query: str | None = None, page_number_limit: int | None = None + self, + query: str | None = None, + page_number_limit: int | None = None, + accessible_only: bool = False, ) -> list[dict[str, Any]]: try: repos = self.get_client().get_repos() diff --git a/src/sentry/middleware/integrations/tasks.py b/src/sentry/middleware/integrations/tasks.py index 416dff03ec6668..ef5dde0491e5fa 100644 --- a/src/sentry/middleware/integrations/tasks.py +++ b/src/sentry/middleware/integrations/tasks.py @@ -123,10 +123,9 @@ def unpack_payload(self, response: Response) -> Any: def convert_to_async_slack_response( payload: dict[str, Any], response_url: str, - cell_names: list[str] | None = None, # TODO(cells): make required once region_names is removed - region_names: list[str] | None = None, # TODO(cells): remove after queue drains + cell_names: list[str], ) -> None: - _AsyncSlackDispatcher(payload, response_url).dispatch(cell_names or region_names or []) + _AsyncSlackDispatcher(payload, response_url).dispatch(cell_names) class _AsyncDiscordDispatcher(_AsyncCellDispatcher): @@ -151,8 +150,7 @@ def unpack_payload(self, response: Response) -> Any: def convert_to_async_discord_response( payload: dict[str, Any], response_url: str, - cell_names: list[str] | None = None, # TODO(cells): make required once region_names is removed - region_names: list[str] | None = None, # TODO(cells): remove after queue drains + cell_names: list[str], ) -> None: """ This task asks relevant cell silos for response data to send asynchronously to Discord. It @@ -161,9 +159,7 @@ def convert_to_async_discord_response( In the event this task finishes prior to returning the above type, the outbound post will fail. """ - response = _AsyncDiscordDispatcher(payload, response_url).dispatch( - cell_names or region_names or [] - ) + response = _AsyncDiscordDispatcher(payload, response_url).dispatch(cell_names) if response is not None and response.status_code == status.HTTP_404_NOT_FOUND: raise Exception("Discord hook is not ready.") diff --git a/src/sentry/middleware/viewer_context.py b/src/sentry/middleware/viewer_context.py new file mode 100644 index 00000000000000..b8b6b48cfe234e --- /dev/null +++ b/src/sentry/middleware/viewer_context.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from collections.abc import Callable + +from django.http.request import HttpRequest +from django.http.response import HttpResponseBase + +from sentry import options +from sentry.viewer_context import ActorType, ViewerContext, viewer_context_scope + + +def ViewerContextMiddleware( + get_response: Callable[[HttpRequest], HttpResponseBase], +) -> Callable[[HttpRequest], HttpResponseBase]: + """Set :class:`ViewerContext` for every request after authentication. + + Must be placed **after** ``AuthenticationMiddleware`` so that + ``request.user`` and ``request.auth`` are already populated. + + Gated by the ``viewer-context.enabled`` option (FLAG_NOSTORE). + Set via deploy config; requires restart to change. + """ + enabled = options.get("viewer-context.enabled") + + def ViewerContextMiddleware_impl(request: HttpRequest) -> HttpResponseBase: + if not enabled: + return get_response(request) + + ctx = _viewer_context_from_request(request) + with viewer_context_scope(ctx): + return get_response(request) + + return ViewerContextMiddleware_impl + + +def _viewer_context_from_request(request: HttpRequest) -> ViewerContext: + user = request.user + auth = getattr(request, "auth", None) + + user_id: int | None = None + if user.is_authenticated: + user_id = user.id + + organization_id: int | None = None + if auth is not None and hasattr(auth, "organization_id"): + organization_id = auth.organization_id + + return ViewerContext( + user_id=user_id, + organization_id=organization_id, + actor_type=ActorType.USER, + token=auth, + ) diff --git a/src/sentry/models/organizationmapping.py b/src/sentry/models/organizationmapping.py index 8241080aa521b1..bbbed9d3e40308 100644 --- a/src/sentry/models/organizationmapping.py +++ b/src/sentry/models/organizationmapping.py @@ -11,7 +11,7 @@ from sentry.db.models import BoundedBigIntegerField, sane_repr from sentry.db.models.base import Model, control_silo_model from sentry.db.models.indexes import IndexWithPostgresNameLimits -from sentry.hybridcloud.rpc import IDEMPOTENCY_KEY_LENGTH, REGION_NAME_LENGTH +from sentry.hybridcloud.rpc import CELL_NAME_LENGTH, IDEMPOTENCY_KEY_LENGTH from sentry.models.organization import OrganizationStatus if TYPE_CHECKING: @@ -39,7 +39,7 @@ class OrganizationMapping(Model): # If a record already exists with the same slug, the organization_id can only be # updated IF the idempotency key is identical. idempotency_key = models.CharField(max_length=IDEMPOTENCY_KEY_LENGTH) - cell_name = models.CharField(max_length=REGION_NAME_LENGTH, db_column="region_name") + cell_name = models.CharField(max_length=CELL_NAME_LENGTH, db_column="region_name") status = BoundedBigIntegerField(choices=OrganizationStatus.as_choices(), null=True) diff --git a/src/sentry/models/organizationslugreservation.py b/src/sentry/models/organizationslugreservation.py index cc86d4099aa537..3b0afa12311123 100644 --- a/src/sentry/models/organizationslugreservation.py +++ b/src/sentry/models/organizationslugreservation.py @@ -13,7 +13,7 @@ from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey from sentry.hybridcloud.outbox.base import ReplicatedControlModel from sentry.hybridcloud.outbox.category import OutboxCategory -from sentry.hybridcloud.rpc import REGION_NAME_LENGTH +from sentry.hybridcloud.rpc import CELL_NAME_LENGTH class OrganizationSlugReservationType(IntEnum): @@ -35,7 +35,7 @@ class OrganizationSlugReservation(ReplicatedControlModel): slug = models.SlugField(unique=True, null=False) organization_id = HybridCloudForeignKey("sentry.organization", null=False, on_delete="CASCADE") user_id = BoundedBigIntegerField(db_index=True, null=True) - cell_name = models.CharField(max_length=REGION_NAME_LENGTH, null=False, db_column="region_name") + cell_name = models.CharField(max_length=CELL_NAME_LENGTH, null=False, db_column="region_name") reservation_type = BoundedBigIntegerField( choices=OrganizationSlugReservationType.as_choices(), null=False, diff --git a/src/sentry/models/organizationslugreservationreplica.py b/src/sentry/models/organizationslugreservationreplica.py index 327580f79062d9..5af6e8b69866a1 100644 --- a/src/sentry/models/organizationslugreservationreplica.py +++ b/src/sentry/models/organizationslugreservationreplica.py @@ -4,7 +4,7 @@ from sentry.backup.scopes import RelocationScope from sentry.db.models import BoundedBigIntegerField, Model, cell_silo_model, sane_repr from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey -from sentry.hybridcloud.rpc import REGION_NAME_LENGTH +from sentry.hybridcloud.rpc import CELL_NAME_LENGTH from sentry.models.organizationslugreservation import OrganizationSlugReservationType @@ -20,7 +20,7 @@ class OrganizationSlugReservationReplica(Model): slug = models.SlugField(unique=True, db_index=True) organization_id = BoundedBigIntegerField(db_index=True) user_id = BoundedBigIntegerField(db_index=True, null=True) - cell_name = models.CharField(max_length=REGION_NAME_LENGTH, null=False, db_column="region_name") + cell_name = models.CharField(max_length=CELL_NAME_LENGTH, null=False, db_column="region_name") reservation_type = BoundedBigIntegerField( choices=OrganizationSlugReservationType.as_choices(), null=False, diff --git a/src/sentry/models/projectkeymapping.py b/src/sentry/models/projectkeymapping.py index a28c14f60897fc..4db2290bc080f5 100644 --- a/src/sentry/models/projectkeymapping.py +++ b/src/sentry/models/projectkeymapping.py @@ -7,7 +7,7 @@ from sentry.db.models import BoundedBigIntegerField, sane_repr from sentry.db.models.base import Model, control_silo_model from sentry.db.models.indexes import IndexWithPostgresNameLimits -from sentry.hybridcloud.rpc import REGION_NAME_LENGTH +from sentry.hybridcloud.rpc import CELL_NAME_LENGTH @control_silo_model @@ -24,7 +24,7 @@ class ProjectKeyMapping(Model): project_key_id = BoundedBigIntegerField() public_key = models.CharField(max_length=32, unique=True, db_index=True) - cell_name = models.CharField(max_length=REGION_NAME_LENGTH) + cell_name = models.CharField(max_length=CELL_NAME_LENGTH) date_updated = models.DateTimeField(db_default=Now(), auto_now=True) class Meta: diff --git a/src/sentry/notifications/platform/slack/renderers/metric_alert.py b/src/sentry/notifications/platform/slack/renderers/metric_alert.py index f03196b1c2cd9c..1896862e454ce5 100644 --- a/src/sentry/notifications/platform/slack/renderers/metric_alert.py +++ b/src/sentry/notifications/platform/slack/renderers/metric_alert.py @@ -1,59 +1,20 @@ from __future__ import annotations -import sentry_sdk - -from sentry import features -from sentry.incidents.charts import build_metric_alert_chart -from sentry.incidents.typings.metric_detector import MetricIssueContext -from sentry.integrations.slack.message_builder.incidents import SlackIncidentsMessageBuilder -from sentry.models.group import Group -from sentry.models.organization import Organization -from sentry.notifications.notification_action.metric_alert_registry.handlers.utils import ( - get_alert_rule_serializer, - get_detector_serializer, -) -from sentry.notifications.notification_action.types import BaseMetricAlertHandler +from sentry.incidents.models.incident import IncidentStatus +from sentry.integrations.messaging.types import LEVEL_TO_COLOR +from sentry.integrations.metric_alerts import get_status_text +from sentry.integrations.slack.message_builder.base.block import BlockSlackMessageBuilder +from sentry.integrations.slack.message_builder.incidents import get_started_at +from sentry.integrations.slack.message_builder.types import INCIDENT_COLOR_MAPPING +from sentry.integrations.slack.utils.escape import escape_slack_text from sentry.notifications.platform.renderer import NotificationRenderer from sentry.notifications.platform.slack.provider import SlackRenderable -from sentry.notifications.platform.templates.metric_alert import ( - ActivityMetricAlertNotificationData, - BaseMetricAlertNotificationData, - MetricAlertNotificationData, -) +from sentry.notifications.platform.templates.metric_alert import MetricAlertNotificationData from sentry.notifications.platform.types import ( NotificationData, NotificationProviderKey, NotificationRenderedTemplate, ) -from sentry.services import eventstore -from sentry.services.eventstore.models import GroupEvent -from sentry.workflow_engine.models.detector import Detector - - -def _build_metric_issue_context_from_group_event( - data: MetricAlertNotificationData, -) -> MetricIssueContext: - event = eventstore.backend.get_event_by_id( - data.project_id, data.event_id, group_id=data.group_id - ) - if event is None: - raise ValueError(f"Event {data.event_id} not found") - elif not isinstance(event, GroupEvent): - raise ValueError(f"Event {data.event_id} is not a GroupEvent") - - evidence_data, priority = BaseMetricAlertHandler._extract_from_group_event(event) - return MetricIssueContext.from_group_event(event.group, evidence_data, priority) - - -def _build_metric_issue_context_from_activity( - data: ActivityMetricAlertNotificationData, -) -> MetricIssueContext: - from sentry.models.activity import Activity - - activity = Activity.objects.get(id=data.activity_id) - group = Group.objects.get_from_cache(id=data.group_id) - evidence_data, priority = BaseMetricAlertHandler._extract_from_activity(activity) - return MetricIssueContext.from_group_event(group, evidence_data, priority) class SlackMetricAlertRenderer(NotificationRenderer[SlackRenderable]): @@ -63,42 +24,24 @@ class SlackMetricAlertRenderer(NotificationRenderer[SlackRenderable]): def render[DataT: NotificationData]( cls, *, data: DataT, rendered_template: NotificationRenderedTemplate ) -> SlackRenderable: - if not isinstance(data, BaseMetricAlertNotificationData): + if not isinstance(data, MetricAlertNotificationData): raise ValueError(f"SlackMetricAlertRenderer does not support {data.__class__.__name__}") - if isinstance(data, MetricAlertNotificationData): - metric_issue_context = _build_metric_issue_context_from_group_event(data) - elif isinstance(data, ActivityMetricAlertNotificationData): - metric_issue_context = _build_metric_issue_context_from_activity(data) + status = get_status_text(IncidentStatus(data.new_status)) - organization = Organization.objects.get_from_cache(id=data.organization_id) - detector = Detector.objects.get(id=data.detector_id) - alert_context = data.alert_context.to_alert_context() - open_period_context = data.open_period_context + incident_text = f"{data.text}\n{get_started_at(data.open_period_context.date_started)}" + blocks = [BlockSlackMessageBuilder.get_markdown_block(text=incident_text)] - chart_url = None - if features.has("organizations:metric-alert-chartcuterie", organization): - try: - chart_url = build_metric_alert_chart( - organization=organization, - alert_rule_serialized_response=get_alert_rule_serializer(detector), - snuba_query=metric_issue_context.snuba_query, - alert_context=alert_context, - open_period_context=open_period_context, - subscription=metric_issue_context.subscription, - detector_serialized_response=get_detector_serializer(detector), - ) - except Exception as e: - sentry_sdk.capture_exception(e) + if data.chart_url: + blocks.append( + BlockSlackMessageBuilder.get_image_block(data.chart_url, alt="Metric Alert Chart") + ) - slack_body = SlackIncidentsMessageBuilder( - alert_context=alert_context, - metric_issue_context=metric_issue_context, - organization=organization, - date_started=open_period_context.date_started, - chart_url=chart_url, - notification_uuid=data.notification_uuid, - ).build() + color = LEVEL_TO_COLOR.get(INCIDENT_COLOR_MAPPING.get(status, "")) + fallback_text = f"<{data.title_link}|*{escape_slack_text(data.title)}*>" + slack_body = BlockSlackMessageBuilder._build_blocks( + *blocks, fallback_text=fallback_text, color=color + ) renderable = SlackRenderable( blocks=slack_body.get("blocks", []), diff --git a/src/sentry/notifications/platform/templates/__init__.py b/src/sentry/notifications/platform/templates/__init__.py index 6ec2b6c884468b..d83201c45fbdbe 100644 --- a/src/sentry/notifications/platform/templates/__init__.py +++ b/src/sentry/notifications/platform/templates/__init__.py @@ -1,13 +1,12 @@ from .data_export import DataExportFailureTemplate, DataExportSuccessTemplate from .issue import IssueNotificationTemplate -from .metric_alert import ActivityMetricAlertNotificationTemplate, MetricAlertNotificationTemplate +from .metric_alert import MetricAlertNotificationTemplate __all__ = ( "DataExportSuccessTemplate", "DataExportFailureTemplate", "IssueNotificationTemplate", "MetricAlertNotificationTemplate", - "ActivityMetricAlertNotificationTemplate", ) # All templates should be imported here so they are registered in the notifications Django app. # See sentry/notifications/apps.py diff --git a/src/sentry/notifications/platform/templates/metric_alert.py b/src/sentry/notifications/platform/templates/metric_alert.py index f313cf05dcfb04..885a590f1d46d2 100644 --- a/src/sentry/notifications/platform/templates/metric_alert.py +++ b/src/sentry/notifications/platform/templates/metric_alert.py @@ -1,11 +1,8 @@ from __future__ import annotations from datetime import datetime -from typing import Self -from pydantic import BaseModel, ConfigDict - -from sentry.incidents.typings.metric_detector import AlertContext, OpenPeriodContext +from sentry.incidents.typings.metric_detector import OpenPeriodContext from sentry.notifications.platform.registry import template_registry from sentry.notifications.platform.types import ( NotificationCategory, @@ -14,94 +11,28 @@ NotificationSource, NotificationTemplate, ) -from sentry.seer.anomaly_detection.types import AnomalyDetectionThresholdType - - -class SerializableAlertContext(BaseModel): - model_config = ConfigDict(frozen=True) - - name: str - action_identifier_id: int - threshold_type: int | None = None # AlertRuleThresholdType or AnomalyDetectionThresholdType - detection_type: str # AlertRuleDetectionType value (TextChoices str) - comparison_delta: int | None = None - sensitivity: str | None = None - resolve_threshold: float | None = None - alert_threshold: float | None = None - - @classmethod - def from_alert_context(cls, ac: AlertContext) -> Self: - return cls( - name=ac.name, - action_identifier_id=ac.action_identifier_id, - threshold_type=int(ac.threshold_type.value) if ac.threshold_type is not None else None, - detection_type=ac.detection_type.value, - comparison_delta=ac.comparison_delta, - sensitivity=ac.sensitivity, - resolve_threshold=ac.resolve_threshold, - alert_threshold=ac.alert_threshold, - ) - - def to_alert_context(self) -> AlertContext: - from sentry.incidents.models.alert_rule import ( - AlertRuleDetectionType, - AlertRuleThresholdType, - ) - - detection_type = AlertRuleDetectionType(self.detection_type) - - threshold_type: AlertRuleThresholdType | AnomalyDetectionThresholdType | None = None - if self.threshold_type is not None: - if detection_type == AlertRuleDetectionType.DYNAMIC: - threshold_type = AnomalyDetectionThresholdType(self.threshold_type) - else: - threshold_type = AlertRuleThresholdType(self.threshold_type) - return AlertContext( - name=self.name, - action_identifier_id=self.action_identifier_id, - threshold_type=threshold_type, - detection_type=detection_type, - comparison_delta=self.comparison_delta, - sensitivity=self.sensitivity, - resolve_threshold=self.resolve_threshold, - alert_threshold=self.alert_threshold, - ) +class MetricAlertNotificationData(NotificationData): + source: NotificationSource = NotificationSource.METRIC_ALERT -class BaseMetricAlertNotificationData(NotificationData): + # Identity / threading group_id: int organization_id: int - detector_id: int - - alert_context: SerializableAlertContext - open_period_context: OpenPeriodContext - notification_uuid: str + action_id: int # for ThreadKey key_data (used in PR 2 hookup) + open_period_context: OpenPeriodContext # id + date_started used in renderer and threading + new_status: int # IncidentStatus value; used for color mapping and reply_broadcast + # Pre-computed from incident_attachment_info() — all serializable strings + title: str + title_link: str + text: str -class MetricAlertNotificationData(BaseMetricAlertNotificationData): - """GroupEvent / firing path. Renderer re-fetches GroupEvent from Snuba.""" - - source: NotificationSource = NotificationSource.METRIC_ALERT - - event_id: str - project_id: int - - -class ActivityMetricAlertNotificationData(BaseMetricAlertNotificationData): - """Activity / SET_RESOLVED path. Renderer re-fetches Activity from Postgres.""" + # Pre-computed chart URL (None if feature disabled or build failed) + chart_url: str | None = None - source: NotificationSource = NotificationSource.ACTIVITY_METRIC_ALERT - activity_id: int - - -_EXAMPLE_ALERT_CONTEXT = SerializableAlertContext( - name="Example Alert", - action_identifier_id=1, - detection_type="static", -) _EXAMPLE_OPEN_PERIOD_CONTEXT = OpenPeriodContext( id=1, date_started=datetime(2024, 1, 1, 0, 0, 0), @@ -113,35 +44,16 @@ class MetricAlertNotificationTemplate(NotificationTemplate[MetricAlertNotificati category = NotificationCategory.METRIC_ALERT hide_from_debugger = True example_data = MetricAlertNotificationData( - event_id="abc123", - project_id=1, group_id=1, organization_id=1, - detector_id=1, - alert_context=_EXAMPLE_ALERT_CONTEXT, - open_period_context=_EXAMPLE_OPEN_PERIOD_CONTEXT, notification_uuid="test-uuid", - ) - - def render(self, data: MetricAlertNotificationData) -> NotificationRenderedTemplate: - return NotificationRenderedTemplate(subject="Metric Alert", body=[]) - - -@template_registry.register(NotificationSource.ACTIVITY_METRIC_ALERT) -class ActivityMetricAlertNotificationTemplate( - NotificationTemplate[ActivityMetricAlertNotificationData] -): - category = NotificationCategory.METRIC_ALERT - hide_from_debugger = True - example_data = ActivityMetricAlertNotificationData( - group_id=1, - organization_id=1, - detector_id=1, - alert_context=_EXAMPLE_ALERT_CONTEXT, + action_id=1, open_period_context=_EXAMPLE_OPEN_PERIOD_CONTEXT, - notification_uuid="test-uuid", - activity_id=1, + new_status=20, # IncidentStatus.CRITICAL + title="Critical: Example Alert", + title_link="https://sentry.io/organizations/example/alerts/rules/details/1/", + text="123 events in the last 5 minutes", ) - def render(self, data: ActivityMetricAlertNotificationData) -> NotificationRenderedTemplate: + def render(self, data: MetricAlertNotificationData) -> NotificationRenderedTemplate: return NotificationRenderedTemplate(subject="Metric Alert", body=[]) diff --git a/src/sentry/notifications/platform/types.py b/src/sentry/notifications/platform/types.py index 78c717dbb67661..6bdd91d9f75a3f 100644 --- a/src/sentry/notifications/platform/types.py +++ b/src/sentry/notifications/platform/types.py @@ -58,7 +58,6 @@ class NotificationSource(StrEnum): # METRIC_ALERT METRIC_ALERT = "metric-alert" - ACTIVITY_METRIC_ALERT = "activity-metric-alert" # SEER SEER_AUTOFIX_ERROR = "seer-autofix-error" @@ -94,7 +93,6 @@ class NotificationSource(StrEnum): ], NotificationCategory.METRIC_ALERT: [ NotificationSource.METRIC_ALERT, - NotificationSource.ACTIVITY_METRIC_ALERT, ], NotificationCategory.SEER: [ NotificationSource.SEER_AUTOFIX_TRIGGER, diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index 3e9788bdbb0e40..028c7f36986a52 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -4070,3 +4070,12 @@ type=Bool, flags=FLAG_MODIFIABLE_BOOL | FLAG_AUTOMATOR_MODIFIABLE, ) + +# ViewerContext — unified caller identity for all entrypoints. +# Set via deploy config (SENTRY_OPTIONS); requires restart to change. +register( + "viewer-context.enabled", + default=False, + type=Bool, + flags=FLAG_NOSTORE, +) diff --git a/src/sentry/preprod/api/endpoints/builds.py b/src/sentry/preprod/api/endpoints/builds.py index 4c6bcdf06e533e..59f28d810eac7a 100644 --- a/src/sentry/preprod/api/endpoints/builds.py +++ b/src/sentry/preprod/api/endpoints/builds.py @@ -89,6 +89,8 @@ def on_results(artifacts: list[PreprodArtifact]) -> list[dict[str, Any]]: display = request.GET.get("display") if display in ("size", "distribution"): queryset = queryset.filter(preprodsnapshotmetrics__isnull=True) + elif display == "snapshot": + queryset = queryset.filter(preprodsnapshotmetrics__isnull=False) except InvalidSearchQuery as e: # CodeQL complains about str(e) below but ~all handlers # of InvalidSearchQuery do the same as this. diff --git a/src/sentry/preprod/api/endpoints/organization_preprod_retention.py b/src/sentry/preprod/api/endpoints/organization_preprod_retention.py index b4a1c385cc63b8..9acff6b9d0fc93 100644 --- a/src/sentry/preprod/api/endpoints/organization_preprod_retention.py +++ b/src/sentry/preprod/api/endpoints/organization_preprod_retention.py @@ -72,6 +72,5 @@ def get(self, request: Request, organization: Organization) -> Response: { "size": size_retention, "buildDistribution": build_distribution_retention, - "snapshots": 30, # Hardcoded for now, check with Objectstore before increasing } ) diff --git a/src/sentry/preprod/api/endpoints/preprod_artifact_approve.py b/src/sentry/preprod/api/endpoints/preprod_artifact_approve.py index 091a9fb6f848c1..a4eabbb6a98b36 100644 --- a/src/sentry/preprod/api/endpoints/preprod_artifact_approve.py +++ b/src/sentry/preprod/api/endpoints/preprod_artifact_approve.py @@ -59,9 +59,6 @@ def post(self, request: Request, organization: Organization, artifact_id: str) - except (PreprodArtifact.DoesNotExist, ValueError): return Response({"detail": "Artifact not found"}, status=404) - # TODO(hybrid-cloud): approved_by is a User FK (control silo). This cell silo - # endpoint stores the ID, and the snapshot GET resolves it via User.objects.filter(). - # Both will need to use an RPC service when hybrid cloud enforcement is enabled. # exists()+create() instead of get_or_create — no unique constraint on this model # (see snapshots/tasks.py for rationale) already_approved = PreprodComparisonApproval.objects.filter( @@ -89,11 +86,9 @@ def post(self, request: Request, organization: Organization, artifact_id: str) - ).delete() task = STATUS_CHECK_TASK_MAP[feature_type] - task.apply_async( - kwargs={ - "preprod_artifact_id": artifact.id, - "caller": "approval_endpoint", - } + task( + preprod_artifact_id=artifact.id, + caller="approval_endpoint", ) return Response({"detail": "Approved"}, status=201) diff --git a/src/sentry/preprod/api/endpoints/preprod_artifact_rerun_analysis.py b/src/sentry/preprod/api/endpoints/preprod_artifact_rerun_analysis.py index 21119044581389..042d6973e35e02 100644 --- a/src/sentry/preprod/api/endpoints/preprod_artifact_rerun_analysis.py +++ b/src/sentry/preprod/api/endpoints/preprod_artifact_rerun_analysis.py @@ -8,7 +8,7 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import analytics +from sentry import analytics, features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, cell_silo_endpoint, internal_cell_silo_endpoint @@ -24,6 +24,7 @@ ) from sentry.preprod.producer import PreprodFeature, produce_preprod_artifact_to_kafka from sentry.preprod.quotas import should_run_distribution, should_run_size +from sentry.preprod.tasks import dispatch_taskbroker logger = logging.getLogger(__name__) @@ -79,26 +80,35 @@ def post( cleanup_old_metrics(head_artifact) reset_artifact_data(head_artifact) - try: - produce_preprod_artifact_to_kafka( - project_id=head_artifact.project.id, - organization_id=organization.id, - artifact_id=head_artifact_id, - requested_features=requested_features, - ) - except Exception: - logger.exception( - "preprod_artifact.rerun_analysis.kafka_error", - extra={ - "artifact_id": head_artifact_id, - "user_id": request.user.id, - "organization_id": organization.id, - "project_id": head_artifact.project.id, - }, + if features.has("organizations:launchpad-taskbroker-rollout", organization): + dispatched = dispatch_taskbroker( + head_artifact.project.id, organization.id, head_artifact_id ) + else: + try: + produce_preprod_artifact_to_kafka( + project_id=head_artifact.project.id, + organization_id=organization.id, + artifact_id=head_artifact_id, + requested_features=requested_features, + ) + dispatched = True + except Exception: + logger.exception( + "preprod_artifact.rerun_analysis.dispatch_error", + extra={ + "artifact_id": head_artifact_id, + "user_id": request.user.id, + "organization_id": organization.id, + "project_id": head_artifact.project.id, + }, + ) + dispatched = False + + if not dispatched: return Response( { - "error": f"Failed to queue analysis for artifact {head_artifact_id}", + "detail": f"Failed to queue analysis for artifact {head_artifact_id}", }, status=500, ) @@ -141,14 +151,14 @@ def post(self, request: Request) -> Response: try: data = orjson.loads(request.body) except (orjson.JSONDecodeError, TypeError): - return Response({"error": "Invalid JSON body"}, status=400) + return Response({"detail": "Invalid JSON body"}, status=400) preprod_artifact_id = data.get("preprod_artifact_id") try: preprod_artifact_id = int(preprod_artifact_id) except (ValueError, TypeError): return Response( - {"error": "preprod_artifact_id is required and must be a valid integer"}, + {"detail": "preprod_artifact_id is required and must be a valid integer"}, status=400, ) @@ -156,7 +166,7 @@ def post(self, request: Request) -> Response: preprod_artifact = PreprodArtifact.objects.get(id=preprod_artifact_id) except PreprodArtifact.DoesNotExist: return Response( - {"error": f"Preprod artifact {preprod_artifact_id} not found"}, status=404 + {"detail": f"Preprod artifact {preprod_artifact_id} not found"}, status=404 ) analytics.record( @@ -171,31 +181,40 @@ def post(self, request: Request) -> Response: cleanup_stats = cleanup_old_metrics(preprod_artifact) reset_artifact_data(preprod_artifact) - try: - # Admin endpoint bypasses quota checks and requests all features - produce_preprod_artifact_to_kafka( - project_id=preprod_artifact.project.id, - organization_id=preprod_artifact.project.organization_id, - artifact_id=preprod_artifact_id, - requested_features=[ - PreprodFeature.SIZE_ANALYSIS, - PreprodFeature.BUILD_DISTRIBUTION, - ], - ) - except Exception as e: - logger.exception( - "preprod_artifact.admin_rerun_analysis.kafka_error", - extra={ - "artifact_id": preprod_artifact_id, - "user_id": request.user.id, - "organization_id": preprod_artifact.project.organization_id, - "project_id": preprod_artifact.project.id, - "error": str(e), - }, + organization = preprod_artifact.project.organization + if features.has("organizations:launchpad-taskbroker-rollout", organization): + dispatched = dispatch_taskbroker( + preprod_artifact.project.id, organization.id, preprod_artifact_id ) + else: + try: + produce_preprod_artifact_to_kafka( + project_id=preprod_artifact.project.id, + organization_id=organization.id, + artifact_id=preprod_artifact_id, + requested_features=[ + PreprodFeature.SIZE_ANALYSIS, + PreprodFeature.BUILD_DISTRIBUTION, + ], + ) + dispatched = True + except Exception as e: + logger.exception( + "preprod_artifact.admin_rerun_analysis.dispatch_error", + extra={ + "artifact_id": preprod_artifact_id, + "user_id": request.user.id, + "organization_id": organization.id, + "project_id": preprod_artifact.project.id, + "error": str(e), + }, + ) + dispatched = False + + if not dispatched: return Response( { - "error": f"Failed to queue analysis for artifact {preprod_artifact_id}", + "detail": f"Failed to queue analysis for artifact {preprod_artifact_id}", }, status=500, ) diff --git a/src/sentry/preprod/api/endpoints/preprod_artifact_snapshot.py b/src/sentry/preprod/api/endpoints/preprod_artifact_snapshot.py index 921e9c1d65e036..b52166275c2806 100644 --- a/src/sentry/preprod/api/endpoints/preprod_artifact_snapshot.py +++ b/src/sentry/preprod/api/endpoints/preprod_artifact_snapshot.py @@ -61,7 +61,7 @@ ) from sentry.ratelimits.config import RateLimitConfig from sentry.types.ratelimit import RateLimit, RateLimitCategory -from sentry.users.models.user import User +from sentry.users.services.user.service import user_service from sentry.utils import metrics logger = logging.getLogger(__name__) @@ -349,7 +349,7 @@ def get(self, request: Request, organization: Organization, snapshot_id: str) -> if approved: sentry_user_ids = list({a.approved_by_id for a in approved if a.approved_by_id}) - users_by_id = {u.id: u for u in User.objects.filter(id__in=sentry_user_ids)} + users_by_id = {u.id: u for u in user_service.get_many_by_id(ids=sentry_user_ids)} approver_list: list[SnapshotApprover] = [] seen_approver_keys: set[str] = set() diff --git a/src/sentry/preprod/tasks.py b/src/sentry/preprod/tasks.py index 493a03ddac08a2..b6bbc37641a759 100644 --- a/src/sentry/preprod/tasks.py +++ b/src/sentry/preprod/tasks.py @@ -166,14 +166,16 @@ def assemble_preprod_artifact( pass if features.has("organizations:launchpad-taskbroker-rollout", organization): - _dispatch_taskbroker_shadow(project_id, org_id, artifact_id) - - kafka_dispatched = _dispatch_kafka(project_id, org_id, artifact_id, checksum) - if not kafka_dispatched: - return + taskbroker_dispatched = dispatch_taskbroker(project_id, org_id, artifact_id) + if not taskbroker_dispatched: + return + else: + kafka_dispatched = _dispatch_kafka(project_id, org_id, artifact_id, checksum) + if not kafka_dispatched: + return logger.info( - "Finished preprod artifact row creation and kafka dispatch", + "Finished preprod artifact dispatch", extra={ "preprod_artifact_id": artifact_id, "project_id": project_id, @@ -1013,13 +1015,10 @@ def _dispatch_kafka(project_id: int, org_id: int, artifact_id: int, checksum: st return False -def _dispatch_taskbroker_shadow(project_id: int, org_id: int, artifact_id: int) -> None: - # TODO: When taskbroker becomes the primary path, add PreprodArtifactSizeMetrics - # state management here (mirroring project_preprod_artifact_update.py). Currently - # omitted to avoid racing with the primary Kafka consumer path. +def dispatch_taskbroker(project_id: int, org_id: int, artifact_id: int) -> bool: try: logger.info( - "preprod.dispatch_taskbroker_shadow", + "preprod.dispatch_taskbroker", extra={ "project_id": project_id, "organization_id": org_id, @@ -1032,12 +1031,26 @@ def _dispatch_taskbroker_shadow(project_id: int, org_id: int, artifact_id: int) project_id=str(project_id), organization_id=str(org_id), ) + return True except Exception: + user_friendly_error_message = "Failed to dispatch preprod artifact event for analysis" logger.exception( - "Failed to dispatch shadow taskbroker event", + user_friendly_error_message, extra={ "project_id": project_id, "organization_id": org_id, "preprod_artifact_id": artifact_id, }, ) + PreprodArtifact.objects.filter(id=artifact_id).update( + state=PreprodArtifact.ArtifactState.FAILED, + error_code=PreprodArtifact.ErrorCode.ARTIFACT_PROCESSING_ERROR, + error_message=user_friendly_error_message, + ) + create_preprod_status_check_task.apply_async( + kwargs={ + "preprod_artifact_id": artifact_id, + "caller": "assemble_dispatch_error", + } + ) + return False diff --git a/src/sentry/preprod/vcs/webhooks/github_check_run.py b/src/sentry/preprod/vcs/webhooks/github_check_run.py index b706c954dabb28..6f14af81b92746 100644 --- a/src/sentry/preprod/vcs/webhooks/github_check_run.py +++ b/src/sentry/preprod/vcs/webhooks/github_check_run.py @@ -215,18 +215,14 @@ def handle_preprod_check_run_event( ) if identifier == APPROVE_SIZE_ACTION_IDENTIFIER: - create_preprod_status_check_task.apply_async( - kwargs={ - "preprod_artifact_id": artifact.id, - "caller": "github_approve_webhook", - } + create_preprod_status_check_task( + preprod_artifact_id=artifact.id, + caller="github_approve_webhook", ) elif identifier == APPROVE_SNAPSHOT_ACTION_IDENTIFIER: - create_preprod_snapshot_status_check_task.apply_async( - kwargs={ - "preprod_artifact_id": artifact.id, - "caller": "github_approve_webhook", - } + create_preprod_snapshot_status_check_task( + preprod_artifact_id=artifact.id, + caller="github_approve_webhook", ) else: raise ValueError(f"Unknown identifier: {identifier}") diff --git a/src/sentry/rules/history/endpoints/project_rule_group_history.py b/src/sentry/rules/history/endpoints/project_rule_group_history.py index 800fa7a196a326..a40b271575b0e2 100644 --- a/src/sentry/rules/history/endpoints/project_rule_group_history.py +++ b/src/sentry/rules/history/endpoints/project_rule_group_history.py @@ -58,7 +58,7 @@ def serialize( @cell_silo_endpoint class ProjectRuleGroupHistoryIndexEndpoint(WorkflowEngineRuleEndpoint): workflow_engine_method_flags = { - "GET": "organizations:workflow-engine-projectrulegroupstats-get", + "GET": "organizations:workflow-engine-issue-alert-endpoints-get", } publish_status = { "GET": ApiPublishStatus.EXPERIMENTAL, diff --git a/src/sentry/rules/history/endpoints/project_rule_stats.py b/src/sentry/rules/history/endpoints/project_rule_stats.py index 3738dee4ab8042..9fc28a081e3ecb 100644 --- a/src/sentry/rules/history/endpoints/project_rule_stats.py +++ b/src/sentry/rules/history/endpoints/project_rule_stats.py @@ -41,7 +41,7 @@ def serialize( @cell_silo_endpoint class ProjectRuleStatsIndexEndpoint(WorkflowEngineRuleEndpoint): workflow_engine_method_flags = { - "GET": "organizations:workflow-engine-projectrulegroupstats-get", + "GET": "organizations:workflow-engine-issue-alert-endpoints-get", } publish_status = { "GET": ApiPublishStatus.EXPERIMENTAL, diff --git a/src/sentry/runner/commands/presenters/webhookpresenter.py b/src/sentry/runner/commands/presenters/webhookpresenter.py index ec861b8cbdc047..f32bc99c14bd3a 100644 --- a/src/sentry/runner/commands/presenters/webhookpresenter.py +++ b/src/sentry/runner/commands/presenters/webhookpresenter.py @@ -53,8 +53,8 @@ def flush(self) -> None: return region: str | None = ( - settings.SENTRY_REGION - if settings.SENTRY_REGION + settings.SENTRY_LOCAL_CELL + if settings.SENTRY_LOCAL_CELL else settings.CUSTOMER_ID if settings.CUSTOMER_ID else settings.SILO_MODE diff --git a/src/sentry/runner/initializer.py b/src/sentry/runner/initializer.py index 96860193a710af..1e90cfc5665f6c 100644 --- a/src/sentry/runner/initializer.py +++ b/src/sentry/runner/initializer.py @@ -439,10 +439,10 @@ def validate_options(settings: Any) -> None: def validate_regions(settings: Any) -> None: from sentry.types.cell import load_from_config - if not settings.SENTRY_REGION_CONFIG: + if not settings.SENTRY_CELLS: return - load_from_config(settings.SENTRY_REGION_CONFIG, settings.SENTRY_LOCALITIES).validate_all() + load_from_config(settings.SENTRY_CELLS, settings.SENTRY_LOCALITIES).validate_all() def monkeypatch_django_migrations() -> None: diff --git a/src/sentry/search/eap/occurrences/attributes.py b/src/sentry/search/eap/occurrences/attributes.py index 3fca47fb584076..e17cbee68ff4fd 100644 --- a/src/sentry/search/eap/occurrences/attributes.py +++ b/src/sentry/search/eap/occurrences/attributes.py @@ -280,6 +280,12 @@ internal_name="frame_stack_levels", search_type="string", ), + ResolvedAttribute( + public_alias="issue", + internal_name="group_id", + search_type="string", + internal_type=constants.INT, + ), ] ) } diff --git a/src/sentry/seer/autofix/autofix.py b/src/sentry/seer/autofix/autofix.py index 99e260fcf69e7c..b8c572d9cff63f 100644 --- a/src/sentry/seer/autofix/autofix.py +++ b/src/sentry/seer/autofix/autofix.py @@ -39,10 +39,15 @@ from sentry.seer.autofix.utils import ( AutofixStoppingPoint, get_autofix_repos_from_project_code_mappings, + get_org_default_seer_automation_handoff, + get_project_seer_preferences, make_autofix_start_request, make_autofix_update_request, + set_project_seer_preference, + write_preference_to_sentry_db, ) from sentry.seer.explorer.utils import _convert_profile_to_execution_tree, fetch_profile_data +from sentry.seer.models import SeerProjectPreference from sentry.seer.signed_seer_api import SeerViewerContext from sentry.services import eventstore from sentry.services.eventstore.models import Event, GroupEvent @@ -530,6 +535,7 @@ def _call_autofix( auto_run_source: str | None = None, stopping_point: AutofixStoppingPoint | None = None, github_username: str | None = None, + preference: SeerProjectPreference | None = None, ): body = orjson.dumps( { @@ -568,6 +574,7 @@ def _call_autofix( ), "stopping_point": stopping_point.value if stopping_point else None, }, + "preference": preference.dict() if preference else None, }, option=orjson.OPT_NON_STR_KEYS, ) @@ -691,6 +698,42 @@ def get_all_tags_overview( } +def _resolve_project_preference( + organization: Organization, project: Project, fallback_repos: list[dict] +) -> SeerProjectPreference: + """ + Resolve the Seer project preference for a project before triggering autofix. + + If an existing preference is found in Seer, returns it. + If not, creates one from fallback_repos. + """ + preference_response = get_project_seer_preferences(project.id) + if preference_response.preference: + return preference_response.preference + + default_stopping_point, default_handoff = get_org_default_seer_automation_handoff(organization) + preference = SeerProjectPreference( + organization_id=organization.id, + project_id=project.id, + repositories=fallback_repos, + automated_run_stopping_point=default_stopping_point, + automation_handoff=default_handoff, + ) + + set_project_seer_preference(preference) + + try: + write_preference_to_sentry_db(project, preference) + except Exception: + logger.exception( + "seer.write_preferences.resolve_project_preference.sentry_db_write_failed", + extra={"project_id": project.id, "organization_id": organization.id}, + exc_info=True, + ) + + return preference + + def trigger_autofix( *, group: Group, @@ -739,6 +782,21 @@ def trigger_autofix( code_mappings = get_sorted_code_mapping_configs(group.project) repos = get_autofix_repos_from_project_code_mappings(group.project, code_mappings=code_mappings) + # Resolve the project preference from Seer, or bootstrap one from code mapping repos. + # On success, preference.repositories becomes the source of truth for repos + # (even if empty — matching Seer's behavior of unconditionally using preference repos). + # On failure, we fall back to the original code mapping repos above. + preference: SeerProjectPreference | None = None + try: + preference = _resolve_project_preference(group.organization, group.project, repos) + repos = [repo.dict() for repo in preference.repositories] + except Exception: + logger.exception( + "seer.write_preferences.resolve_project_preference.failed", + extra={"project_id": group.project.id, "organization_id": group.organization.id}, + exc_info=True, + ) + # Pre-resolve stacktrace frame paths using code mappings so Seer can skip # expensive git tree fetches for large repos. try: @@ -796,6 +854,7 @@ def trigger_autofix( auto_run_source=auto_run_source, stopping_point=stopping_point, github_username=github_username, + preference=preference, ) except Exception: logger.exception("Failed to send autofix to seer") diff --git a/src/sentry/seer/autofix/issue_summary.py b/src/sentry/seer/autofix/issue_summary.py index dac295292c1d7d..367131d81def82 100644 --- a/src/sentry/seer/autofix/issue_summary.py +++ b/src/sentry/seer/autofix/issue_summary.py @@ -283,6 +283,7 @@ def _call_seer( group: Group, serialized_event: dict[str, Any], trace_tree: dict[str, Any] | None, + experiment_variant: str | None = None, ): body = SummarizeIssueRequest( group_id=group.id, @@ -296,6 +297,7 @@ def _call_seer( organization_slug=group.organization.slug, organization_id=group.organization.id, project_id=group.project.id, + experiment_variant=experiment_variant, ) viewer_context = SeerViewerContext(organization_id=group.organization.id) response = make_summarize_issue_request(body, timeout=30, viewer_context=viewer_context) @@ -539,12 +541,32 @@ def _generate_summary( exc_info=True, ) + is_experiment = features.has("organizations:issue-summary-experimental", group.organization) + issue_summary = _call_seer( group, serialized_event, trace_tree, + experiment_variant="control" if is_experiment else None, ) + # Experiment: test summary quality without breadcrumbs and trace + if is_experiment: + try: + experimental_event = { + **serialized_event, + "entries": [ + e for e in serialized_event.get("entries", []) if e.get("type") != "breadcrumbs" + ], + } + _call_seer(group, experimental_event, None, experiment_variant="experimental") + except Exception: + logger.warning( + "Failed to generate experimental issue summary", + extra={"group_id": group.id}, + exc_info=True, + ) + summary_dict = issue_summary.dict() summary_dict["event_id"] = event.event_id cache.set(cache_key, summary_dict, timeout=int(timedelta(days=7).total_seconds())) diff --git a/src/sentry/seer/autofix/utils.py b/src/sentry/seer/autofix/utils.py index fba791d6413418..f0bf410ef22102 100644 --- a/src/sentry/seer/autofix/utils.py +++ b/src/sentry/seer/autofix/utils.py @@ -67,6 +67,16 @@ class AutofixStoppingPoint(StrEnum): OPEN_PR = "open_pr" +def get_valid_automated_run_stopping_points( + organization: Organization, +) -> set[AutofixStoppingPoint]: + """Return the set of stopping points valid for the given organization.""" + valid = {AutofixStoppingPoint.CODE_CHANGES, AutofixStoppingPoint.OPEN_PR} + if features.has("organizations:root-cause-stopping-point", organization): + valid.add(AutofixStoppingPoint.ROOT_CAUSE) + return valid + + class AutofixRequest(BaseModel): organization_id: int project_id: int @@ -374,12 +384,17 @@ class SeerAutofixSettingsSerializer(serializers.Serializer): required=False, help_text="The tuning setting for the projects.", ) - automatedRunStoppingPoint = serializers.ChoiceField( - choices=[opt.value for opt in AutofixStoppingPoint], + automatedRunStoppingPoint = serializers.CharField( required=False, help_text="The stopping point for the projects.", ) + def validate_automatedRunStoppingPoint(self, value: str) -> str: + organization = self.context["organization"] + if value not in get_valid_automated_run_stopping_points(organization): + raise serializers.ValidationError(f'"{value}" is not a valid choice.') + return value + def validate(self, data): if "autofixAutomationTuning" not in data and "automatedRunStoppingPoint" not in data: raise serializers.ValidationError( @@ -405,6 +420,9 @@ def get_org_default_seer_automation_handoff( stopping_point = organization.get_option( "sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT ) + # Guard against stored stopping points that are no longer valid. + if stopping_point not in get_valid_automated_run_stopping_points(organization): + stopping_point = SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT auto_open_prs = organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT) diff --git a/src/sentry/seer/blueprints/api.md b/src/sentry/seer/blueprints/api.md index 2eb4dcd9a0de55..ba9a570e9f90ce 100644 --- a/src/sentry/seer/blueprints/api.md +++ b/src/sentry/seer/blueprints/api.md @@ -27,12 +27,12 @@ Retrieves a paginated list of projects with their autofix automation settings. **Attributes** -| Column | Type | Description | -| ------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- | -| projectId | int | The project ID | -| autofixAutomationTuning | string | The tuning setting for automated autofix. One of: `off`, `medium`, (deprecated values: `super_low`, `low`, `high`, `always`) | -| automatedRunStoppingPoint | string | The stopping point for automated runs. One of: `code_changes`, `open_pr`, (deprecated values: `root_cause`, `solution`) | -| reposCount | int | Number of repositories configured for the project | +| Column | Type | Description | +| ------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| projectId | int | The project ID | +| autofixAutomationTuning | string | The tuning setting for automated autofix. One of: `off`, `medium`, (deprecated values: `super_low`, `low`, `high`, `always`) | +| automatedRunStoppingPoint | string | The stopping point for automated runs. One of: `code_changes`, `open_pr`, `root_cause` (requires `root-cause-stopping-point` flag), (deprecated values: `solution`) | +| reposCount | int | Number of repositories configured for the project | - Response 200 @@ -61,11 +61,11 @@ Bulk create/update the autofix automation settings for multiple projects in a si **Attributes** -| Column | Type | Required | Description | -| ------------------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------ | -| projectIds | list[int] | Yes | List of project IDs to update (min: 1, max: 1000) | -| autofixAutomationTuning | string | No\* | The tuning setting. One of: `off`, `medium`, (deprecated values: `super_low`, `low`, `high`, `always`) | -| automatedRunStoppingPoint | string | No\* | The stopping point. One of: `code_changes`, `open_pr`, (deprecated values: `root_cause`, `solution`) | +| Column | Type | Required | Description | +| ------------------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| projectIds | list[int] | Yes | List of project IDs to update (min: 1, max: 1000) | +| autofixAutomationTuning | string | No\* | The tuning setting. One of: `off`, `medium`, (deprecated values: `super_low`, `low`, `high`, `always`) | +| automatedRunStoppingPoint | string | No\* | The stopping point. One of: `code_changes`, `open_pr`, `root_cause` (requires `root-cause-stopping-point` flag), (deprecated values: `solution`) | \* At least one of either `autofixAutomationTuning` or `automatedRunStoppingPoint` must be provided. diff --git a/src/sentry/seer/endpoints/organization_autofix_automation_settings.py b/src/sentry/seer/endpoints/organization_autofix_automation_settings.py index 70991078d39321..399d73bf32481e 100644 --- a/src/sentry/seer/endpoints/organization_autofix_automation_settings.py +++ b/src/sentry/seer/endpoints/organization_autofix_automation_settings.py @@ -246,7 +246,9 @@ def post(self, request: Request, organization: Organization) -> Response: :pparam string organization_id_or_slug: the id or slug of the organization. :auth: required """ - serializer = SeerAutofixSettingsPostSerializer(data=request.data) + serializer = SeerAutofixSettingsPostSerializer( + data=request.data, context={"organization": organization} + ) if not serializer.is_valid(): return Response(serializer.errors, status=400) diff --git a/src/sentry/seer/endpoints/organization_seer_explorer_chat.py b/src/sentry/seer/endpoints/organization_seer_explorer_chat.py index 3969a125af1f40..f4925560a74793 100644 --- a/src/sentry/seer/endpoints/organization_seer_explorer_chat.py +++ b/src/sentry/seer/endpoints/organization_seer_explorer_chat.py @@ -39,6 +39,13 @@ class SeerExplorerChatSerializer(serializers.Serializer): allow_null=True, help_text="Optional context from the user's screen.", ) + page_name = serializers.CharField( + required=False, + allow_null=True, + allow_blank=True, + default=None, + help_text="The UI page name where the request originated (e.g., route string).", + ) override_ce_enable = serializers.BooleanField( required=False, default=True, @@ -143,6 +150,7 @@ def post( query = validated_data["query"] insert_index = validated_data.get("insert_index") on_page_context = validated_data.get("on_page_context") + page_name = validated_data.get("page_name") override_ce_enable = validated_data["override_ce_enable"] try: @@ -164,12 +172,14 @@ def post( prompt=query, insert_index=insert_index, on_page_context=on_page_context, + page_name=page_name, ) else: # Start new conversation result_run_id = client.start_run( prompt=query, on_page_context=on_page_context, + page_name=page_name, override_ce_enable=override_ce_enable, ) diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index cdabc2867d0bef..ec983725e07746 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -48,7 +48,6 @@ from sentry.hybridcloud.rpc.service import RpcAuthenticationSetupException, RpcResolutionException from sentry.hybridcloud.rpc.sig import SerializableFunctionValueException from sentry.integrations.github_enterprise.integration import GitHubEnterpriseIntegration -from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig from sentry.integrations.services.integration import integration_service from sentry.integrations.types import IntegrationProviderSlug from sentry.models.organization import Organization, OrganizationStatus @@ -115,6 +114,7 @@ rpc_get_trace_waterfall, ) from sentry.seer.fetch_issues import by_error_type, by_function_name, by_text_query, utils +from sentry.seer.fetch_issues.utils import NoProjectsForRepoError, get_repo_and_projects from sentry.seer.issue_detection import create_issue_occurrence from sentry.seer.models.seer_api_models import SeerProjectPreference from sentry.seer.utils import filter_repo_by_provider @@ -655,7 +655,7 @@ def trigger_coding_agent_launch( def has_repo_code_mappings( *, organization_id: int, provider: SeerSCMProvider, external_id: str, owner: str, name: str -) -> dict[str, bool]: +) -> dict[str, bool | dict[str, int]]: """ Validate that a repository exists and belongs to the given organization. @@ -667,19 +667,17 @@ def has_repo_code_mappings( name: The repository name (e.g., "sentry") Returns: - dict: {"has_code_mappings": bool} + dict: {"has_code_mappings": bool, "project_slug_to_id": dict[str, int]} """ - repo = filter_repo_by_provider(organization_id, provider, external_id, owner, name).first() - - if not repo: - return {"has_code_mappings": False} - - has_mappings = RepositoryProjectPathConfig.objects.filter( - organization_id=organization_id, - repository_id=repo.id, - ).exists() + try: + repo_projects = get_repo_and_projects(organization_id, provider, external_id, owner, name) + except (Repository.DoesNotExist, NoProjectsForRepoError): + return {"has_code_mappings": False, "project_slug_to_id": {}} - return {"has_code_mappings": has_mappings} + project_slug_to_id = dict( + sorted((project.slug, project.id) for project in repo_projects.projects) + ) + return {"has_code_mappings": True, "project_slug_to_id": project_slug_to_id} def validate_repo( diff --git a/src/sentry/seer/explorer/client.py b/src/sentry/seer/explorer/client.py index 03648c3aaaa39c..1c3d0dd803ad50 100644 --- a/src/sentry/seer/explorer/client.py +++ b/src/sentry/seer/explorer/client.py @@ -238,6 +238,7 @@ def start_run( prompt: str, prompt_metadata: dict[str, str] | None = None, on_page_context: str | None = None, + page_name: str | None = None, artifact_key: str | None = None, artifact_schema: type[BaseModel] | None = None, metadata: dict[str, Any] | None = None, @@ -271,6 +272,7 @@ def start_run( run_id=None, insert_index=None, on_page_context=on_page_context, + page_name=page_name, user_org_context=collect_user_org_context( self.user, self.organization, request=request ), @@ -339,6 +341,7 @@ def continue_run( prompt_metadata: dict[str, str] | None = None, insert_index: int | None = None, on_page_context: str | None = None, + page_name: str | None = None, artifact_key: str | None = None, artifact_schema: type[BaseModel] | None = None, ) -> int: @@ -369,6 +372,7 @@ def continue_run( run_id=run_id, insert_index=insert_index, on_page_context=on_page_context, + page_name=page_name, is_interactive=self.is_interactive, enable_coding=self.enable_coding, ) diff --git a/src/sentry/seer/explorer/client_utils.py b/src/sentry/seer/explorer/client_utils.py index fd5aa32171957b..191a4577120323 100644 --- a/src/sentry/seer/explorer/client_utils.py +++ b/src/sentry/seer/explorer/client_utils.py @@ -52,6 +52,7 @@ class ExplorerChatRequest(TypedDict): run_id: int | None insert_index: int | None on_page_context: str | None + page_name: NotRequired[str | None] user_org_context: NotRequired[dict[str, Any] | None] intelligence_level: NotRequired[str] is_interactive: NotRequired[bool] diff --git a/src/sentry/seer/fetch_issues/utils.py b/src/sentry/seer/fetch_issues/utils.py index 5411e3839442da..7567811f2c0969 100644 --- a/src/sentry/seer/fetch_issues/utils.py +++ b/src/sentry/seer/fetch_issues/utils.py @@ -23,6 +23,10 @@ MAX_NUM_DAYS_AGO_DEFAULT = 90 +class NoProjectsForRepoError(Exception): + """Raised when a repo exists but has no Sentry projects via code mappings.""" + + class SeerResponseError(TypedDict): error: str @@ -91,15 +95,27 @@ def get_repo_and_projects( repository_id=repo.id, ) ) - projects = [config.project for config in repo_configs] + + projects = [] + valid_configs = [] + for config in repo_configs: + try: + project = config.project + except Project.DoesNotExist: + continue + else: + valid_configs.append(config) + projects.append(project) + if not projects: - raise ValueError("No Sentry projects found for repo") + raise NoProjectsForRepoError("No Sentry projects found for repo") + return RepoProjects( organization_id=organization_id, provider=provider, external_id=external_id, repo=repo, - repo_configs=repo_configs, + repo_configs=valid_configs, projects=projects, ) diff --git a/src/sentry/seer/signed_seer_api.py b/src/sentry/seer/signed_seer_api.py index 10e6034f4392c9..20cf09976c871b 100644 --- a/src/sentry/seer/signed_seer_api.py +++ b/src/sentry/seer/signed_seer_api.py @@ -249,6 +249,7 @@ class SummarizeIssueRequest(TypedDict): organization_slug: str organization_id: int project_id: int + experiment_variant: NotRequired[str | None] class SupergroupsEmbeddingRequest(TypedDict): diff --git a/src/sentry/silo/client.py b/src/sentry/silo/client.py index ca723f57e99679..cbb71092f6605e 100644 --- a/src/sentry/silo/client.py +++ b/src/sentry/silo/client.py @@ -44,7 +44,7 @@ class SiloClientError(Exception): def get_cell_ip_addresses() -> frozenset[ipaddress.IPv4Address | ipaddress.IPv6Address]: """ - Infers the Cell Silo IP addresses from the SENTRY_REGION_CONFIG setting. + Infers the Cell Silo IP addresses from the SENTRY_CELLS setting. """ cell_ip_addresses: set[ipaddress.IPv4Address | ipaddress.IPv6Address] = set() diff --git a/src/sentry/snuba/occurrences_rpc.py b/src/sentry/snuba/occurrences_rpc.py index 3240e98162d431..f1a73c5005e3d9 100644 --- a/src/sentry/snuba/occurrences_rpc.py +++ b/src/sentry/snuba/occurrences_rpc.py @@ -11,16 +11,19 @@ TraceItemFilter, ) -from sentry.search.eap.columns import ColumnDefinitions, ResolvedAttribute +from sentry.models.group import Group +from sentry.search.eap.columns import ColumnDefinitions, ResolvedAttribute, ResolvedColumn from sentry.search.eap.occurrences.definitions import OCCURRENCE_DEFINITIONS from sentry.search.eap.resolver import SearchResolver from sentry.search.eap.types import AdditionalQueries, EAPResponse, SearchResolverConfig -from sentry.search.events.types import SAMPLING_MODES, SnubaParams +from sentry.search.events.types import SAMPLING_MODES, SnubaData, SnubaParams from sentry.snuba import rpc_dataset_common from sentry.utils.snuba import process_value logger = logging.getLogger(__name__) +UNKNOWN_ISSUE = "UNKNOWN" + class OccurrenceCategory(Enum): """ @@ -245,6 +248,48 @@ def run_grouped_timeseries_query( return results + @classmethod + def _fetch_issue_labels( + cls, + group_ids: list[int | None], + project_ids: list[int], + ) -> dict[int, str]: + resultant_map: dict[int, str] = {} + grp_ids: set[int] = set({grp_id for grp_id in (group_ids or []) if grp_id}) + qs = Group.objects.filter(pk__in=grp_ids, project_id__in=project_ids).select_related( + "project" + ) + for grp in qs: + resultant_map[grp.id] = grp.qualified_short_id or UNKNOWN_ISSUE + return resultant_map + + @classmethod + def process_column_values( + cls, + column_value: Any, + final_data: SnubaData, + attribute: Any, + resolved_column: ResolvedColumn, + **context_kwargs: Any, + ) -> None: + if attribute == "issue": + group_ids: list[int | None] = [ + getattr(result, str(result.WhichOneof("value"))) if not result.is_null else None + for result in column_value.results + ] + group_id_to_issue_map = cls._fetch_issue_labels( + group_ids, context_kwargs.get("project_ids", []) + ) + for index, group_id in enumerate(group_ids): + issue_label = UNKNOWN_ISSUE + if group_id and group_id in group_id_to_issue_map: + issue_label = group_id_to_issue_map[group_id] + final_data[index][attribute] = issue_label + else: + super().process_column_values( + column_value, final_data, attribute, resolved_column, **context_kwargs + ) + @classmethod def _build_category_filter(cls, category: OccurrenceCategory | None) -> TraceItemFilter | None: issue_occurrence_id_key = AttributeKey( diff --git a/src/sentry/snuba/rpc_dataset_common.py b/src/sentry/snuba/rpc_dataset_common.py index 21b11101cf79b1..0c359b7958e162 100644 --- a/src/sentry/snuba/rpc_dataset_common.py +++ b/src/sentry/snuba/rpc_dataset_common.py @@ -236,6 +236,13 @@ def filter_project(cls, project: Project) -> bool: """ Table Methods """ + @classmethod + def build_rpc_table_row_context(cls, query: TableQuery) -> dict[str, Any]: + return { + "project_ids": list(query.resolver.params.project_ids), + "organization_id": query.resolver.params.organization_id, + } + @classmethod def get_table_rpc_request(cls, query: TableQuery) -> TableRequest: """Make the query""" @@ -395,7 +402,9 @@ def _run_table_query( "query.storage_meta.tier", rpc_response.meta.downsampled_storage_meta.tier ) - return cls.process_table_response(rpc_response, table_request, debug=debug) + return cls.process_table_response( + rpc_response, table_request, debug=debug, context=cls.build_rpc_table_row_context(query) + ) @classmethod def run_table_query( @@ -425,27 +434,61 @@ def run_bulk_table_queries(cls, queries: list[TableQuery]): for query in queries: if query.name is None: raise ValueError("Query name is required for bulk queries") - elif query.name in names: + if query.name in names: raise ValueError("Query names need to be unique") - else: - names.add(query.name) - prepared_queries = {query.name: cls.get_table_rpc_request(query) for query in queries} - """Run the query""" - responses = snuba_rpc.table_rpc([query.rpc_request for query in prepared_queries.values()]) + names.add(query.name) + + request_context_pairs: list[tuple[str, TableRequest, dict[str, Any]]] = [] + for query in queries: + assert query.name is not None + table_request = cls.get_table_rpc_request(query) + request_context_pairs.append( + (query.name, table_request, cls.build_rpc_table_row_context(query)) + ) + responses = snuba_rpc.table_rpc( + [request.rpc_request for _, request, _ in request_context_pairs] + ) results = { - name: cls.process_table_response(response, request) - for (name, request), response in zip(prepared_queries.items(), responses) + name: cls.process_table_response(response, request, context=process_context) + for (name, request, process_context), response in zip(request_context_pairs, responses) } return results + @classmethod + def process_column_values( + cls, + column_value: Any, + final_data: SnubaData, + attribute: Any, + resolved_column: ResolvedColumn, + **_context_kwargs: Any, + ) -> None: + for index, result in enumerate(column_value.results): + result_value: str | int | float | None + if result.is_null: + result_value = None + else: + result_value = getattr(result, str(result.WhichOneof("value"))) + result_value = process_value(result_value) + final_data[index][attribute] = resolved_column.process_column(result_value) + + @classmethod + def process_column_confidence(cls, column_value, final_confidence, attribute) -> None: + for index, result in enumerate(column_value.results): + final_confidence[index][attribute] = CONFIDENCES.get( + column_value.reliabilities[index], None + ) + @classmethod def process_table_response( cls, rpc_response: TraceItemTableResponse, table_request: TableRequest, debug: str | bool = False, + context: dict[str, Any] | None = None, ) -> EAPResponse: """Process the results""" + context_kwargs = dict(context) if context else {} final_data: SnubaData = [] final_confidence: ConfidenceData = [] final_meta: EventsMeta = events_meta_from_rpc_request_meta(rpc_response.meta) @@ -480,18 +523,15 @@ def process_table_response( final_data.append({}) final_confidence.append({}) - for index, result in enumerate(column_value.results): - result_value: str | int | float | None - if result.is_null: - result_value = None - else: - result_value = getattr(result, str(result.WhichOneof("value"))) - result_value = process_value(result_value) - final_data[index][attribute] = resolved_column.process_column(result_value) - if has_reliability: - final_confidence[index][attribute] = CONFIDENCES.get( - column_value.reliabilities[index], None - ) + cls.process_column_values( + column_value, + final_data, + attribute, + resolved_column, + **context_kwargs, + ) + if has_reliability: + cls.process_column_confidence(column_value, final_confidence, attribute) if debug: set_debug_meta(final_meta, rpc_response.meta, table_request.rpc_request) diff --git a/src/sentry/synapse/endpoints/project_key_cell_mappings.py b/src/sentry/synapse/endpoints/project_key_cell_mappings.py new file mode 100644 index 00000000000000..207c32bd7ad3f3 --- /dev/null +++ b/src/sentry/synapse/endpoints/project_key_cell_mappings.py @@ -0,0 +1,78 @@ +from rest_framework.request import Request +from rest_framework.response import Response + +from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus +from sentry.api.base import Endpoint, control_silo_endpoint +from sentry.models.projectkeymapping import ProjectKeyMapping +from sentry.synapse.endpoints.authentication import ( + SynapseAuthPermission, + SynapseSignatureAuthentication, +) +from sentry.synapse.paginator import SynapsePaginator +from sentry.types.cell import get_global_directory + + +@control_silo_endpoint +class ProjectKeyCellMappingsEndpoint(Endpoint): + """ + Returns the project-key-to-cell mappings for all project keys in pages. + Only accessible by the Synapse internal service via X-Synapse-Auth header. + """ + + owner = ApiOwner.INFRA_ENG + publish_status = { + "GET": ApiPublishStatus.PRIVATE, + } + authentication_classes = (SynapseSignatureAuthentication,) + permission_classes = (SynapseAuthPermission,) + + MAX_LIMIT = 10000 + + def get(self, request: Request) -> Response: + """ + Retrieve project-key-to-cell mappings. + """ + directory = get_global_directory() + + query = ProjectKeyMapping.objects.all() + localities = request.GET.getlist("locality") + if localities: + cell_names = [ + r.name + for locality in localities + for r in directory.get_cells_for_locality(locality) + ] + query = query.filter(cell_name__in=cell_names) + + per_page = self.get_per_page(request, max_per_page=self.MAX_LIMIT) + paginator = SynapsePaginator( + queryset=query, + id_field="id", + timestamp_field="date_updated", + ) + pagination_result = paginator.get_result( + limit=per_page, + cursor_str=request.GET.get("cursor"), + ) + + mappings = [ + {"id": str(item.project_key_id), "publickey": item.public_key, "cell": item.cell_name} + for item in pagination_result.results + ] + + cell_to_locality = { + cell.name: loc.name + for cell in directory.cells + if (loc := directory.get_locality_for_cell(cell.name)) is not None + } + + response_data = { + "data": mappings, + "metadata": { + "cursor": pagination_result.next_cursor, + "has_more": pagination_result.has_more, + "cell_to_locality": cell_to_locality, + }, + } + return Response(response_data, status=200) diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 1d4ac97d8abcb2..0ab062fa8a2cb9 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -28,6 +28,7 @@ get_autofix_state, get_org_default_seer_automation_handoff, get_seer_seat_based_tier_cache_key, + get_valid_automated_run_stopping_points, resolve_repository_ids, ) from sentry.seer.models import SeerProjectPreference @@ -243,8 +244,7 @@ def configure_seer_for_existing_org(organization_id: int) -> None: default_stopping_point, default_handoff = get_org_default_seer_automation_handoff(organization) default_handoff_dict = default_handoff.dict() if default_handoff else None - - valid_stopping_points = {"open_pr", "code_changes"} + valid_stopping_points = get_valid_automated_run_stopping_points(organization) preferences_by_id = bulk_get_project_preferences(organization_id, project_ids) diff --git a/src/sentry/templatetags/sentry_api.py b/src/sentry/templatetags/sentry_api.py index a615f76175f5e4..ff88e114e13a11 100644 --- a/src/sentry/templatetags/sentry_api.py +++ b/src/sentry/templatetags/sentry_api.py @@ -3,7 +3,7 @@ from sentry.api.serializers.base import serialize as serialize_func from sentry.api.serializers.models.organization import ( - DetailedOrganizationSerializerWithProjectsAndTeams, + OrganizationWithProjectsAndTeamsSerializer, ) from sentry.auth.access import NoAccess, from_user from sentry.utils import json @@ -35,9 +35,7 @@ def serialize_detailed_org(context, obj): user = None access = NoAccess() - context = serialize_func( - obj, user, DetailedOrganizationSerializerWithProjectsAndTeams(), access=access - ) + context = serialize_func(obj, user, OrganizationWithProjectsAndTeamsSerializer(), access=access) return convert_to_json(context) diff --git a/src/sentry/testutils/cell.py b/src/sentry/testutils/cell.py index 7594b2b4edb01a..1000e20161d8be 100644 --- a/src/sentry/testutils/cell.py +++ b/src/sentry/testutils/cell.py @@ -53,7 +53,10 @@ def swap_state( monolith_cell = cells[0] with override_settings(SENTRY_MONOLITH_REGION=monolith_cell.name): if local_cell: - with override_settings(SENTRY_REGION=local_cell.name): + # TODO(cells): Remove SENTRY_REGION once all references in getsentry tests updated + with override_settings( + SENTRY_LOCAL_CELL=local_cell.name, SENTRY_REGION=local_cell.name + ): yield else: yield @@ -70,7 +73,10 @@ def swap_state( @contextmanager def swap_to_default_cell(self) -> Generator[None]: """Swap to the monolith cell when entering cell mode.""" - with override_settings(SENTRY_REGION=self._default_cell.name): + # TODO(cells): Remove SENTRY_REGION once all references in getsentry tests updated + with override_settings( + SENTRY_LOCAL_CELL=self._default_cell.name, SENTRY_REGION=self._default_cell.name + ): yield @contextmanager @@ -79,7 +85,8 @@ def swap_to_cell_by_name(self, cell_name: str) -> Generator[None]: cell = self.get_cell_by_name(cell_name) if cell is None: raise Exception("specified swap cell not found") - with override_settings(SENTRY_REGION=cell.name): + # TODO(cells): Remove SENTRY_REGION once all references in getsentry tests updated + with override_settings(SENTRY_LOCAL_CELL=cell.name, SENTRY_REGION=cell.name): yield @@ -93,9 +100,9 @@ def get_test_env_directory() -> TestEnvCellDirectory: def override_cells(cells: Sequence[Cell], local_cell: Cell | None = None) -> Generator[None]: """Override the global set of existing cells. - The overriding value takes the place of the `SENTRY_REGION_CONFIG` setting and + The overriding value takes the place of the `SENTRY_CELLS` setting and changes the behavior of the module-level functions in `sentry.types.cell`. This - is preferable to overriding the `SENTRY_REGION_CONFIG` setting value directly + is preferable to overriding the `SENTRY_CELLS` setting value directly because the cell mapping may already be cached. """ with get_test_env_directory().swap_state(cells, local_cell=local_cell): diff --git a/src/sentry/testutils/factories.py b/src/sentry/testutils/factories.py index cb3cd02f25d466..fe4ba6414a5786 100644 --- a/src/sentry/testutils/factories.py +++ b/src/sentry/testutils/factories.py @@ -393,7 +393,7 @@ def create_organization(name=None, owner=None, cell: Cell | str | None = None, * cell_name = cell_obj.name ctx.enter_context( - override_settings(SILO_MODE=SiloMode.CELL, SENTRY_REGION=cell_name) + override_settings(SILO_MODE=SiloMode.CELL, SENTRY_LOCAL_CELL=cell_name) ) with outbox_context(flush=False): diff --git a/src/sentry/testutils/helpers/response.py b/src/sentry/testutils/helpers/response.py index 87d359c443bc2b..e91358c335b9a0 100644 --- a/src/sentry/testutils/helpers/response.py +++ b/src/sentry/testutils/helpers/response.py @@ -19,11 +19,21 @@ def is_streaming_response(response: HttpResponseBase) -> TypeGuard[StreamingHttp async def _async_streaming_response_content(response: StreamingHttpResponse) -> bytes: data = [] - async for chunk in response: - data.append(chunk) + try: + async for chunk in response: + data.append(chunk) + except RuntimeError as e: + if "Event loop is closed" not in str(e): + raise + # Occurs when the async view and close_streaming_response run on different event + # loops. Harmless — all data was already received before httpcore attempts cleanup. iterator = response._iterator # type: ignore[attr-defined] if hasattr(iterator, "aclose"): - await iterator.aclose() + try: + await iterator.aclose() + except RuntimeError as e: + if "Event loop is closed" not in str(e): + raise return b"".join(data) diff --git a/src/sentry/testutils/pytest/sentry.py b/src/sentry/testutils/pytest/sentry.py index b98ff776d41a5a..4ee03479684b0b 100644 --- a/src/sentry/testutils/pytest/sentry.py +++ b/src/sentry/testutils/pytest/sentry.py @@ -85,9 +85,12 @@ def _configure_test_env_cells() -> None: RegionCategory.MULTI_TENANT, ) - settings.SENTRY_REGION = cell_name + settings.SENTRY_LOCAL_CELL = cell_name settings.SENTRY_MONOLITH_REGION = cell_name + # TODO(cells): Remove once all references in getsentry tests updated + settings.SENTRY_REGION = cell_name + # This not only populates the environment with the default cell, but also # ensures that a TestEnvCellDirectory instance is injected into global state. # See sentry.testutils.cell.get_test_env_directory, which relies on it. diff --git a/src/sentry/testutils/silo.py b/src/sentry/testutils/silo.py index 74a79df4be2bd2..37778335eb1547 100644 --- a/src/sentry/testutils/silo.py +++ b/src/sentry/testutils/silo.py @@ -378,7 +378,7 @@ def assume_test_silo_mode( with cell_dir.swap_to_cell_by_name(cell_name): yield else: - with override_settings(SENTRY_REGION=None): + with override_settings(SENTRY_LOCAL_CELL=None): yield diff --git a/src/sentry/types/cell.py b/src/sentry/types/cell.py index e8a56d605616ca..d01ae02a23cf2c 100644 --- a/src/sentry/types/cell.py +++ b/src/sentry/types/cell.py @@ -325,7 +325,7 @@ def get_global_directory() -> CellDirectory: # For now, assume that all cell configs can be taken in through Django # settings. We may investigate other ways of delivering those configs in # production. - _global_directory = load_from_config(settings.SENTRY_REGION_CONFIG, settings.SENTRY_LOCALITIES) + _global_directory = load_from_config(settings.SENTRY_CELLS, settings.SENTRY_LOCALITIES) return _global_directory @@ -433,12 +433,12 @@ def get_local_cell() -> Cell: if single_process_cell is not None: return single_process_cell - if not settings.SENTRY_REGION: + if not settings.SENTRY_LOCAL_CELL: if in_test_environment(): return get_cell_by_name(settings.SENTRY_MONOLITH_REGION) else: - raise Exception("SENTRY_REGION must be set when server is in REGION silo mode") - return get_cell_by_name(settings.SENTRY_REGION) + raise Exception("SENTRY_LOCAL_CELL must be set when server is in CELL silo mode") + return get_cell_by_name(settings.SENTRY_LOCAL_CELL) @control_silo_function diff --git a/src/sentry/utils/sdk.py b/src/sentry/utils/sdk.py index 04d7916bdcb0c9..823fe4e40e9070 100644 --- a/src/sentry/utils/sdk.py +++ b/src/sentry/utils/sdk.py @@ -252,8 +252,8 @@ def before_send(event: Event, hint: Hint) -> Event | None: if event.get("tags"): if settings.SILO_MODE: event["tags"]["silo_mode"] = str(settings.SILO_MODE) - if settings.SENTRY_REGION: - event["tags"]["sentry_region"] = settings.SENTRY_REGION + if settings.SENTRY_LOCAL_CELL: + event["tags"]["sentry_region"] = settings.SENTRY_LOCAL_CELL if hint.get("exc_info", [None])[0] == OperationalError: event["level"] = "warning" diff --git a/src/sentry/utils/sdk_crashes/README.rst b/src/sentry/utils/sdk_crashes/README.rst index c0cb910251f618..3ba6d1bd2501a9 100644 --- a/src/sentry/utils/sdk_crashes/README.rst +++ b/src/sentry/utils/sdk_crashes/README.rst @@ -30,5 +30,7 @@ SDK and system library frames. For grouping to work correctly, the event_strippe config will change it to in_app false for all Cocoa SDK frames. To not change the grouping logic, we add the following stacktrace rule ``stack.abs_path:Sentry.framework +app +group`` to the configured in project with the id configured in the option ``issues.sdk_crash_detection.cocoa.project_id``. -You can turn the feature on or off in https://sentry.io/_admin/options. The option name is ``issues.sdk-crash-detection`` and the feature name is ``organizations:sdk-crash-detection``. -Furthermore, you can change the project to store the crash events and the sample rate per SDK with the options ``issues.sdk_crash_detection.cocoa.project_id`` and ``issues.sdk_crash_detection.cocoa.sample_rate``. +The feature flag ``organizations:sdk-crash-detection`` controls whether SDK crash detection runs for an organization. It is managed via +flagpole in `sentry-options-automator `_ and is currently enabled in the US and s4s2 +regions. The per-SDK project IDs and sample rates are still controlled via options (e.g. ``issues.sdk_crash_detection.cocoa.project_id``, +``issues.sdk_crash_detection.cocoa.sample_rate``). diff --git a/src/sentry/viewer_context.py b/src/sentry/viewer_context.py new file mode 100644 index 00000000000000..f1e6d3ee1cfe2d --- /dev/null +++ b/src/sentry/viewer_context.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import contextlib +import contextvars +import dataclasses +import enum +from collections.abc import Generator +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from sentry.auth.services.auth import AuthenticatedToken + +_viewer_context_var: contextvars.ContextVar[ViewerContext | None] = contextvars.ContextVar( + "viewer_context", default=None +) + +""" +ViewerContext is an object propagated across codebase (and crossing service boundary) to deliver +accurate information regarding the viewer on behalf of which the request is being made. + +This can be global, limited to an organization, or particular user. + +The proposal for this project alongside needs and specific considerations can be found in: +https://www.notion.so/sentry/RFC-Unified-ViewerContext-via-ContextVar-32f8b10e4b5d81988625cb5787035e02 +""" + + +class ActorType(enum.StrEnum): + USER = "user" + SYSTEM = "system" + INTEGRATION = "integration" + UNKNOWN = "unknown" + + +@dataclasses.dataclass(frozen=True) +class ViewerContext: + """Identity of the caller for the current unit of work. + + Set once at each entrypoint (API request, task, consumer, RPC) via + ``viewer_context_scope`` and readable anywhere via ``get_viewer_context``. + + Frozen so that ``copy_context()`` produces a safe shallow copy when + propagating across threads. + """ + + organization_id: int | None = None + user_id: int | None = None + actor_type: ActorType = ActorType.UNKNOWN + + # Carries scopes/kind for in-process permission checks. + # NOT propagated across process/service boundaries. + token: AuthenticatedToken | None = None + + +@contextlib.contextmanager +def viewer_context_scope(ctx: ViewerContext) -> Generator[None]: + """Enter a viewer context for the duration of a unit of work. + + Always use this instead of raw ``_viewer_context_var.set()`` — + it guarantees cleanup via ``reset(token)`` even on exceptions. + """ + tok = _viewer_context_var.set(ctx) + try: + yield + finally: + _viewer_context_var.reset(tok) + + +def get_viewer_context() -> ViewerContext | None: + """Return the current ``ViewerContext``, or ``None`` if not set.""" + return _viewer_context_var.get() diff --git a/src/sentry/web/frontend/pipeline_advancer.py b/src/sentry/web/frontend/pipeline_advancer.py index ffbdef10bebe64..26ddf6a125a463 100644 --- a/src/sentry/web/frontend/pipeline_advancer.py +++ b/src/sentry/web/frontend/pipeline_advancer.py @@ -28,7 +28,7 @@ style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; flex-direction:column;padding:2rem"> -