diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 436ee737bacf2a..27f57a40061be6 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -230,73 +230,6 @@ jobs: github-token: ${{ steps.token.outputs.token }} message: ':snowflake: re-freeze requirements' - lint: - if: needs.files-changed.outputs.backend == 'true' - needs: files-changed - name: backend lint - runs-on: ubuntu-20.04 - timeout-minutes: 10 - steps: - - uses: getsentry/action-github-app-token@97c9e23528286821f97fba885c1b1123284b29cc # v2.0.0 - id: token - continue-on-error: true - with: - app_id: ${{ vars.SENTRY_INTERNAL_APP_ID }} - private_key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }} - - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 - - - uses: getsentry/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # v2.11.1 - id: files - with: - # Enable listing of files matching each filter. - # Paths to files will be available in `${FILTER_NAME}_files` output variable. - # Paths will be escaped and space-delimited. - # Output is usable as command line argument list in linux shell - list-files: shell - - # It doesn't make sense to lint deleted files. - # Therefore we specify we are only interested in added or modified files. - filters: | - all: - - added|modified: '**/*.py' - - added|modified: 'requirements-*.txt' - - - uses: getsentry/action-setup-venv@9e3bbae3836b1b6f129955bf55a19e1d99a61c67 # v1.0.5 - with: - python-version: 3.8.16 - cache-dependency-path: | - requirements-dev.txt - requirements-dev-frozen.txt - install-cmd: pip install -r requirements-dev.txt -c requirements-dev-frozen.txt - - - uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 # v3.0.11 - with: - path: ~/.cache/pre-commit - key: cache-epoch-1|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} - - - name: Setup pre-commit - # We don't use make setup-git because we're only interested in installing - # requirements-dev.txt as a fast path. - # We don't need pre-commit install --install-hooks since we're just interested - # in running the hooks. - run: | - pre-commit install-hooks - - - name: Run pre-commit on changed files - run: | - # Run pre-commit to lint and format check files that were changed (but not deleted) compared to master. - # XXX: there is a very small chance that it'll expand to exceed Linux's limits - # `getconf ARG_MAX` - max # bytes of args + environ for exec() - pre-commit run --files ${{ steps.files.outputs.all_files }} - - - name: Apply any pre-commit fixed files - if: steps.token.outcome == 'success' && github.ref != 'refs/heads/master' && always() - uses: getsentry/action-github-commit@748c31dd78cffe76f51bef49a0be856b6effeda7 # v1.1.0 - with: - github-token: ${{ steps.token.outputs.token }} - message: ':hammer_and_wrench: apply pre-commit fixes' - migration: if: needs.files-changed.outputs.migration_lockfile == 'true' needs: files-changed @@ -527,9 +460,9 @@ jobs: github-token: ${{ steps.token.outputs.token }} message: ':knife: regenerate mypy module blocklist' - # This check runs once all dependant jobs have passed + # This check runs once all dependent jobs have passed # It symbolizes that all required Backend checks have succesfully passed (Or skipped) - # This check is the only required Github check + # This step is the only required backend check backend-required-check: needs: [ @@ -538,7 +471,6 @@ jobs: backend-migration-tests, cli, files-changed, - lint, requirements, migration, plugins, diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000000000..51178fe17467fa --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,83 @@ +name: pre-commit + +on: + push: + branches: + - master + pull_request: + +# Cancel in progress workflows on pull_requests. +# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +defaults: + run: + # the default default is: + # bash --noprofile --norc -eo pipefail {0} + shell: bash --noprofile --norc -eo pipefail -ux {0} + +# hack for https://github.com/actions/cache/issues/810#issuecomment-1222550359 +env: + SEGMENT_DOWNLOAD_TIMEOUT_MINS: 3 + +jobs: + lint: + name: pre-commit lint + runs-on: ubuntu-20.04 + timeout-minutes: 10 + steps: + - uses: getsentry/action-github-app-token@97c9e23528286821f97fba885c1b1123284b29cc # v2.0.0 + id: token + with: + app_id: ${{ vars.SENTRY_INTERNAL_APP_ID }} + private_key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }} + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 + - name: Get changed files + id: changes + uses: getsentry/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # v2.11.1 + with: + token: ${{ steps.token.outputs.token }} + + # Enable listing of files matching each filter. + # Paths to files will be available in `${FILTER_NAME}_files` output variable. + list-files: json + + # It doesn't make sense to lint deleted files. + # Therefore we specify we are only interested in added or modified files. + filters: | + all: + - added|modified: '**/*' + + - uses: getsentry/action-setup-venv@9e3bbae3836b1b6f129955bf55a19e1d99a61c67 # v1.0.5 + with: + python-version: 3.8.16 + cache-dependency-path: | + requirements-dev.txt + requirements-dev-frozen.txt + install-cmd: pip install -r requirements-dev.txt -c requirements-dev-frozen.txt + - uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 # v3.0.11 + with: + path: ~/.cache/pre-commit + key: cache-epoch-1|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} + - name: Setup pre-commit + # We don't use make setup-git because we're only interested in installing + # requirements-dev.txt as a fast path. + # We don't need pre-commit install --install-hooks since we're just interested + # in running the hooks. + run: | + pre-commit install-hooks + + - name: Run pre-commit on PR commits + run: | + jq '.[]' --raw-output <<< '${{steps.changes.outputs.all_files}}' | + # Run pre-commit to lint and format check files that were changed (but not deleted) compared to master. + xargs pre-commit run --files + + - name: Apply any pre-commit fixed files + if: startsWith(github.ref, 'refs/pull') + uses: getsentry/action-github-commit@748c31dd78cffe76f51bef49a0be856b6effeda7 # v1.1.0 + with: + github-token: ${{ steps.token.outputs.token }} + message: ':hammer_and_wrench: apply pre-commit fixes' diff --git a/.github/workflows/self-hosted-e2e-tests.yml b/.github/workflows/self-hosted-e2e-tests.yml deleted file mode 100644 index efb5a468314aae..00000000000000 --- a/.github/workflows/self-hosted-e2e-tests.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Self-hosted Sentry end to end tests -on: - push: - branches: - - master - - releases/** - pull_request: - -# Cancel in progress workflows on pull_requests. -# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - self-hosted-end-to-end: - name: self-hosted tests - runs-on: ubuntu-20.04 - # temporary, remove once we are confident the action is working - continue-on-error: true - timeout-minutes: 30 - steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 - - - name: Check for backend file changes - uses: getsentry/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # v2.11.1 - id: changes - with: - token: ${{ github.token }} - filters: .github/file-filters.yml - - name: Run Sentry self-hosted e2e CI - if: steps.changes.outputs.backend_all == 'true' - uses: getsentry/action-self-hosted-e2e-tests@3f39ef5bbb432f3f1d163f7e312e8425de2244c7 - with: - project_name: sentry - image_url: us.gcr.io/sentryio/sentry:${{ github.event.pull_request.head.sha || github.sha }} - docker_repo: getsentry/sentry - docker_password: ${{ secrets.DOCKER_HUB_RW_TOKEN }} diff --git a/fixtures/backup/user-with-maximum-privileges.json b/fixtures/backup/user-with-maximum-privileges.json new file mode 100644 index 00000000000000..32ef6372838264 --- /dev/null +++ b/fixtures/backup/user-with-maximum-privileges.json @@ -0,0 +1,99 @@ +[ + { + "model": "sentry.user", + "pk": 1, + "fields": { + "password": "pbkdf2_sha256$150000$iEvdIknqYjTr$+QsGn0tfIJ1FZLxQI37mVU1gL2KbL/wqjMtG/dFhsMA=", + "last_login": null, + "username": "testing@example.com", + "name": "", + "email": "testing@example.com", + "is_staff": true, + "is_active": true, + "is_superuser": true, + "is_managed": false, + "is_sentry_app": null, + "is_password_expired": false, + "last_password_change": "2023-06-22T22:59:57.023Z", + "flags": "0", + "session_nonce": null, + "date_joined": "2023-06-22T22:59:55.488Z", + "last_active": "2023-06-22T22:59:55.489Z", + "avatar_type": 0, + "avatar_url": null + } + }, + { + "model": "sentry.authenticator", + "pk": 1, + "fields": { + "user": 1, + "created_at": "2023-07-27T16:30:53.325Z", + "last_used_at": null, + "type": 1, + "config": "\"\"" + } + }, + { + "model": "sentry.useremail", + "pk": 1, + "fields": { + "user": 1, + "email": "testing@example.com", + "validation_hash": "mCnWesSVvYQcq7qXQ36AZHwosAd6cghE", + "date_hash_added": "2023-06-22T22:59:55.521Z", + "is_verified": true + } + }, + { + "model": "sentry.userip", + "pk": 1, + "fields": { + "user": 1, + "ip_address": "127.0.0.2", + "country_code": null, + "region_code": null, + "first_seen": "2012-04-05T03:29:45.000Z", + "last_seen": "2012-04-05T03:29:45.000Z" + } + }, + { + "model": "sentry.useroption", + "pk": 1, + "fields": { + "user": 1, + "project_id": null, + "organization_id": null, + "key": "timezone", + "value": "\"Europe/Vienna\"" + } + }, + { + "model": "sentry.userpermission", + "pk": 1, + "fields": { + "user": 1, + "permission": "users.admin" + } + }, + { + "model": "sentry.userrole", + "pk": 1, + "fields": { + "date_updated": "2023-06-22T23:00:00.123Z", + "date_added": "2023-06-22T22:54:27.960Z", + "name": "Super Admin", + "permissions": "['broadcasts.admin', 'users.admin', 'options.admin']" + } + }, + { + "model": "sentry.userroleuser", + "pk": 1, + "fields": { + "date_updated": "2023-06-22T23:00:00.123Z", + "date_added": "2023-06-22T22:59:57.000Z", + "user": 1, + "role": 1 + } + } +] diff --git a/fixtures/backup/user-with-minimum-privileges.json b/fixtures/backup/user-with-minimum-privileges.json new file mode 100644 index 00000000000000..53c3195f5a6da3 --- /dev/null +++ b/fixtures/backup/user-with-minimum-privileges.json @@ -0,0 +1,71 @@ +[ + { + "model": "sentry.user", + "pk": 1, + "fields": { + "password": "pbkdf2_sha256$150000$iEvdIknqYjTr$+QsGn0tfIJ1FZLxQI37mVU1gL2KbL/wqjMtG/dFhsMA=", + "last_login": null, + "username": "testing@example.com", + "name": "", + "email": "testing@example.com", + "is_staff": false, + "is_active": true, + "is_superuser": false, + "is_managed": false, + "is_sentry_app": null, + "is_password_expired": false, + "last_password_change": "2023-06-22T22:59:57.023Z", + "flags": "0", + "session_nonce": null, + "date_joined": "2023-06-22T22:59:55.488Z", + "last_active": "2023-06-22T22:59:55.489Z", + "avatar_type": 0, + "avatar_url": null + } + }, + { + "model": "sentry.authenticator", + "pk": 1, + "fields": { + "user": 1, + "created_at": "2023-07-27T16:30:53.325Z", + "last_used_at": null, + "type": 1, + "config": "\"\"" + } + }, + { + "model": "sentry.useremail", + "pk": 1, + "fields": { + "user": 1, + "email": "testing@example.com", + "validation_hash": "mCnWesSVvYQcq7qXQ36AZHwosAd6cghE", + "date_hash_added": "2023-06-22T22:59:55.521Z", + "is_verified": true + } + }, + { + "model": "sentry.userip", + "pk": 1, + "fields": { + "user": 1, + "ip_address": "127.0.0.2", + "country_code": null, + "region_code": null, + "first_seen": "2012-04-05T03:29:45.000Z", + "last_seen": "2012-04-05T03:29:45.000Z" + } + }, + { + "model": "sentry.useroption", + "pk": 1, + "fields": { + "user": 1, + "project_id": null, + "organization_id": null, + "key": "timezone", + "value": "\"Europe/Vienna\"" + } + } +] diff --git a/fixtures/js-stubs/missingMembers.tsx b/fixtures/js-stubs/missingMembers.tsx index e93017a5523cba..06f6cb0097f9ed 100644 --- a/fixtures/js-stubs/missingMembers.tsx +++ b/fixtures/js-stubs/missingMembers.tsx @@ -5,27 +5,27 @@ export function MissingMembers(params = []): MissingMember[] { { commitCount: 6, email: 'hello@sentry.io', - externalId: 'hello', + externalId: 'github:hello', }, { commitCount: 5, email: 'abcd@sentry.io', - externalId: 'abcd', + externalId: 'github:abcd', }, { commitCount: 4, email: 'hola@sentry.io', - externalId: 'hola', + externalId: 'github:hola', }, { commitCount: 3, email: 'test@sentry.io', - externalId: 'test', + externalId: 'github:test', }, { commitCount: 2, email: 'five@sentry.io', - externalId: 'five', + externalId: 'github:five', }, ...params, ]; diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index c4785f25e43f21..f9ff644a6ea3d3 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -7,5 +7,5 @@ will then be regenerated, and you should be able to merge without conflicts. nodestore: 0002_nodestore_no_dictfield replays: 0003_add_size_to_recording_segment -sentry: 0538_remove_name_data_from_rule +sentry: 0539_add_last_state_change_monitorenv social_auth: 0002_default_auto_field diff --git a/package.json b/package.json index 17d678c4b8fe9c..72a6ee4947da36 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,7 @@ "babel-plugin-dynamic-import-node": "^2.3.3", "benchmark": "^2.1.4", "eslint": "8.44.0", - "eslint-config-sentry-app": "1.122.0", + "eslint-config-sentry-app": "1.123.0", "html-webpack-plugin": "^5.5.0", "jest": "29.6.2", "jest-canvas-mock": "^2.5.2", diff --git a/src/sentry/api/api_publish_status.py b/src/sentry/api/api_publish_status.py new file mode 100644 index 00000000000000..295612011d76f4 --- /dev/null +++ b/src/sentry/api/api_publish_status.py @@ -0,0 +1,12 @@ +from enum import Enum + + +class ApiPublishStatus(Enum): + """ + Used to track if an API is publicly documented + """ + + UNKNOWN = "unknown" + PUBLIC = "public" + PRIVATE = "private" + EXPERIMENTAL = "experimental" diff --git a/src/sentry/api/base.py b/src/sentry/api/base.py index 3055de955a9f7b..e2ff8289883ddb 100644 --- a/src/sentry/api/base.py +++ b/src/sentry/api/base.py @@ -22,9 +22,10 @@ from rest_framework.views import APIView from sentry_sdk import Scope -from sentry import analytics, features, options, tsdb +from sentry import analytics, options, tsdb from sentry.api.api_owners import ApiOwner -from sentry.apidocs.hooks import HTTP_METHODS_SET +from sentry.api.api_publish_status import ApiPublishStatus +from sentry.apidocs.hooks import HTTP_METHOD_NAME, HTTP_METHODS_SET from sentry.auth import access from sentry.models import Environment from sentry.ratelimits.config import DEFAULT_RATE_LIMIT_CONFIG, RateLimitConfig @@ -155,7 +156,7 @@ class Endpoint(APIView): public: Optional[HTTP_METHODS_SET] = None owner: ApiOwner = ApiOwner.UNOWNED - + publish_status: dict[HTTP_METHOD_NAME, ApiPublishStatus] = {} rate_limits: RateLimitConfig | dict[ str, dict[RateLimitCategory, RateLimit] ] = DEFAULT_RATE_LIMIT_CONFIG @@ -603,7 +604,7 @@ def validate_slug(self, slug: str) -> str: Validates that the slug is not entirely numeric. Requires a feature flag to be turned on. """ - if features.has("app:enterprise-prevent-numeric-slugs") and slug.isnumeric(): + if options.get("api.prevent-numeric-slugs") and slug.isnumeric(): raise serializers.ValidationError(DEFAULT_SLUG_ERROR_MESSAGE) return slug diff --git a/src/sentry/api/endpoints/accept_organization_invite.py b/src/sentry/api/endpoints/accept_organization_invite.py index 28cc579bcc3c14..dcd869dcafc497 100644 --- a/src/sentry/api/endpoints/accept_organization_invite.py +++ b/src/sentry/api/endpoints/accept_organization_invite.py @@ -7,6 +7,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.api.invite_helper import ( ApiInviteHelper, @@ -66,6 +67,10 @@ def get_invite_state( @control_silo_endpoint class AcceptOrganizationInvite(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } # Disable authentication and permission requirements. permission_classes = [] diff --git a/src/sentry/api/endpoints/accept_project_transfer.py b/src/sentry/api/endpoints/accept_project_transfer.py index 55e33e3a8863f7..b70af2065133d8 100644 --- a/src/sentry/api/endpoints/accept_project_transfer.py +++ b/src/sentry/api/endpoints/accept_project_transfer.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import audit_log, roles +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.decorators import sudo_required from sentry.api.serializers import serialize @@ -24,6 +25,10 @@ class InvalidPayload(Exception): @region_silo_endpoint class AcceptProjectTransferEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = (SessionAuthentication,) permission_classes = (IsAuthenticated,) diff --git a/src/sentry/api/endpoints/actionable_items.py b/src/sentry/api/endpoints/actionable_items.py index 163a1d0ae1df0d..010cd4c1e439f3 100644 --- a/src/sentry/api/endpoints/actionable_items.py +++ b/src/sentry/api/endpoints/actionable_items.py @@ -7,6 +7,7 @@ from sentry import eventstore, features from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.helpers.actionable_items_helper import ( @@ -37,6 +38,9 @@ class SourceMapProcessingResponse(TypedDict): # errors or messages we show to users about problems with their event which we will show the user how to fix. @region_silo_endpoint class ActionableItemsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ISSUES def has_feature(self, organization: Organization, request: Request): diff --git a/src/sentry/api/endpoints/admin_project_configs.py b/src/sentry/api/endpoints/admin_project_configs.py index 4728749cf03761..dbd8d86d061476 100644 --- a/src/sentry/api/endpoints/admin_project_configs.py +++ b/src/sentry/api/endpoints/admin_project_configs.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.permissions import SuperuserPermission from sentry.models import Project @@ -10,6 +11,9 @@ @region_silo_endpoint class AdminRelayProjectConfigsEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/api_application_details.py b/src/sentry/api/endpoints/api_application_details.py index 42ac59ba06ecc0..1f2f110a02fbbc 100644 --- a/src/sentry/api/endpoints/api_application_details.py +++ b/src/sentry/api/endpoints/api_application_details.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from rest_framework.serializers import ListField +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers import serialize @@ -33,6 +34,11 @@ class ApiApplicationSerializer(serializers.Serializer): @control_silo_endpoint class ApiApplicationDetailsEndpoint(Endpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } authentication_classes = (SessionAuthentication,) permission_classes = (IsAuthenticated,) diff --git a/src/sentry/api/endpoints/api_application_rotate_secret.py b/src/sentry/api/endpoints/api_application_rotate_secret.py index 132936af1f25e8..364773bdff05f8 100644 --- a/src/sentry/api/endpoints/api_application_rotate_secret.py +++ b/src/sentry/api/endpoints/api_application_rotate_secret.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers import serialize @@ -12,6 +13,9 @@ @control_silo_endpoint class ApiApplicationRotateSecretEndpoint(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = (SessionAuthentication,) permission_classes = (IsAuthenticated,) diff --git a/src/sentry/api/endpoints/api_applications.py b/src/sentry/api/endpoints/api_applications.py index 409cdcc1d71edf..ca0907e11d16ea 100644 --- a/src/sentry/api/endpoints/api_applications.py +++ b/src/sentry/api/endpoints/api_applications.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.api.paginator import OffsetPaginator from sentry.api.serializers import serialize @@ -11,6 +12,10 @@ @control_silo_endpoint class ApiApplicationsEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = (SessionAuthentication,) permission_classes = (IsAuthenticated,) diff --git a/src/sentry/api/endpoints/api_authorizations.py b/src/sentry/api/endpoints/api_authorizations.py index da6c2dd0c47e88..30d799c8e16f68 100644 --- a/src/sentry/api/endpoints/api_authorizations.py +++ b/src/sentry/api/endpoints/api_authorizations.py @@ -5,6 +5,7 @@ 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.api.paginator import OffsetPaginator from sentry.api.serializers import serialize @@ -13,6 +14,10 @@ @control_silo_endpoint class ApiAuthorizationsEndpoint(Endpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE authentication_classes = (SessionAuthentication,) permission_classes = (IsAuthenticated,) diff --git a/src/sentry/api/endpoints/api_tokens.py b/src/sentry/api/endpoints/api_tokens.py index 807cb128e243fb..2d8d1d95c66f97 100644 --- a/src/sentry/api/endpoints/api_tokens.py +++ b/src/sentry/api/endpoints/api_tokens.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import analytics +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.authentication import SessionNoAuthTokenAuthentication from sentry.api.base import Endpoint, control_silo_endpoint from sentry.api.fields import MultipleChoiceField @@ -22,6 +23,11 @@ class ApiTokenSerializer(serializers.Serializer): @control_silo_endpoint class ApiTokensEndpoint(Endpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = (SessionNoAuthTokenAuthentication,) permission_classes = (IsAuthenticated,) diff --git a/src/sentry/api/endpoints/artifact_bundles.py b/src/sentry/api/endpoints/artifact_bundles.py index a09c225ef56ba1..a77b8b8119d43b 100644 --- a/src/sentry/api/endpoints/artifact_bundles.py +++ b/src/sentry/api/endpoints/artifact_bundles.py @@ -9,6 +9,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.api.exceptions import ResourceDoesNotExist, SentryAPIException @@ -52,6 +53,10 @@ def is_valid_uuid(cls, value): @region_silo_endpoint class ArtifactBundlesEndpoint(ProjectEndpoint, ArtifactBundlesMixin): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) def get(self, request: Request, project) -> Response: diff --git a/src/sentry/api/endpoints/artifact_lookup.py b/src/sentry/api/endpoints/artifact_lookup.py index 4b8b430963508e..7fbefee9eb59fd 100644 --- a/src/sentry/api/endpoints/artifact_lookup.py +++ b/src/sentry/api/endpoints/artifact_lookup.py @@ -8,6 +8,7 @@ from symbolic.exceptions import SymbolicError from sentry import ratelimits +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.api.endpoints.debug_files import has_download_permission @@ -32,6 +33,9 @@ @region_silo_endpoint class ProjectArtifactLookupEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) def download_file(self, download_id, project: Project): diff --git a/src/sentry/api/endpoints/assistant.py b/src/sentry/api/endpoints/assistant.py index da36e57796deb4..c819830655a938 100644 --- a/src/sentry/api/endpoints/assistant.py +++ b/src/sentry/api/endpoints/assistant.py @@ -9,6 +9,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.assistant import manager from sentry.models import AssistantActivity @@ -55,6 +56,10 @@ def validate(self, attrs): @control_silo_endpoint class AssistantEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (IsAuthenticated,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/auth_config.py b/src/sentry/api/endpoints/auth_config.py index 12f8ff34a7f978..0b9f012be01578 100644 --- a/src/sentry/api/endpoints/auth_config.py +++ b/src/sentry/api/endpoints/auth_config.py @@ -6,6 +6,7 @@ from sentry import newsletter 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.constants import WARN_SESSION_EXPIRED from sentry.http import get_server_hostname @@ -22,6 +23,9 @@ @control_silo_endpoint class AuthConfigEndpoint(Endpoint, OrganizationMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE # Disable authentication and permission requirements. permission_classes = [] diff --git a/src/sentry/api/endpoints/auth_index.py b/src/sentry/api/endpoints/auth_index.py index dcbcb6deb43cd4..e8becce053d673 100644 --- a/src/sentry/api/endpoints/auth_index.py +++ b/src/sentry/api/endpoints/auth_index.py @@ -11,6 +11,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.authentication import QuietBasicAuthentication from sentry.api.base import Endpoint, control_silo_endpoint from sentry.api.exceptions import SsoRequired @@ -40,6 +41,12 @@ @control_silo_endpoint class AuthIndexEndpoint(Endpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } """ Manage session authentication diff --git a/src/sentry/api/endpoints/auth_login.py b/src/sentry/api/endpoints/auth_login.py index 01da41e83d5851..3d594348cae4e7 100644 --- a/src/sentry/api/endpoints/auth_login.py +++ b/src/sentry/api/endpoints/auth_login.py @@ -5,6 +5,7 @@ from sentry import ratelimits as ratelimiter 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.api.serializers.base import serialize from sentry.api.serializers.models.user import DetailedSelfUserSerializer @@ -17,6 +18,9 @@ @control_silo_endpoint class AuthLoginEndpoint(Endpoint, OrganizationMixin): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE # Disable authentication and permission requirements. permission_classes = [] diff --git a/src/sentry/api/endpoints/authenticator_index.py b/src/sentry/api/endpoints/authenticator_index.py index e1471bf04fbea4..b4b7407518f0ae 100644 --- a/src/sentry/api/endpoints/authenticator_index.py +++ b/src/sentry/api/endpoints/authenticator_index.py @@ -5,12 +5,16 @@ 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 import Authenticator @control_silo_endpoint class AuthenticatorIndexEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE permission_classes = (IsAuthenticated,) diff --git a/src/sentry/api/endpoints/avatar/doc_integration.py b/src/sentry/api/endpoints/avatar/doc_integration.py index 6e502c7e7b7ac1..5f41b31d2ee7aa 100644 --- a/src/sentry/api/endpoints/avatar/doc_integration.py +++ b/src/sentry/api/endpoints/avatar/doc_integration.py @@ -1,3 +1,4 @@ +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.avatar import AvatarMixin from sentry.api.bases.doc_integrations import DocIntegrationBaseEndpoint @@ -7,6 +8,10 @@ @control_silo_endpoint class DocIntegrationAvatarEndpoint(AvatarMixin, DocIntegrationBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } object_type = "doc_integration" model = DocIntegrationAvatar serializer_cls = DocIntegrationAvatarSerializer diff --git a/src/sentry/api/endpoints/avatar/organization.py b/src/sentry/api/endpoints/avatar/organization.py index 678f7a51d9a2ba..e2d74311af2ab3 100644 --- a/src/sentry/api/endpoints/avatar/organization.py +++ b/src/sentry/api/endpoints/avatar/organization.py @@ -1,3 +1,4 @@ +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.avatar import AvatarMixin from sentry.api.bases.organization import OrganizationEndpoint @@ -6,6 +7,10 @@ @region_silo_endpoint class OrganizationAvatarEndpoint(AvatarMixin, OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } object_type = "organization" model = OrganizationAvatar diff --git a/src/sentry/api/endpoints/avatar/project.py b/src/sentry/api/endpoints/avatar/project.py index 6526b3f833c258..d5b0b79d6854ca 100644 --- a/src/sentry/api/endpoints/avatar/project.py +++ b/src/sentry/api/endpoints/avatar/project.py @@ -1,3 +1,4 @@ +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.avatar import AvatarMixin from sentry.api.bases.project import ProjectEndpoint @@ -6,5 +7,9 @@ @region_silo_endpoint class ProjectAvatarEndpoint(AvatarMixin, ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } object_type = "project" model = ProjectAvatar diff --git a/src/sentry/api/endpoints/avatar/sentry_app.py b/src/sentry/api/endpoints/avatar/sentry_app.py index e872213aec4c19..f7c74257c75721 100644 --- a/src/sentry/api/endpoints/avatar/sentry_app.py +++ b/src/sentry/api/endpoints/avatar/sentry_app.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases import SentryAppBaseEndpoint from sentry.api.bases.avatar import AvatarMixin @@ -10,6 +11,10 @@ @control_silo_endpoint class SentryAppAvatarEndpoint(AvatarMixin, SentryAppBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } object_type = "sentry_app" model = SentryAppAvatar serializer_cls = SentryAppAvatarSerializer diff --git a/src/sentry/api/endpoints/avatar/team.py b/src/sentry/api/endpoints/avatar/team.py index 3b935d0f0e77ac..7d53aa8ceb82a9 100644 --- a/src/sentry/api/endpoints/avatar/team.py +++ b/src/sentry/api/endpoints/avatar/team.py @@ -1,3 +1,4 @@ +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.avatar import AvatarMixin from sentry.api.bases.team import TeamEndpoint @@ -6,5 +7,9 @@ @region_silo_endpoint class TeamAvatarEndpoint(AvatarMixin, TeamEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } object_type = "team" model = TeamAvatar diff --git a/src/sentry/api/endpoints/avatar/user.py b/src/sentry/api/endpoints/avatar/user.py index 089a97b3edee78..492deec2865240 100644 --- a/src/sentry/api/endpoints/avatar/user.py +++ b/src/sentry/api/endpoints/avatar/user.py @@ -3,6 +3,7 @@ from rest_framework.response import Response from sentry import options +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.avatar import AvatarMixin from sentry.api.bases.user import UserEndpoint @@ -13,6 +14,10 @@ @control_silo_endpoint class UserAvatarEndpoint(AvatarMixin, UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } object_type = "user" model = UserAvatar diff --git a/src/sentry/api/endpoints/broadcast_details.py b/src/sentry/api/endpoints/broadcast_details.py index b3622a32a77bf2..b3e8782ab04fca 100644 --- a/src/sentry/api/endpoints/broadcast_details.py +++ b/src/sentry/api/endpoints/broadcast_details.py @@ -5,6 +5,7 @@ from django.utils import timezone from rest_framework.permissions import IsAuthenticated +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers import AdminBroadcastSerializer, BroadcastSerializer, serialize @@ -20,6 +21,10 @@ @control_silo_endpoint class BroadcastDetailsEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (IsAuthenticated,) def _get_broadcast(self, request: Request, broadcast_id): diff --git a/src/sentry/api/endpoints/broadcast_index.py b/src/sentry/api/endpoints/broadcast_index.py index 8ebc27f7862acf..7f29c73020bd35 100644 --- a/src/sentry/api/endpoints/broadcast_index.py +++ b/src/sentry/api/endpoints/broadcast_index.py @@ -8,6 +8,7 @@ from django.db.models import Q from django.utils import timezone +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.organization import ControlSiloOrganizationEndpoint, OrganizationPermission from sentry.api.paginator import DateTimePaginator @@ -27,6 +28,11 @@ @control_silo_endpoint class BroadcastIndexEndpoint(ControlSiloOrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationPermission,) def _get_serializer(self, request: Request): diff --git a/src/sentry/api/endpoints/builtin_symbol_sources.py b/src/sentry/api/endpoints/builtin_symbol_sources.py index 4cdccbfdace074..19e4d839e03917 100644 --- a/src/sentry/api/endpoints/builtin_symbol_sources.py +++ b/src/sentry/api/endpoints/builtin_symbol_sources.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.serializers import serialize @@ -17,6 +18,9 @@ def normalize_symbol_source(key, source): @region_silo_endpoint class BuiltinSymbolSourcesEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = () def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/check_am2_compatibility.py b/src/sentry/api/endpoints/check_am2_compatibility.py index 9caaaa00e93abe..db9b030e84079c 100644 --- a/src/sentry/api/endpoints/check_am2_compatibility.py +++ b/src/sentry/api/endpoints/check_am2_compatibility.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.permissions import SuperuserPermission from sentry.tasks.check_am2_compatibility import ( @@ -14,6 +15,9 @@ @region_silo_endpoint class CheckAM2CompatibilityEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/chunk.py b/src/sentry/api/endpoints/chunk.py index 86d8f31bfc7243..27c5cf856fb3bc 100644 --- a/src/sentry/api/endpoints/chunk.py +++ b/src/sentry/api/endpoints/chunk.py @@ -11,6 +11,7 @@ from sentry import options from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationReleasePermission from sentry.models import FileBlob @@ -46,6 +47,10 @@ def __init__(self, file): @region_silo_endpoint class ChunkUploadEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.OWNERS_NATIVE permission_classes = (OrganizationReleasePermission,) rate_limits = RateLimitConfig(group="CLI") diff --git a/src/sentry/api/endpoints/codeowners/details.py b/src/sentry/api/endpoints/codeowners/details.py index cada9adfedbecc..2b1a54b717dff4 100644 --- a/src/sentry/api/endpoints/codeowners/details.py +++ b/src/sentry/api/endpoints/codeowners/details.py @@ -9,6 +9,7 @@ from rest_framework.response import Response from sentry import analytics +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -23,6 +24,11 @@ @region_silo_endpoint class ProjectCodeOwnersDetailsEndpoint(ProjectEndpoint, ProjectCodeOwnersMixin): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def convert_args( self, request: Request, diff --git a/src/sentry/api/endpoints/codeowners/external_actor/team_details.py b/src/sentry/api/endpoints/codeowners/external_actor/team_details.py index 4875c41b200ce1..2183ea8512edd9 100644 --- a/src/sentry/api/endpoints/codeowners/external_actor/team_details.py +++ b/src/sentry/api/endpoints/codeowners/external_actor/team_details.py @@ -5,6 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.external_actor import ExternalActorEndpointMixin, ExternalTeamSerializer from sentry.api.bases.team import TeamEndpoint @@ -16,6 +17,11 @@ @region_silo_endpoint class ExternalTeamDetailsEndpoint(TeamEndpoint, ExternalActorEndpointMixin): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def convert_args( self, request: Request, diff --git a/src/sentry/api/endpoints/codeowners/external_actor/team_index.py b/src/sentry/api/endpoints/codeowners/external_actor/team_index.py index e5244acffbdd65..fec092110855b3 100644 --- a/src/sentry/api/endpoints/codeowners/external_actor/team_index.py +++ b/src/sentry/api/endpoints/codeowners/external_actor/team_index.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.external_actor import ExternalActorEndpointMixin, ExternalTeamSerializer from sentry.api.bases.team import TeamEndpoint @@ -15,6 +16,10 @@ @region_silo_endpoint class ExternalTeamEndpoint(TeamEndpoint, ExternalActorEndpointMixin): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } + def post(self, request: Request, team: Team) -> Response: """ Create an External Team diff --git a/src/sentry/api/endpoints/codeowners/external_actor/user_details.py b/src/sentry/api/endpoints/codeowners/external_actor/user_details.py index 37232dabd5f664..4274e3208e7c27 100644 --- a/src/sentry/api/endpoints/codeowners/external_actor/user_details.py +++ b/src/sentry/api/endpoints/codeowners/external_actor/user_details.py @@ -7,6 +7,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.external_actor import ExternalActorEndpointMixin, ExternalUserSerializer from sentry.api.bases.organization import OrganizationEndpoint @@ -18,6 +19,11 @@ @control_silo_endpoint class ExternalUserDetailsEndpoint(OrganizationEndpoint, ExternalActorEndpointMixin): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def convert_args( # type: ignore[override] self, request: Request, diff --git a/src/sentry/api/endpoints/codeowners/external_actor/user_index.py b/src/sentry/api/endpoints/codeowners/external_actor/user_index.py index dff12297bc0245..4a66928a4d4546 100644 --- a/src/sentry/api/endpoints/codeowners/external_actor/user_index.py +++ b/src/sentry/api/endpoints/codeowners/external_actor/user_index.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationEndpoint from sentry.api.bases.external_actor import ExternalActorEndpointMixin, ExternalUserSerializer @@ -15,6 +16,10 @@ @region_silo_endpoint class ExternalUserEndpoint(OrganizationEndpoint, ExternalActorEndpointMixin): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } + def post(self, request: Request, organization: Organization) -> Response: """ Create an External User diff --git a/src/sentry/api/endpoints/codeowners/index.py b/src/sentry/api/endpoints/codeowners/index.py index c09e368d4cbe68..18864dae15eca9 100644 --- a/src/sentry/api/endpoints/codeowners/index.py +++ b/src/sentry/api/endpoints/codeowners/index.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import analytics, features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.serializers import serialize @@ -17,6 +18,11 @@ @region_silo_endpoint class ProjectCodeOwnersEndpoint(ProjectEndpoint, ProjectCodeOwnersMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } + def add_owner_id_to_schema(self, codeowner: ProjectCodeOwners, project: Project) -> None: if not hasattr(codeowner, "schema") or ( codeowner.schema diff --git a/src/sentry/api/endpoints/data_scrubbing_selector_suggestions.py b/src/sentry/api/endpoints/data_scrubbing_selector_suggestions.py index 6bfbda88f31a68..c80bb932b2d92c 100644 --- a/src/sentry/api/endpoints/data_scrubbing_selector_suggestions.py +++ b/src/sentry/api/endpoints/data_scrubbing_selector_suggestions.py @@ -5,6 +5,7 @@ from sentry_relay.processing import pii_selector_suggestions_from_event from sentry import nodestore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.eventstore.models import Event @@ -12,6 +13,10 @@ @region_silo_endpoint class DataScrubbingSelectorSuggestionsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: """ Generate a list of data scrubbing selectors from existing event data. diff --git a/src/sentry/api/endpoints/debug_files.py b/src/sentry/api/endpoints/debug_files.py index d990b1ec88650d..c0ac4f8ba70b61 100644 --- a/src/sentry/api/endpoints/debug_files.py +++ b/src/sentry/api/endpoints/debug_files.py @@ -15,6 +15,7 @@ from symbolic.exceptions import SymbolicError from sentry import ratelimits, roles +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.api.exceptions import ResourceDoesNotExist @@ -88,6 +89,10 @@ def has_download_permission(request, project): @region_silo_endpoint class ProguardArtifactReleasesEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) def post(self, request: Request, project) -> Response: @@ -165,6 +170,11 @@ def get(self, request: Request, project) -> Response: @region_silo_endpoint class DebugFilesEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) def download(self, debug_file_id, project): @@ -340,6 +350,9 @@ def post(self, request: Request, project) -> Response: @region_silo_endpoint class UnknownDebugFilesEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) def get(self, request: Request, project) -> Response: @@ -350,6 +363,9 @@ def get(self, request: Request, project) -> Response: @region_silo_endpoint class AssociateDSymFilesEndpoint(ProjectEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) # Legacy endpoint, kept for backwards compatibility @@ -359,6 +375,9 @@ def post(self, request: Request, project) -> Response: @region_silo_endpoint class DifAssembleEndpoint(ProjectEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) def post(self, request: Request, project) -> Response: @@ -477,6 +496,10 @@ def post(self, request: Request, project) -> Response: @region_silo_endpoint class SourceMapsEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) def get(self, request: Request, project) -> Response: diff --git a/src/sentry/api/endpoints/event_ai_suggested_fix.py b/src/sentry/api/endpoints/event_ai_suggested_fix.py index 7644462ddf0c7e..a907230846c725 100644 --- a/src/sentry/api/endpoints/event_ai_suggested_fix.py +++ b/src/sentry/api/endpoints/event_ai_suggested_fix.py @@ -7,6 +7,7 @@ from django.http import HttpResponse, StreamingHttpResponse from sentry import eventstore, features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -274,6 +275,9 @@ def reduce_stream(response): @region_silo_endpoint class EventAiSuggestedFixEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.PRIVATE, + } # go away private = True enforce_rate_limit = True diff --git a/src/sentry/api/endpoints/event_apple_crash_report.py b/src/sentry/api/endpoints/event_apple_crash_report.py index 55dc53075960c8..b4620781578a86 100644 --- a/src/sentry/api/endpoints/event_apple_crash_report.py +++ b/src/sentry/api/endpoints/event_apple_crash_report.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from sentry import eventstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -11,6 +12,10 @@ @region_silo_endpoint class EventAppleCrashReportEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, event_id) -> HttpResponse: """ Retrieve an Apple Crash Report from an event diff --git a/src/sentry/api/endpoints/event_attachment_details.py b/src/sentry/api/endpoints/event_attachment_details.py index c4dd332efb665e..88365153aa7d57 100644 --- a/src/sentry/api/endpoints/event_attachment_details.py +++ b/src/sentry/api/endpoints/event_attachment_details.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import eventstore, features, roles +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectPermission from sentry.api.serializers import serialize @@ -45,6 +46,10 @@ def has_object_permission(self, request: Request, view, project): @region_silo_endpoint class EventAttachmentDetailsEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (EventAttachmentDetailsPermission,) def download(self, attachment): diff --git a/src/sentry/api/endpoints/event_attachments.py b/src/sentry/api/endpoints/event_attachments.py index 8242ff9fbbf7ab..74077e2600610b 100644 --- a/src/sentry/api/endpoints/event_attachments.py +++ b/src/sentry/api/endpoints/event_attachments.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import eventstore, features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.paginator import OffsetPaginator @@ -12,6 +13,10 @@ @region_silo_endpoint class EventAttachmentsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, event_id) -> Response: """ Retrieve attachments for an event diff --git a/src/sentry/api/endpoints/event_file_committers.py b/src/sentry/api/endpoints/event_file_committers.py index 51b73f459a7b86..b379a6a1bfd02d 100644 --- a/src/sentry/api/endpoints/event_file_committers.py +++ b/src/sentry/api/endpoints/event_file_committers.py @@ -3,6 +3,7 @@ from rest_framework.response import Response from sentry import eventstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.models import Commit, Group, Release @@ -11,6 +12,10 @@ @region_silo_endpoint class EventFileCommittersEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, event_id) -> Response: """ Retrieve Committer information for an event diff --git a/src/sentry/api/endpoints/event_grouping_info.py b/src/sentry/api/endpoints/event_grouping_info.py index e502a333e41225..dc4a50c7d366e2 100644 --- a/src/sentry/api/endpoints/event_grouping_info.py +++ b/src/sentry/api/endpoints/event_grouping_info.py @@ -3,6 +3,7 @@ from django.http import HttpRequest, HttpResponse from sentry import eventstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -16,6 +17,10 @@ @region_silo_endpoint class EventGroupingInfoEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: HttpRequest, project, event_id) -> HttpResponse: """ Returns the grouping information for an event diff --git a/src/sentry/api/endpoints/event_owners.py b/src/sentry/api/endpoints/event_owners.py index d397fbd3192ecf..56cac7eda3d306 100644 --- a/src/sentry/api/endpoints/event_owners.py +++ b/src/sentry/api/endpoints/event_owners.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import eventstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.serializers import serialize @@ -12,6 +13,10 @@ @region_silo_endpoint class EventOwnersEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, event_id) -> Response: """ Retrieve suggested owners information for an event diff --git a/src/sentry/api/endpoints/event_reprocessable.py b/src/sentry/api/endpoints/event_reprocessable.py index b986f944248d6f..2713760501b73c 100644 --- a/src/sentry/api/endpoints/event_reprocessable.py +++ b/src/sentry/api/endpoints/event_reprocessable.py @@ -3,6 +3,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.reprocessing2 import CannotReprocess, pull_event_data @@ -10,6 +11,10 @@ @region_silo_endpoint class EventReprocessableEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, event_id) -> Response: """ Retrieve information about whether an event can be reprocessed. diff --git a/src/sentry/api/endpoints/filechange.py b/src/sentry/api/endpoints/filechange.py index f510c29bc7ac20..b402496f7ff678 100644 --- a/src/sentry/api/endpoints/filechange.py +++ b/src/sentry/api/endpoints/filechange.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -12,6 +13,10 @@ @region_silo_endpoint class CommitFileChangeEndpoint(OrganizationReleasesBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, version) -> Response: """ Retrieve Files Changed in a Release's Commits diff --git a/src/sentry/api/endpoints/group_activities.py b/src/sentry/api/endpoints/group_activities.py index 379183849b75c8..d6b7551fb05d9c 100644 --- a/src/sentry/api/endpoints/group_activities.py +++ b/src/sentry/api/endpoints/group_activities.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases import GroupEndpoint from sentry.api.serializers import serialize @@ -9,6 +10,10 @@ @region_silo_endpoint class GroupActivitiesEndpoint(GroupEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, group) -> Response: """ Retrieve all the Activities for a Group diff --git a/src/sentry/api/endpoints/group_attachments.py b/src/sentry/api/endpoints/group_attachments.py index 84f78605b581a8..95bceb74a280a5 100644 --- a/src/sentry/api/endpoints/group_attachments.py +++ b/src/sentry/api/endpoints/group_attachments.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.group import GroupEndpoint from sentry.api.paginator import DateTimePaginator @@ -11,6 +12,10 @@ @region_silo_endpoint class GroupAttachmentsEndpoint(GroupEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, group) -> Response: """ List Event Attachments diff --git a/src/sentry/api/endpoints/group_current_release.py b/src/sentry/api/endpoints/group_current_release.py index 9b9a84c9694a1f..4cbf0dca56c871 100644 --- a/src/sentry/api/endpoints/group_current_release.py +++ b/src/sentry/api/endpoints/group_current_release.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases import GroupEndpoint from sentry.api.helpers.environments import get_environments @@ -12,6 +13,10 @@ @region_silo_endpoint class GroupCurrentReleaseEndpoint(GroupEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def _get_current_release(self, group, environments): release_projects = ReleaseProject.objects.filter(project_id=group.project_id).values_list( "release_id", flat=True diff --git a/src/sentry/api/endpoints/group_details.py b/src/sentry/api/endpoints/group_details.py index 17a1d96b8a4dd4..4e4968268c4cdf 100644 --- a/src/sentry/api/endpoints/group_details.py +++ b/src/sentry/api/endpoints/group_details.py @@ -10,6 +10,7 @@ from sentry import features, tagstore, tsdb from sentry.api import client +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases import GroupEndpoint from sentry.api.helpers.environments import get_environments @@ -39,6 +40,11 @@ @region_silo_endpoint class GroupDetailsEndpoint(GroupEndpoint, EnvironmentMixin): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } enforce_rate_limit = True rate_limits = { "GET": { diff --git a/src/sentry/api/endpoints/group_event_details.py b/src/sentry/api/endpoints/group_event_details.py index 1e0ecce4d00100..dcee0240b5a03d 100644 --- a/src/sentry/api/endpoints/group_event_details.py +++ b/src/sentry/api/endpoints/group_event_details.py @@ -9,6 +9,7 @@ from snuba_sdk.legacy import is_condition, parse_condition from sentry import eventstore, features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.group import GroupEndpoint from sentry.api.endpoints.project_event_details import wrap_event_response @@ -94,6 +95,9 @@ def issue_search_query_to_conditions( @region_silo_endpoint class GroupEventDetailsEndpoint(GroupEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } enforce_rate_limit = True rate_limits = { "GET": { diff --git a/src/sentry/api/endpoints/group_events.py b/src/sentry/api/endpoints/group_events.py index 29f542d7f97649..038eca7b72b6e4 100644 --- a/src/sentry/api/endpoints/group_events.py +++ b/src/sentry/api/endpoints/group_events.py @@ -10,6 +10,7 @@ from sentry import eventstore from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases import GroupEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -37,6 +38,9 @@ class GroupEventsError(Exception): @region_silo_endpoint class GroupEventsEndpoint(GroupEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ISSUES def get(self, request: Request, group: Group) -> Response: diff --git a/src/sentry/api/endpoints/group_external_issue_details.py b/src/sentry/api/endpoints/group_external_issue_details.py index 6242e88c3739a4..07e0d5a067cc08 100644 --- a/src/sentry/api/endpoints/group_external_issue_details.py +++ b/src/sentry/api/endpoints/group_external_issue_details.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import deletions +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.group import GroupEndpoint from sentry.models import PlatformExternalIssue @@ -9,6 +10,10 @@ @region_silo_endpoint class GroupExternalIssueDetailsEndpoint(GroupEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + } + def delete(self, request: Request, external_issue_id, group) -> Response: try: external_issue = PlatformExternalIssue.objects.get( diff --git a/src/sentry/api/endpoints/group_external_issues.py b/src/sentry/api/endpoints/group_external_issues.py index 81acb4ba5d0f7e..38de9a1675123a 100644 --- a/src/sentry/api/endpoints/group_external_issues.py +++ b/src/sentry/api/endpoints/group_external_issues.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.group import GroupEndpoint from sentry.api.serializers import serialize @@ -13,6 +14,10 @@ @region_silo_endpoint class GroupExternalIssuesEndpoint(GroupEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, group) -> Response: external_issues = PlatformExternalIssue.objects.filter(group_id=group.id) diff --git a/src/sentry/api/endpoints/group_first_last_release.py b/src/sentry/api/endpoints/group_first_last_release.py index d0626302c23e0c..35f09b73a59aef 100644 --- a/src/sentry/api/endpoints/group_first_last_release.py +++ b/src/sentry/api/endpoints/group_first_last_release.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases import GroupEndpoint from sentry.api.helpers.group_index import get_first_last_release @@ -9,6 +10,9 @@ @region_silo_endpoint class GroupFirstLastReleaseEndpoint(GroupEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } enforce_rate_limit = True rate_limits = { "GET": { diff --git a/src/sentry/api/endpoints/group_hashes.py b/src/sentry/api/endpoints/group_hashes.py index 79a37f0d1ec45b..ec4088a559fa0a 100644 --- a/src/sentry/api/endpoints/group_hashes.py +++ b/src/sentry/api/endpoints/group_hashes.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import eventstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import GroupEndpoint from sentry.api.paginator import GenericOffsetPaginator @@ -16,6 +17,11 @@ @region_silo_endpoint class GroupHashesEndpoint(GroupEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, group) -> Response: """ List an Issue's Hashes diff --git a/src/sentry/api/endpoints/group_hashes_split.py b/src/sentry/api/endpoints/group_hashes_split.py index 16042ae788a73b..31ba988bf8b873 100644 --- a/src/sentry/api/endpoints/group_hashes_split.py +++ b/src/sentry/api/endpoints/group_hashes_split.py @@ -11,6 +11,7 @@ from snuba_sdk.query import Column, Entity, Function, Query from sentry import eventstore, features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import GroupEndpoint from sentry.api.serializers import EventSerializer, serialize @@ -21,6 +22,12 @@ @region_silo_endpoint class GroupHashesSplitEndpoint(GroupEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, group) -> Response: """ Return information on whether the group can be split up, has been split diff --git a/src/sentry/api/endpoints/group_integration_details.py b/src/sentry/api/endpoints/group_integration_details.py index 89e672f36bfe2f..b86f6b5832ac3b 100644 --- a/src/sentry/api/endpoints/group_integration_details.py +++ b/src/sentry/api/endpoints/group_integration_details.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import GroupEndpoint from sentry.api.serializers import serialize @@ -48,6 +49,13 @@ def serialize( @region_silo_endpoint class GroupIntegrationDetailsEndpoint(GroupEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } + def _has_issue_feature(self, organization, user): has_issue_basic = features.has( "organizations:integrations-issue-basic", organization, actor=user diff --git a/src/sentry/api/endpoints/group_integrations.py b/src/sentry/api/endpoints/group_integrations.py index b6eb0f901bfd6f..8e5e2184ca1cd9 100644 --- a/src/sentry/api/endpoints/group_integrations.py +++ b/src/sentry/api/endpoints/group_integrations.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import features, integrations +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import GroupEndpoint from sentry.api.serializers import IntegrationSerializer, serialize @@ -69,6 +70,10 @@ def serialize( @region_silo_endpoint class GroupIntegrationsEndpoint(GroupEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, group) -> Response: has_issue_basic = features.has( "organizations:integrations-issue-basic", group.organization, actor=request.user diff --git a/src/sentry/api/endpoints/group_notes.py b/src/sentry/api/endpoints/group_notes.py index 308272b0a2c153..3d2ce30cffe5b2 100644 --- a/src/sentry/api/endpoints/group_notes.py +++ b/src/sentry/api/endpoints/group_notes.py @@ -5,6 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.group import GroupEndpoint from sentry.api.paginator import DateTimePaginator @@ -19,6 +20,11 @@ @region_silo_endpoint class GroupNotesEndpoint(GroupEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, group) -> Response: notes = Activity.objects.filter(group=group, type=ActivityType.NOTE.value) diff --git a/src/sentry/api/endpoints/group_notes_details.py b/src/sentry/api/endpoints/group_notes_details.py index 089714575ed0e0..1e52ff4798d2e7 100644 --- a/src/sentry/api/endpoints/group_notes_details.py +++ b/src/sentry/api/endpoints/group_notes_details.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.group import GroupEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -15,6 +16,11 @@ @region_silo_endpoint class GroupNotesDetailsEndpoint(GroupEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + # We explicitly don't allow a request with an ApiKey # since an ApiKey is bound to the Organization, not # an individual. Not sure if we'd want to allow an ApiKey diff --git a/src/sentry/api/endpoints/group_participants.py b/src/sentry/api/endpoints/group_participants.py index 4e533bd6476146..61e3966ac65dba 100644 --- a/src/sentry/api/endpoints/group_participants.py +++ b/src/sentry/api/endpoints/group_participants.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import GroupEndpoint from sentry.models import GroupSubscriptionManager @@ -11,6 +12,10 @@ @region_silo_endpoint class GroupParticipantsEndpoint(GroupEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, group, organization_slug: str | None = None) -> Response: participants = GroupSubscriptionManager.get_participating_user_ids(group) diff --git a/src/sentry/api/endpoints/group_reprocessing.py b/src/sentry/api/endpoints/group_reprocessing.py index 29c7e04bfed001..1015bd7314d556 100644 --- a/src/sentry/api/endpoints/group_reprocessing.py +++ b/src/sentry/api/endpoints/group_reprocessing.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import GroupEndpoint from sentry.tasks.reprocessing2 import reprocess_group @@ -9,6 +10,10 @@ @region_silo_endpoint class GroupReprocessingEndpoint(GroupEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } + def post(self, request: Request, group) -> Response: """ Reprocess a group diff --git a/src/sentry/api/endpoints/group_similar_issues.py b/src/sentry/api/endpoints/group_similar_issues.py index 82baaf35f25285..f42bbd72fe9227 100644 --- a/src/sentry/api/endpoints/group_similar_issues.py +++ b/src/sentry/api/endpoints/group_similar_issues.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import similarity +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.group import GroupEndpoint from sentry.api.serializers import serialize @@ -20,6 +21,10 @@ def _fix_label(label): @region_silo_endpoint class GroupSimilarIssuesEndpoint(GroupEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, group) -> Response: features = similarity.features diff --git a/src/sentry/api/endpoints/group_stats.py b/src/sentry/api/endpoints/group_stats.py index a590fb4f5545df..358f28603a4a63 100644 --- a/src/sentry/api/endpoints/group_stats.py +++ b/src/sentry/api/endpoints/group_stats.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import tsdb +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, StatsMixin, region_silo_endpoint from sentry.api.bases.group import GroupEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -11,6 +12,10 @@ @region_silo_endpoint class GroupStatsEndpoint(GroupEndpoint, EnvironmentMixin, StatsMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, group) -> Response: try: environment_id = self._get_environment_id_from_request( diff --git a/src/sentry/api/endpoints/group_tagkey_details.py b/src/sentry/api/endpoints/group_tagkey_details.py index 65111d041e9123..e985e1ca4856b3 100644 --- a/src/sentry/api/endpoints/group_tagkey_details.py +++ b/src/sentry/api/endpoints/group_tagkey_details.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import tagstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.group import GroupEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -11,6 +12,10 @@ @region_silo_endpoint class GroupTagKeyDetailsEndpoint(GroupEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, group, key) -> Response: """ Retrieve Tag Details diff --git a/src/sentry/api/endpoints/group_tagkey_values.py b/src/sentry/api/endpoints/group_tagkey_values.py index 86e0dcd3479cc5..d1632b10727c0c 100644 --- a/src/sentry/api/endpoints/group_tagkey_values.py +++ b/src/sentry/api/endpoints/group_tagkey_values.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import tagstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.group import GroupEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -12,6 +13,10 @@ @region_silo_endpoint class GroupTagKeyValuesEndpoint(GroupEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, group, key) -> Response: """ List a Tag's Values diff --git a/src/sentry/api/endpoints/group_tags.py b/src/sentry/api/endpoints/group_tags.py index d88d13847cfb6e..7d80450560ab4f 100644 --- a/src/sentry/api/endpoints/group_tags.py +++ b/src/sentry/api/endpoints/group_tags.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from sentry import tagstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.group import GroupEndpoint from sentry.api.helpers.environments import get_environments @@ -19,6 +20,10 @@ @region_silo_endpoint class GroupTagsEndpoint(GroupEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, group: Group) -> Response: # optional queryparam `key` can be used to get results diff --git a/src/sentry/api/endpoints/group_tombstone.py b/src/sentry/api/endpoints/group_tombstone.py index 81b6f59544621e..f806b59c30f929 100644 --- a/src/sentry/api/endpoints/group_tombstone.py +++ b/src/sentry/api/endpoints/group_tombstone.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.paginator import OffsetPaginator @@ -10,6 +11,10 @@ @region_silo_endpoint class GroupTombstoneEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: """ Retrieve a Project's GroupTombstones diff --git a/src/sentry/api/endpoints/group_tombstone_details.py b/src/sentry/api/endpoints/group_tombstone_details.py index 195d58713b7306..33a38f521e3f1c 100644 --- a/src/sentry/api/endpoints/group_tombstone_details.py +++ b/src/sentry/api/endpoints/group_tombstone_details.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -9,6 +10,10 @@ @region_silo_endpoint class GroupTombstoneDetailsEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + } + def delete(self, request: Request, project, tombstone_id) -> Response: """ Remove a GroupTombstone diff --git a/src/sentry/api/endpoints/group_user_reports.py b/src/sentry/api/endpoints/group_user_reports.py index dbec5fdea6342b..f74338dac27b32 100644 --- a/src/sentry/api/endpoints/group_user_reports.py +++ b/src/sentry/api/endpoints/group_user_reports.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.group import GroupEndpoint from sentry.api.paginator import DateTimePaginator @@ -10,6 +11,10 @@ @region_silo_endpoint class GroupUserReportsEndpoint(GroupEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, group) -> Response: """ List User Reports diff --git a/src/sentry/api/endpoints/grouping_configs.py b/src/sentry/api/endpoints/grouping_configs.py index db1ff5ffae78f3..8d9257034c26e9 100644 --- a/src/sentry/api/endpoints/grouping_configs.py +++ b/src/sentry/api/endpoints/grouping_configs.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.serializers import serialize from sentry.grouping.strategies.configurations import CONFIGURATIONS @@ -8,6 +9,9 @@ @region_silo_endpoint class GroupingConfigsEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = () def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/index.py b/src/sentry/api/endpoints/index.py index 30e6c5b213ef9f..60a6f8918e127d 100644 --- a/src/sentry/api/endpoints/index.py +++ b/src/sentry/api/endpoints/index.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.api.serializers import serialize from sentry.models.user import User @@ -8,6 +9,9 @@ @control_silo_endpoint class IndexEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = () def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/integration_features.py b/src/sentry/api/endpoints/integration_features.py index 310aa738025e94..cafbb39686b9f2 100644 --- a/src/sentry/api/endpoints/integration_features.py +++ b/src/sentry/api/endpoints/integration_features.py @@ -5,6 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.api.bases.integration import PARANOID_GET from sentry.api.permissions import SentryPermission @@ -21,6 +22,9 @@ class IntegrationFeaturesPermissions(SentryPermission): @control_silo_endpoint class IntegrationFeaturesEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (IntegrationFeaturesPermissions,) def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: diff --git a/src/sentry/api/endpoints/integrations/doc_integrations/details.py b/src/sentry/api/endpoints/integrations/doc_integrations/details.py index f4a9d2b0ece286..c0b8f851bc24c1 100644 --- a/src/sentry/api/endpoints/integrations/doc_integrations/details.py +++ b/src/sentry/api/endpoints/integrations/doc_integrations/details.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.doc_integrations import DocIntegrationBaseEndpoint from sentry.api.serializers import serialize @@ -16,6 +17,12 @@ @control_silo_endpoint class DocIntegrationDetailsEndpoint(DocIntegrationBaseEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, doc_integration: DocIntegration) -> Response: return self.respond(serialize(doc_integration, request.user), status=status.HTTP_200_OK) diff --git a/src/sentry/api/endpoints/integrations/doc_integrations/index.py b/src/sentry/api/endpoints/integrations/doc_integrations/index.py index 6befdb4748e811..faec4bdc874f05 100644 --- a/src/sentry/api/endpoints/integrations/doc_integrations/index.py +++ b/src/sentry/api/endpoints/integrations/doc_integrations/index.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.doc_integrations import DocIntegrationsBaseEndpoint from sentry.api.paginator import OffsetPaginator @@ -17,6 +18,11 @@ @control_silo_endpoint class DocIntegrationsEndpoint(DocIntegrationsBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request): if is_active_superuser(request): queryset = DocIntegration.objects.all() diff --git a/src/sentry/api/endpoints/integrations/index.py b/src/sentry/api/endpoints/integrations/index.py index a514f9296afe3b..612b3c0b2ad375 100644 --- a/src/sentry/api/endpoints/integrations/index.py +++ b/src/sentry/api/endpoints/integrations/index.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import features, integrations +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.serializers import serialize @@ -10,6 +11,10 @@ @region_silo_endpoint class OrganizationConfigIntegrationsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: def is_provider_enabled(provider): if not provider.requires_feature_flag: diff --git a/src/sentry/api/endpoints/integrations/install_request.py b/src/sentry/api/endpoints/integrations/install_request.py index fc1a7feb12ec13..0fea03cb3ce967 100644 --- a/src/sentry/api/endpoints/integrations/install_request.py +++ b/src/sentry/api/endpoints/integrations/install_request.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import integrations +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization_request_change import OrganizationRequestChangeEndpoint from sentry.models import SentryApp @@ -41,6 +42,10 @@ def get_provider_name(provider_type: str, provider_slug: str) -> str | None: @region_silo_endpoint class OrganizationIntegrationRequestEndpoint(OrganizationRequestChangeEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } + def post(self, request: Request, organization) -> Response: """ Email the organization owners asking them to install an integration. diff --git a/src/sentry/api/endpoints/integrations/organization_integrations/details.py b/src/sentry/api/endpoints/integrations/organization_integrations/details.py index 31d6f8348753bc..b3608350c2e214 100644 --- a/src/sentry/api/endpoints/integrations/organization_integrations/details.py +++ b/src/sentry/api/endpoints/integrations/organization_integrations/details.py @@ -11,6 +11,7 @@ from rest_framework.response import Response from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.organization_integrations import OrganizationIntegrationBaseEndpoint from sentry.api.serializers import serialize @@ -30,6 +31,12 @@ class IntegrationSerializer(serializers.Serializer): @control_silo_endpoint class OrganizationIntegrationDetailsEndpoint(OrganizationIntegrationBaseEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } + @set_referrer_policy("strict-origin-when-cross-origin") @method_decorator(never_cache) def get( diff --git a/src/sentry/api/endpoints/integrations/organization_integrations/index.py b/src/sentry/api/endpoints/integrations/organization_integrations/index.py index a991419f4a563b..c0f4c667a88888 100644 --- a/src/sentry/api/endpoints/integrations/organization_integrations/index.py +++ b/src/sentry/api/endpoints/integrations/organization_integrations/index.py @@ -5,6 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationIntegrationsPermission from sentry.api.serializers import serialize @@ -49,6 +50,9 @@ def filter_by_features( @region_silo_endpoint class OrganizationIntegrationsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationIntegrationsPermission,) def get(self, request: Request, organization: Organization) -> Response: diff --git a/src/sentry/api/endpoints/integrations/plugins/configs_index.py b/src/sentry/api/endpoints/integrations/plugins/configs_index.py index 55fbf7fd72ea75..9f42ae43b797f0 100644 --- a/src/sentry/api/endpoints/integrations/plugins/configs_index.py +++ b/src/sentry/api/endpoints/integrations/plugins/configs_index.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.serializers import serialize @@ -13,6 +14,10 @@ @region_silo_endpoint class OrganizationPluginsConfigsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: """ diff --git a/src/sentry/api/endpoints/integrations/plugins/index.py b/src/sentry/api/endpoints/integrations/plugins/index.py index 21e34d983318ac..0bf82928cecd3b 100644 --- a/src/sentry/api/endpoints/integrations/plugins/index.py +++ b/src/sentry/api/endpoints/integrations/plugins/index.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.serializers import serialize @@ -12,6 +13,10 @@ @region_silo_endpoint class OrganizationPluginsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: all_plugins = {p.slug: p for p in plugins.all()} diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/authorizations.py b/src/sentry/api/endpoints/integrations/sentry_apps/authorizations.py index 17b127d03b67b6..f34cfe4a61f25f 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/authorizations.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/authorizations.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases import SentryAppAuthorizationsBaseEndpoint from sentry.api.serializers.models.apitoken import ApiTokenSerializer @@ -15,6 +16,10 @@ @control_silo_endpoint class SentryAppAuthorizationsEndpoint(SentryAppAuthorizationsBaseEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } + def post(self, request: Request, installation) -> Response: with sentry_sdk.configure_scope() as scope: scope.set_tag("organization", installation.organization_id) diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/components.py b/src/sentry/api/endpoints/integrations/sentry_apps/components.py index b685c6d8752993..d021696b3dbfdb 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/components.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/components.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases import SentryAppBaseEndpoint, add_integration_platform_metric_tag from sentry.api.bases.organization import ControlSiloOrganizationEndpoint @@ -20,6 +21,10 @@ # endpoint that can take project_id or sentry_app_id as a query parameter. @control_silo_endpoint class SentryAppComponentsEndpoint(SentryAppBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, sentry_app) -> Response: return self.paginate( request=request, @@ -31,6 +36,10 @@ def get(self, request: Request, sentry_app) -> Response: @control_silo_endpoint class OrganizationSentryAppComponentsEndpoint(ControlSiloOrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + @add_integration_platform_metric_tag def get( self, diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/details.py b/src/sentry/api/endpoints/integrations/sentry_apps/details.py index 31787d81c906d0..817427c61e8ad1 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/details.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/details.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import analytics, audit_log, deletions, features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.sentryapps import SentryAppBaseEndpoint, catch_raised_errors from sentry.api.serializers import serialize @@ -25,6 +26,12 @@ @control_silo_endpoint class SentryAppDetailsEndpoint(SentryAppBaseEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, sentry_app) -> Response: return Response(serialize(sentry_app, request.user, access=request.access)) diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/features.py b/src/sentry/api/endpoints/integrations/sentry_apps/features.py index 91800c911e9ca5..508ae7a72bf1ea 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/features.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/features.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.sentryapps import SentryAppBaseEndpoint from sentry.api.paginator import OffsetPaginator @@ -11,6 +12,10 @@ @control_silo_endpoint class SentryAppFeaturesEndpoint(SentryAppBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, sentry_app) -> Response: features = IntegrationFeature.objects.filter( target_id=sentry_app.id, target_type=IntegrationTypes.SENTRY_APP.value diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/index.py b/src/sentry/api/endpoints/integrations/sentry_apps/index.py index d5133dd2d32ecd..dfb233f4f3e31a 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/index.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/index.py @@ -6,6 +6,7 @@ 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 control_silo_endpoint from sentry.api.bases import SentryAppsBaseEndpoint from sentry.api.paginator import OffsetPaginator @@ -23,6 +24,10 @@ @control_silo_endpoint class SentryAppsEndpoint(SentryAppsBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ISSUES def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/installation/details.py b/src/sentry/api/endpoints/integrations/sentry_apps/installation/details.py index e2ef38f588fa2f..8b32f93e90bfd7 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/installation/details.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/installation/details.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import analytics, audit_log, deletions +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases import SentryAppInstallationBaseEndpoint from sentry.api.serializers import serialize @@ -20,6 +21,12 @@ @control_silo_endpoint class SentryAppInstallationDetailsEndpoint(SentryAppInstallationBaseEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, installation) -> Response: return Response(serialize(SentryAppInstallation.objects.get(id=installation.id))) diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/actions.py b/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/actions.py index 7235d3cb485643..39adad41eb87ff 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/actions.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/actions.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import SentryAppInstallationBaseEndpoint from sentry.api.serializers import serialize @@ -13,6 +14,10 @@ @region_silo_endpoint class SentryAppInstallationExternalIssueActionsEndpoint(SentryAppInstallationBaseEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } + def post(self, request: Request, installation) -> Response: data = request.data.copy() diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/details.py b/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/details.py index 6f3b0921777cf1..45860e0631c2c2 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/details.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/details.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import deletions +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import ( SentryAppInstallationExternalIssueBaseEndpoint as ExternalIssueBaseEndpoint, @@ -11,6 +12,10 @@ @region_silo_endpoint class SentryAppInstallationExternalIssueDetailsEndpoint(ExternalIssueBaseEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + } + def delete(self, request: Request, installation, external_issue_id) -> Response: try: platform_external_issue = PlatformExternalIssue.objects.get( diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/index.py b/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/index.py index f5fdb783702cb1..d287ef10e84296 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/index.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_issue/index.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import ( SentryAppInstallationExternalIssueBaseEndpoint as ExternalIssueBaseEndpoint, @@ -20,6 +21,10 @@ class PlatformExternalIssueSerializer(serializers.Serializer): @region_silo_endpoint class SentryAppInstallationExternalIssuesEndpoint(ExternalIssueBaseEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } + def post(self, request: Request, installation) -> Response: data = request.data diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_requests.py b/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_requests.py index 7ad401fb2c2368..72648e9f65a6f7 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_requests.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/installation/external_requests.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import SentryAppInstallationBaseEndpoint from sentry.mediators import external_requests @@ -9,6 +10,10 @@ @region_silo_endpoint class SentryAppInstallationExternalRequestsEndpoint(SentryAppInstallationBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, installation) -> Response: try: project = Project.objects.get( diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/installation/index.py b/src/sentry/api/endpoints/integrations/sentry_apps/installation/index.py index 2b0d2282e63f90..36739143a3d686 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/installation/index.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/installation/index.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases import SentryAppInstallationsBaseEndpoint from sentry.api.paginator import OffsetPaginator @@ -32,6 +33,11 @@ def validate(self, attrs): @control_silo_endpoint class SentryAppInstallationsEndpoint(SentryAppInstallationsBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: queryset = SentryAppInstallation.objects.filter(organization_id=organization.id) diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/interaction.py b/src/sentry/api/endpoints/integrations/sentry_apps/interaction.py index df11aea4ab1a14..98cc7d76978818 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/interaction.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/interaction.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import tsdb +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import StatsMixin, region_silo_endpoint from sentry.api.bases import RegionSentryAppBaseEndpoint, SentryAppStatsPermission from sentry.api.bases.sentryapps import COMPONENT_TYPES @@ -21,6 +22,10 @@ def get_component_interaction_key(sentry_app, component_type): @region_silo_endpoint class SentryAppInteractionEndpoint(RegionSentryAppBaseEndpoint, StatsMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (SentryAppStatsPermission,) def get(self, request: Request, sentry_app) -> Response: diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/internal_app_token/details.py b/src/sentry/api/endpoints/integrations/sentry_apps/internal_app_token/details.py index b5f95335f16af0..6216b95b040eff 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/internal_app_token/details.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/internal_app_token/details.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import analytics, deletions +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases import SentryAppBaseEndpoint, SentryInternalAppTokenPermission from sentry.models import ApiToken, SentryAppInstallationToken @@ -12,6 +13,9 @@ @control_silo_endpoint class SentryInternalAppTokenDetailsEndpoint(SentryAppBaseEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + } permission_classes = (SentryInternalAppTokenPermission,) def convert_args(self, request: Request, sentry_app_slug, api_token, *args, **kwargs): diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/internal_app_token/index.py b/src/sentry/api/endpoints/integrations/sentry_apps/internal_app_token/index.py index aa6dd84b7437c5..279be83d63bad2 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/internal_app_token/index.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/internal_app_token/index.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases import SentryAppBaseEndpoint, SentryInternalAppTokenPermission from sentry.api.serializers.models.apitoken import ApiTokenSerializer @@ -13,6 +14,10 @@ @control_silo_endpoint class SentryInternalAppTokensEndpoint(SentryAppBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (SentryInternalAppTokenPermission,) def get(self, request: Request, sentry_app) -> Response: diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/organization_sentry_apps.py b/src/sentry/api/endpoints/integrations/sentry_apps/organization_sentry_apps.py index 244f67d430ce85..1e4e0403da75b9 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/organization_sentry_apps.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/organization_sentry_apps.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationEndpoint, add_integration_platform_metric_tag from sentry.api.paginator import OffsetPaginator @@ -11,6 +12,10 @@ @region_silo_endpoint class OrganizationSentryAppsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + @add_integration_platform_metric_tag def get(self, request: Request, organization) -> Response: queryset = SentryApp.objects.filter(owner_id=organization.id, application__isnull=False) diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/publish_request.py b/src/sentry/api/endpoints/integrations/sentry_apps/publish_request.py index 300ed598dc5c65..abcbf608537dce 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/publish_request.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/publish_request.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import options +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.sentryapps import COMPONENT_TYPES, SentryAppBaseEndpoint from sentry.constants import SentryAppStatus @@ -14,6 +15,10 @@ @control_silo_endpoint class SentryAppPublishRequestEndpoint(SentryAppBaseEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } + def has_ui_component(self, sentry_app): """Determine if the sentry app supports issue linking or stack trace linking.""" elements = (sentry_app.schema or {}).get("elements", []) diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/requests.py b/src/sentry/api/endpoints/integrations/sentry_apps/requests.py index e9f56f0ddaa94e..8022eff43b063d 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/requests.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/requests.py @@ -5,6 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import RegionSentryAppBaseEndpoint, SentryAppStatsPermission from sentry.api.serializers import serialize @@ -40,6 +41,9 @@ def __hash__(self): @region_silo_endpoint class SentryAppRequestsEndpoint(RegionSentryAppBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (SentryAppStatsPermission,) def get(self, request: Request, sentry_app) -> Response: diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/stats/details.py b/src/sentry/api/endpoints/integrations/sentry_apps/stats/details.py index d64f66ffc4db42..c061c4b623451a 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/stats/details.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/stats/details.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import tsdb +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import StatsMixin, control_silo_endpoint from sentry.api.bases import SentryAppBaseEndpoint, SentryAppStatsPermission from sentry.models import SentryAppInstallation @@ -9,6 +10,9 @@ @control_silo_endpoint class SentryAppStatsEndpoint(SentryAppBaseEndpoint, StatsMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (SentryAppStatsPermission,) def get(self, request: Request, sentry_app) -> Response: diff --git a/src/sentry/api/endpoints/integrations/sentry_apps/stats/index.py b/src/sentry/api/endpoints/integrations/sentry_apps/stats/index.py index c97111e8bdff14..72af417b4952ca 100644 --- a/src/sentry/api/endpoints/integrations/sentry_apps/stats/index.py +++ b/src/sentry/api/endpoints/integrations/sentry_apps/stats/index.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases import SentryAppsBaseEndpoint from sentry.api.permissions import SuperuserPermission @@ -11,6 +12,9 @@ @control_silo_endpoint class SentryAppsStatsEndpoint(SentryAppsBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/internal/beacon.py b/src/sentry/api/endpoints/internal/beacon.py index c662b4115ddad7..291c6729840aa2 100644 --- a/src/sentry/api/endpoints/internal/beacon.py +++ b/src/sentry/api/endpoints/internal/beacon.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.tasks.beacon import send_beacon_metric @@ -38,6 +39,9 @@ def validate(self, attrs): @control_silo_endpoint class InternalBeaconEndpoint(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = () def post(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/internal/environment.py b/src/sentry/api/endpoints/internal/environment.py index db76aa2ec69b54..e0de8404eeeccf 100644 --- a/src/sentry/api/endpoints/internal/environment.py +++ b/src/sentry/api/endpoints/internal/environment.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, all_silo_endpoint from sentry.api.permissions import SuperuserPermission from sentry.app import env @@ -11,6 +12,9 @@ @all_silo_endpoint class InternalEnvironmentEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/internal/mail.py b/src/sentry/api/endpoints/internal/mail.py index b8f06669fb7deb..9c254474bf9489 100644 --- a/src/sentry/api/endpoints/internal/mail.py +++ b/src/sentry/api/endpoints/internal/mail.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import options +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, all_silo_endpoint from sentry.api.permissions import SuperuserPermission from sentry.utils.email import send_mail @@ -9,6 +10,10 @@ @all_silo_endpoint class InternalMailEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/internal/packages.py b/src/sentry/api/endpoints/internal/packages.py index 23d7b61e4143af..72e05eac7064c1 100644 --- a/src/sentry/api/endpoints/internal/packages.py +++ b/src/sentry/api/endpoints/internal/packages.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, all_silo_endpoint from sentry.api.permissions import SuperuserPermission from sentry.plugins.base import plugins @@ -9,6 +10,9 @@ @all_silo_endpoint class InternalPackagesEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/internal/queue_tasks.py b/src/sentry/api/endpoints/internal/queue_tasks.py index 3bc10855e76198..be7f1dd7209ba8 100644 --- a/src/sentry/api/endpoints/internal/queue_tasks.py +++ b/src/sentry/api/endpoints/internal/queue_tasks.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, all_silo_endpoint from sentry.api.permissions import SuperuserPermission from sentry.celery import app @@ -8,6 +9,9 @@ @all_silo_endpoint class InternalQueueTasksEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/internal/quotas.py b/src/sentry/api/endpoints/internal/quotas.py index 9f3aded91a6b10..ce1a0d88edc1cc 100644 --- a/src/sentry/api/endpoints/internal/quotas.py +++ b/src/sentry/api/endpoints/internal/quotas.py @@ -3,12 +3,16 @@ from rest_framework.response import Response from sentry import options +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, all_silo_endpoint from sentry.api.permissions import SuperuserPermission @all_silo_endpoint class InternalQuotasEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/internal/stats.py b/src/sentry/api/endpoints/internal/stats.py index 2b681840c2d7ef..8b37fd65854d7f 100644 --- a/src/sentry/api/endpoints/internal/stats.py +++ b/src/sentry/api/endpoints/internal/stats.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import tsdb +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, StatsMixin, region_silo_endpoint from sentry.api.permissions import SuperuserPermission from sentry.tsdb.base import TSDBModel @@ -9,6 +10,9 @@ @region_silo_endpoint class InternalStatsEndpoint(Endpoint, StatsMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/internal/warnings.py b/src/sentry/api/endpoints/internal/warnings.py index a82b8821a45933..391e1b5b009389 100644 --- a/src/sentry/api/endpoints/internal/warnings.py +++ b/src/sentry/api/endpoints/internal/warnings.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, all_silo_endpoint from sentry.api.permissions import SuperuserPermission from sentry.utils.warnings import DeprecatedSettingWarning, UnsupportedBackend, seen_warnings @@ -11,6 +12,9 @@ @all_silo_endpoint class InternalWarningsEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/issue_occurrence.py b/src/sentry/api/endpoints/issue_occurrence.py index f7d10db69d2782..3448c42ee63dd4 100644 --- a/src/sentry/api/endpoints/issue_occurrence.py +++ b/src/sentry/api/endpoints/issue_occurrence.py @@ -7,6 +7,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.permissions import SuperuserPermission from sentry.models import Project @@ -18,6 +19,9 @@ @region_silo_endpoint class IssueOccurrenceEndpoint(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def post(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/notification_defaults.py b/src/sentry/api/endpoints/notification_defaults.py index ee3736b42df0aa..2742f08d1d42d9 100644 --- a/src/sentry/api/endpoints/notification_defaults.py +++ b/src/sentry/api/endpoints/notification_defaults.py @@ -2,6 +2,7 @@ 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.notifications.defaults import ( NOTIFICATION_SETTING_DEFAULTS, @@ -44,6 +45,9 @@ def get_type_defaults(): @control_silo_endpoint class NotificationDefaultsEndpoints(Endpoint): + publish_status = { + "GET": ApiPublishStatus.PRIVATE, + } owner = ApiOwner.ISSUES permission_classes = () private = True diff --git a/src/sentry/api/endpoints/notifications/notification_actions_available.py b/src/sentry/api/endpoints/notifications/notification_actions_available.py index 1445456b6c03f7..1dfa1f53d40a09 100644 --- a/src/sentry/api/endpoints/notifications/notification_actions_available.py +++ b/src/sentry/api/endpoints/notifications/notification_actions_available.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.constants import ObjectStatus @@ -11,6 +12,10 @@ @region_silo_endpoint class NotificationActionsAvailableEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization: Organization) -> Response: """ Responds with a payload serialized directly from running the 'serialize_available' methods diff --git a/src/sentry/api/endpoints/notifications/notification_actions_details.py b/src/sentry/api/endpoints/notifications/notification_actions_details.py index f791be2cad539b..5ebac4b9640e59 100644 --- a/src/sentry/api/endpoints/notifications/notification_actions_details.py +++ b/src/sentry/api/endpoints/notifications/notification_actions_details.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.endpoints.notifications.notification_actions_index import ( @@ -23,6 +24,11 @@ @region_silo_endpoint class NotificationActionsDetailsEndpoint(OrganizationEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } """ Manages a single NotificationAction via the action_id passed in the path. GET: Returns the serialized NotificationAction diff --git a/src/sentry/api/endpoints/notifications/notification_actions_index.py b/src/sentry/api/endpoints/notifications/notification_actions_index.py index f515af4c87fca5..949bbb6bb2d06f 100644 --- a/src/sentry/api/endpoints/notifications/notification_actions_index.py +++ b/src/sentry/api/endpoints/notifications/notification_actions_index.py @@ -8,6 +8,7 @@ from rest_framework.response import Response from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.paginator import OffsetPaginator @@ -42,6 +43,10 @@ class NotificationActionsPermission(OrganizationPermission): @region_silo_endpoint class NotificationActionsIndexEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } """ View existing NotificationActions or create a new one. GET: Returns paginated, serialized NotificationActions for an organization diff --git a/src/sentry/api/endpoints/oauth_userinfo.py b/src/sentry/api/endpoints/oauth_userinfo.py index 56d44ba9791096..64ab98f8b78973 100644 --- a/src/sentry/api/endpoints/oauth_userinfo.py +++ b/src/sentry/api/endpoints/oauth_userinfo.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.api.exceptions import ParameterValidationError, ResourceDoesNotExist, SentryAPIException from sentry.models import ApiToken, UserEmail @@ -16,6 +17,9 @@ class InsufficientScopesError(SentryAPIException): @control_silo_endpoint class OAuthUserInfoEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } authentication_classes = () permission_classes = () diff --git a/src/sentry/api/endpoints/org_auth_token_details.py b/src/sentry/api/endpoints/org_auth_token_details.py index d4aeca464cf480..b280c8cd7954a9 100644 --- a/src/sentry/api/endpoints/org_auth_token_details.py +++ b/src/sentry/api/endpoints/org_auth_token_details.py @@ -4,6 +4,7 @@ from sentry import analytics, audit_log from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.organization import ControlSiloOrganizationEndpoint, OrgAuthTokenPermission from sentry.api.exceptions import ResourceDoesNotExist @@ -17,6 +18,11 @@ @control_silo_endpoint class OrgAuthTokenDetailsEndpoint(ControlSiloOrganizationEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE permission_classes = (OrgAuthTokenPermission,) diff --git a/src/sentry/api/endpoints/org_auth_tokens.py b/src/sentry/api/endpoints/org_auth_tokens.py index 7c58d1a0ad289d..3a4a4a37529ea4 100644 --- a/src/sentry/api/endpoints/org_auth_tokens.py +++ b/src/sentry/api/endpoints/org_auth_tokens.py @@ -11,6 +11,7 @@ from sentry import analytics, audit_log, roles from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.organization import ControlSiloOrganizationEndpoint, OrgAuthTokenPermission from sentry.api.serializers import serialize @@ -28,6 +29,10 @@ @control_silo_endpoint class OrgAuthTokensEndpoint(ControlSiloOrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE permission_classes = (OrgAuthTokenPermission,) diff --git a/src/sentry/api/endpoints/organization_access_request_details.py b/src/sentry/api/endpoints/organization_access_request_details.py index 93ecead95cfd53..49d87634d9e031 100644 --- a/src/sentry/api/endpoints/organization_access_request_details.py +++ b/src/sentry/api/endpoints/organization_access_request_details.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.exceptions import ResourceDoesNotExist @@ -43,6 +44,10 @@ class AccessRequestSerializer(serializers.Serializer): @region_silo_endpoint class OrganizationAccessRequestDetailsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = [AccessRequestPermission] # TODO(dcramer): this should go onto AccessRequestPermission diff --git a/src/sentry/api/endpoints/organization_activity.py b/src/sentry/api/endpoints/organization_activity.py index d56bddd254b2ec..a405982fd95dcf 100644 --- a/src/sentry/api/endpoints/organization_activity.py +++ b/src/sentry/api/endpoints/organization_activity.py @@ -2,6 +2,7 @@ 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 EnvironmentMixin, region_silo_endpoint from sentry.api.bases import OrganizationMemberEndpoint from sentry.api.paginator import DateTimePaginator @@ -12,6 +13,9 @@ @region_silo_endpoint class OrganizationActivityEndpoint(OrganizationMemberEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ISSUES def get(self, request: Request, organization, member) -> Response: diff --git a/src/sentry/api/endpoints/organization_api_key_details.py b/src/sentry/api/endpoints/organization_api_key_details.py index b58b2beaaef466..bdd6cfcb492c8a 100644 --- a/src/sentry/api/endpoints/organization_api_key_details.py +++ b/src/sentry/api/endpoints/organization_api_key_details.py @@ -3,6 +3,7 @@ from rest_framework.response import Response from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.organization import ( ControlSiloOrganizationEndpoint, @@ -21,6 +22,11 @@ class Meta: @control_silo_endpoint class OrganizationApiKeyDetailsEndpoint(ControlSiloOrganizationEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationAdminPermission,) def get(self, request: Request, organization_context, organization, api_key_id) -> Response: diff --git a/src/sentry/api/endpoints/organization_api_key_index.py b/src/sentry/api/endpoints/organization_api_key_index.py index ad9442c37b4334..cb606f2e46c00e 100644 --- a/src/sentry/api/endpoints/organization_api_key_index.py +++ b/src/sentry/api/endpoints/organization_api_key_index.py @@ -3,6 +3,7 @@ from rest_framework.response import Response from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.organization import ( ControlSiloOrganizationEndpoint, @@ -16,6 +17,10 @@ @control_silo_endpoint class OrganizationApiKeyIndexEndpoint(ControlSiloOrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationAdminPermission,) def get(self, request: Request, organization_context, organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_artifactbundle_assemble.py b/src/sentry/api/endpoints/organization_artifactbundle_assemble.py index 3c42dad7e83c59..7f8e23f5eaf632 100644 --- a/src/sentry/api/endpoints/organization_artifactbundle_assemble.py +++ b/src/sentry/api/endpoints/organization_artifactbundle_assemble.py @@ -3,6 +3,7 @@ from rest_framework.response import Response from sentry import analytics, options +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -22,6 +23,10 @@ @region_silo_endpoint class OrganizationArtifactBundleAssembleEndpoint(OrganizationReleasesBaseEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } + def post(self, request: Request, organization) -> Response: """ Assembles an artifact bundle and stores the debug ids in the database. diff --git a/src/sentry/api/endpoints/organization_auditlogs.py b/src/sentry/api/endpoints/organization_auditlogs.py index 980e9e247d12d2..0d483619767814 100644 --- a/src/sentry/api/endpoints/organization_auditlogs.py +++ b/src/sentry/api/endpoints/organization_auditlogs.py @@ -4,6 +4,7 @@ from sentry import audit_log from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases import ControlSiloOrganizationEndpoint from sentry.api.bases.organization import OrganizationAuditPermission @@ -32,6 +33,9 @@ def validate_event(self, event): @control_silo_endpoint class OrganizationAuditLogsEndpoint(ControlSiloOrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE permission_classes = (OrganizationAuditPermission,) diff --git a/src/sentry/api/endpoints/organization_auth_provider_details.py b/src/sentry/api/endpoints/organization_auth_provider_details.py index 89a6f77e7e604c..e92d98129da0d4 100644 --- a/src/sentry/api/endpoints/organization_auth_provider_details.py +++ b/src/sentry/api/endpoints/organization_auth_provider_details.py @@ -3,6 +3,7 @@ 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 region_silo_endpoint from sentry.api.bases.organization import OrganizationAuthProviderPermission, OrganizationEndpoint from sentry.api.serializers import serialize @@ -13,6 +14,9 @@ @region_silo_endpoint class OrganizationAuthProviderDetailsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE permission_classes = (OrganizationAuthProviderPermission,) @@ -25,13 +29,12 @@ def get(self, request: Request, organization: Organization) -> Response: :pparam string organization_slug: the organization short name :auth: required """ - auth_providers = auth_service.get_auth_providers(organization_id=organization.id) - if not auth_providers: + auth_provider = auth_service.get_auth_provider(organization_id=organization.id) + if not auth_provider: # This is a valid state where org does not have an auth provider # configured, make sure we respond with a 20x return Response(status=status.HTTP_204_NO_CONTENT) - auth_provider = auth_providers[0] return Response( serialize( auth_provider, diff --git a/src/sentry/api/endpoints/organization_auth_provider_send_reminders.py b/src/sentry/api/endpoints/organization_auth_provider_send_reminders.py index 30481cddc67561..90416b6cd4445f 100644 --- a/src/sentry/api/endpoints/organization_auth_provider_send_reminders.py +++ b/src/sentry/api/endpoints/organization_auth_provider_send_reminders.py @@ -4,6 +4,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationAdminPermission, OrganizationEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -15,6 +16,9 @@ @region_silo_endpoint class OrganizationAuthProviderSendRemindersEndpoint(OrganizationEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE permission_classes = (OrganizationAdminPermission,) diff --git a/src/sentry/api/endpoints/organization_auth_providers.py b/src/sentry/api/endpoints/organization_auth_providers.py index 4a4686c7119ad6..9497b38cd20a98 100644 --- a/src/sentry/api/endpoints/organization_auth_providers.py +++ b/src/sentry/api/endpoints/organization_auth_providers.py @@ -2,6 +2,7 @@ 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 region_silo_endpoint from sentry.api.bases.organization import OrganizationAuthProviderPermission, OrganizationEndpoint from sentry.api.serializers import serialize @@ -10,6 +11,9 @@ @region_silo_endpoint class OrganizationAuthProvidersEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE permission_classes = (OrganizationAuthProviderPermission,) diff --git a/src/sentry/api/endpoints/organization_code_mapping_codeowners.py b/src/sentry/api/endpoints/organization_code_mapping_codeowners.py index 49b77070fffe86..6a67dabfa92479 100644 --- a/src/sentry/api/endpoints/organization_code_mapping_codeowners.py +++ b/src/sentry/api/endpoints/organization_code_mapping_codeowners.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationIntegrationsPermission from sentry.models import RepositoryProjectPathConfig @@ -22,6 +23,9 @@ def get_codeowner_contents(config): @region_silo_endpoint class OrganizationCodeMappingCodeOwnersEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationIntegrationsPermission,) def convert_args(self, request: Request, organization_slug, config_id, *args, **kwargs): diff --git a/src/sentry/api/endpoints/organization_code_mapping_details.py b/src/sentry/api/endpoints/organization_code_mapping_details.py index 2913f542376717..adce07f37ba7a3 100644 --- a/src/sentry/api/endpoints/organization_code_mapping_details.py +++ b/src/sentry/api/endpoints/organization_code_mapping_details.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import ( OrganizationEndpoint, @@ -21,6 +22,10 @@ @region_silo_endpoint class OrganizationCodeMappingDetailsEndpoint(OrganizationEndpoint, OrganizationIntegrationMixin): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationIntegrationsLoosePermission,) def convert_args(self, request: Request, organization_slug, config_id, *args, **kwargs): diff --git a/src/sentry/api/endpoints/organization_code_mappings.py b/src/sentry/api/endpoints/organization_code_mappings.py index 1f0faf5c66ca2f..e58c93c9078dc9 100644 --- a/src/sentry/api/endpoints/organization_code_mappings.py +++ b/src/sentry/api/endpoints/organization_code_mappings.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import ( OrganizationEndpoint, @@ -126,6 +127,10 @@ def get_project(self, organization, project_id): @region_silo_endpoint class OrganizationCodeMappingsEndpoint(OrganizationEndpoint, OrganizationIntegrationMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationIntegrationsLoosePermission,) def get(self, request: Request, organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_codeowners_associations.py b/src/sentry/api/endpoints/organization_codeowners_associations.py index 9bc8631e72bfb5..6cb17c236e4223 100644 --- a/src/sentry/api/endpoints/organization_codeowners_associations.py +++ b/src/sentry/api/endpoints/organization_codeowners_associations.py @@ -1,6 +1,7 @@ from rest_framework import status from rest_framework.request import Request +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import ( OrganizationEndpoint, @@ -14,6 +15,9 @@ @region_silo_endpoint class OrganizationCodeOwnersAssociationsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationIntegrationsLoosePermission,) def get(self, request: Request, organization: Organization): diff --git a/src/sentry/api/endpoints/organization_config_repositories.py b/src/sentry/api/endpoints/organization_config_repositories.py index 95f4e3bcd7d2c3..fd625e6f27e133 100644 --- a/src/sentry/api/endpoints/organization_config_repositories.py +++ b/src/sentry/api/endpoints/organization_config_repositories.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.plugins.base import bindings @@ -8,6 +9,10 @@ @region_silo_endpoint class OrganizationConfigRepositoriesEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: provider_bindings = bindings.get("repository.provider") providers = [] diff --git a/src/sentry/api/endpoints/organization_dashboard_details.py b/src/sentry/api/endpoints/organization_dashboard_details.py index 951416aa035d96..8d55470686b98a 100644 --- a/src/sentry/api/endpoints/organization_dashboard_details.py +++ b/src/sentry/api/endpoints/organization_dashboard_details.py @@ -7,6 +7,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.endpoints.organization_dashboards import OrganizationDashboardsPermission @@ -43,6 +44,12 @@ def _get_dashboard(self, request: Request, organization, dashboard_id): @region_silo_endpoint class OrganizationDashboardDetailsEndpoint(OrganizationDashboardBase): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, dashboard) -> Response: """ Retrieve an Organization's Dashboard @@ -141,6 +148,10 @@ def put(self, request: Request, organization, dashboard) -> Response: @region_silo_endpoint class OrganizationDashboardVisitEndpoint(OrganizationDashboardBase): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } + def post(self, request: Request, organization, dashboard) -> Response: """ Update last_visited and increment visits counter diff --git a/src/sentry/api/endpoints/organization_dashboard_widget_details.py b/src/sentry/api/endpoints/organization_dashboard_widget_details.py index b804fed7758d90..71d702fa412927 100644 --- a/src/sentry/api/endpoints/organization_dashboard_widget_details.py +++ b/src/sentry/api/endpoints/organization_dashboard_widget_details.py @@ -3,6 +3,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationEndpoint from sentry.api.endpoints.organization_dashboards import OrganizationDashboardsPermission @@ -11,6 +12,9 @@ @region_silo_endpoint class OrganizationDashboardWidgetDetailsEndpoint(OrganizationEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.DISCOVER_N_DASHBOARDS permission_classes = (OrganizationDashboardsPermission,) diff --git a/src/sentry/api/endpoints/organization_dashboards.py b/src/sentry/api/endpoints/organization_dashboards.py index ce661040745745..31782edf189f68 100644 --- a/src/sentry/api/endpoints/organization_dashboards.py +++ b/src/sentry/api/endpoints/organization_dashboards.py @@ -7,6 +7,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.paginator import ChainPaginator @@ -30,6 +31,10 @@ class OrganizationDashboardsPermission(OrganizationPermission): @region_silo_endpoint class OrganizationDashboardsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.DISCOVER_N_DASHBOARDS permission_classes = (OrganizationDashboardsPermission,) diff --git a/src/sentry/api/endpoints/organization_derive_code_mappings.py b/src/sentry/api/endpoints/organization_derive_code_mappings.py index 1a2888d0842acc..957f081798f2d4 100644 --- a/src/sentry/api/endpoints/organization_derive_code_mappings.py +++ b/src/sentry/api/endpoints/organization_derive_code_mappings.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import ( OrganizationEndpoint, @@ -25,6 +26,10 @@ @region_silo_endpoint class OrganizationDeriveCodeMappingsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationIntegrationsLoosePermission,) def get(self, request: Request, organization: Organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_details.py b/src/sentry/api/endpoints/organization_details.py index 55534a2333a52f..167207f0810f23 100644 --- a/src/sentry/api/endpoints/organization_details.py +++ b/src/sentry/api/endpoints/organization_details.py @@ -11,6 +11,7 @@ from bitfield.types import BitHandler from sentry import audit_log, roles +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import ONE_DAY, region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.decorators import sudo_required @@ -202,8 +203,8 @@ def _has_legacy_rate_limits(self): def _has_sso_enabled(self): org = self.context["organization"] - org_auth_providers = auth_service.get_auth_providers(organization_id=org.id) - return len(org_auth_providers) > 0 + org_auth_provider = auth_service.get_auth_provider(organization_id=org.id) + return org_auth_provider is not None def validate_relayPiiConfig(self, value): organization = self.context["organization"] @@ -476,6 +477,12 @@ def save(self, *args, **kwargs): @region_silo_endpoint class OrganizationDetailsEndpoint(OrganizationEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: """ Retrieve an Organization diff --git a/src/sentry/api/endpoints/organization_environments.py b/src/sentry/api/endpoints/organization_environments.py index d2c1f0a3ff6110..a9ee0d52a087b3 100644 --- a/src/sentry/api/endpoints/organization_environments.py +++ b/src/sentry/api/endpoints/organization_environments.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationEndpoint from sentry.api.helpers.environments import environment_visibility_filter_options @@ -10,6 +11,10 @@ @region_silo_endpoint class OrganizationEnvironmentsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: visibility = request.GET.get("visibility", "visible") if visibility not in environment_visibility_filter_options: diff --git a/src/sentry/api/endpoints/organization_event_details.py b/src/sentry/api/endpoints/organization_event_details.py index f5d69641b57bda..92b35afaa91e80 100644 --- a/src/sentry/api/endpoints/organization_event_details.py +++ b/src/sentry/api/endpoints/organization_event_details.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import eventstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationEventsEndpointBase from sentry.api.serializers import serialize @@ -12,6 +13,10 @@ @region_silo_endpoint class OrganizationEventDetailsEndpoint(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, project_slug, event_id) -> Response: """event_id is validated by a regex in the URL""" if not self.has_feature(organization, request): diff --git a/src/sentry/api/endpoints/organization_eventid.py b/src/sentry/api/endpoints/organization_eventid.py index fd52793cb80ac0..e50719c7860037 100644 --- a/src/sentry/api/endpoints/organization_eventid.py +++ b/src/sentry/api/endpoints/organization_eventid.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import eventstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -13,6 +14,9 @@ @region_silo_endpoint class EventIdLookupEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } enforce_rate_limit = True rate_limits = { "GET": { diff --git a/src/sentry/api/endpoints/organization_events.py b/src/sentry/api/endpoints/organization_events.py index f6d01772934d29..3e4df5f73dd81e 100644 --- a/src/sentry/api/endpoints/organization_events.py +++ b/src/sentry/api/endpoints/organization_events.py @@ -8,6 +8,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase from sentry.api.paginator import GenericOffsetPaginator @@ -122,6 +123,9 @@ def rate_limit_events(request: Request, organization_slug=None, *args, **kwargs) @extend_schema(tags=["Discover"]) @region_silo_endpoint class OrganizationEventsEndpoint(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.PUBLIC, + } public = {"GET"} enforce_rate_limit = True diff --git a/src/sentry/api/endpoints/organization_events_facets.py b/src/sentry/api/endpoints/organization_events_facets.py index e02dc88b348546..5cd234fb207e5b 100644 --- a/src/sentry/api/endpoints/organization_events_facets.py +++ b/src/sentry/api/endpoints/organization_events_facets.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import tagstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase from sentry.api.paginator import GenericOffsetPaginator @@ -14,6 +15,10 @@ @region_silo_endpoint class OrganizationEventsFacetsEndpoint(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: if not self.has_feature(organization, request): return Response(status=404) diff --git a/src/sentry/api/endpoints/organization_events_facets_performance.py b/src/sentry/api/endpoints/organization_events_facets_performance.py index ad249149ac7121..be820e82f720d5 100644 --- a/src/sentry/api/endpoints/organization_events_facets_performance.py +++ b/src/sentry/api/endpoints/organization_events_facets_performance.py @@ -10,6 +10,7 @@ from snuba_sdk import Column, Condition, Function, Op from sentry import features, tagstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase from sentry.api.paginator import GenericOffsetPaginator @@ -33,6 +34,10 @@ class OrganizationEventsFacetsPerformanceEndpointBase(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def has_feature(self, organization, request): return features.has("organizations:performance-view", organization, actor=request.user) @@ -130,6 +135,10 @@ def data_fn(offset, limit): class OrganizationEventsFacetsPerformanceHistogramEndpoint( OrganizationEventsFacetsPerformanceEndpointBase ): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: try: params, aggregate_column, filter_query = self._setup(request, organization) diff --git a/src/sentry/api/endpoints/organization_events_facets_stats_performance.py b/src/sentry/api/endpoints/organization_events_facets_stats_performance.py index 606f2fe391cca8..1be57606e72bd4 100644 --- a/src/sentry/api/endpoints/organization_events_facets_stats_performance.py +++ b/src/sentry/api/endpoints/organization_events_facets_stats_performance.py @@ -6,6 +6,7 @@ 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 region_silo_endpoint from sentry.api.bases import NoProjects from sentry.api.endpoints.organization_events_facets_performance import ( @@ -24,6 +25,9 @@ class OrganizationEventsFacetsStatsPerformanceEndpoint( OrganizationEventsFacetsPerformanceEndpointBase ): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.TEAM_STARFISH def get(self, request: Request, organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_events_has_measurements.py b/src/sentry/api/endpoints/organization_events_has_measurements.py index 5a3f7892e15194..1ae8f17d2bebfd 100644 --- a/src/sentry/api/endpoints/organization_events_has_measurements.py +++ b/src/sentry/api/endpoints/organization_events_has_measurements.py @@ -7,6 +7,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase from sentry.snuba import discover @@ -48,6 +49,10 @@ def validate(self, data): @region_silo_endpoint class OrganizationEventsHasMeasurementsEndpoint(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: if not self.has_feature(organization, request): return Response(status=404) diff --git a/src/sentry/api/endpoints/organization_events_histogram.py b/src/sentry/api/endpoints/organization_events_histogram.py index 75d3d61fff3d24..943f82def57e02 100644 --- a/src/sentry/api/endpoints/organization_events_histogram.py +++ b/src/sentry/api/endpoints/organization_events_histogram.py @@ -5,6 +5,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase from sentry.snuba import discover @@ -42,6 +43,9 @@ def validate_field(self, fields): @region_silo_endpoint class OrganizationEventsHistogramEndpoint(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.PERFORMANCE def has_feature(self, organization, request): diff --git a/src/sentry/api/endpoints/organization_events_meta.py b/src/sentry/api/endpoints/organization_events_meta.py index 7c8e3165b2860f..85601f22ab8fbc 100644 --- a/src/sentry/api/endpoints/organization_events_meta.py +++ b/src/sentry/api/endpoints/organization_events_meta.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from sentry import search +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.event_search import parse_search_query @@ -18,6 +19,10 @@ @region_silo_endpoint class OrganizationEventsMetaEndpoint(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: try: params = self.get_snuba_params(request, organization) @@ -42,6 +47,10 @@ def get(self, request: Request, organization) -> Response: @region_silo_endpoint class OrganizationEventsRelatedIssuesEndpoint(OrganizationEventsEndpointBase, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: try: # events-meta is still used by events v1 which doesn't require global views @@ -101,6 +110,10 @@ def get(self, request: Request, organization) -> Response: @region_silo_endpoint class OrganizationSpansSamplesEndpoint(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: try: params = self.get_snuba_params(request, organization) diff --git a/src/sentry/api/endpoints/organization_events_root_cause_analysis.py b/src/sentry/api/endpoints/organization_events_root_cause_analysis.py index d509637e4375d4..39b13589fb1abc 100644 --- a/src/sentry/api/endpoints/organization_events_root_cause_analysis.py +++ b/src/sentry/api/endpoints/organization_events_root_cause_analysis.py @@ -1,6 +1,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization_events import OrganizationEventsEndpointBase from sentry.snuba.metrics_performance import query as metrics_query @@ -8,6 +9,10 @@ @region_silo_endpoint class OrganizationEventsRootCauseAnalysisEndpoint(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request, organization): if not features.has( "organizations:statistical-detectors-root-cause-analysis", diff --git a/src/sentry/api/endpoints/organization_events_span_ops.py b/src/sentry/api/endpoints/organization_events_span_ops.py index db85c1078a02f5..c872ecfe3f1c24 100644 --- a/src/sentry/api/endpoints/organization_events_span_ops.py +++ b/src/sentry/api/endpoints/organization_events_span_ops.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.paginator import GenericOffsetPaginator @@ -19,6 +20,10 @@ class SpanOp(TypedDict): @region_silo_endpoint class OrganizationEventsSpanOpsEndpoint(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization: Organization) -> Response: try: diff --git a/src/sentry/api/endpoints/organization_events_spans_histogram.py b/src/sentry/api/endpoints/organization_events_spans_histogram.py index 35c4a43c8fd7e5..c69e58f63bb3f7 100644 --- a/src/sentry/api/endpoints/organization_events_spans_histogram.py +++ b/src/sentry/api/endpoints/organization_events_spans_histogram.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase from sentry.api.endpoints.organization_events_spans_performance import Span @@ -35,6 +36,10 @@ def validate_span(self, span: str) -> Span: @region_silo_endpoint class OrganizationEventsSpansHistogramEndpoint(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def has_feature(self, organization, request): return features.has( "organizations:performance-span-histogram-view", organization, actor=request.user diff --git a/src/sentry/api/endpoints/organization_events_spans_performance.py b/src/sentry/api/endpoints/organization_events_spans_performance.py index b482a740f005d8..304351bcde28b8 100644 --- a/src/sentry/api/endpoints/organization_events_spans_performance.py +++ b/src/sentry/api/endpoints/organization_events_spans_performance.py @@ -15,6 +15,7 @@ from snuba_sdk.orderby import Direction, OrderBy from sentry import eventstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase from sentry.api.paginator import GenericOffsetPaginator @@ -146,6 +147,10 @@ def validate_spanGroup(self, span_groups): @region_silo_endpoint class OrganizationEventsSpansPerformanceEndpoint(OrganizationEventsSpansEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization: Organization) -> Response: try: @@ -221,6 +226,10 @@ def validate_span(self, span: str) -> Span: @region_silo_endpoint class OrganizationEventsSpansExamplesEndpoint(OrganizationEventsSpansEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization: Organization) -> Response: try: @@ -302,6 +311,10 @@ def get_result(self, limit: int, cursor: Optional[Cursor] = None) -> CursorResul @region_silo_endpoint class OrganizationEventsSpansStatsEndpoint(OrganizationEventsSpansEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization: Organization) -> Response: serializer = SpanSerializer(data=request.GET) diff --git a/src/sentry/api/endpoints/organization_events_starfish.py b/src/sentry/api/endpoints/organization_events_starfish.py index 4fce77b1fd1a25..2dc7578a5a924f 100644 --- a/src/sentry/api/endpoints/organization_events_starfish.py +++ b/src/sentry/api/endpoints/organization_events_starfish.py @@ -4,6 +4,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects from sentry.api.bases.organization_events import OrganizationEventsV2EndpointBase @@ -19,6 +20,9 @@ @region_silo_endpoint class OrganizationEventsStarfishEndpoint(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """ This is a test endpoint that's meant to only be used for starfish testing purposes. diff --git a/src/sentry/api/endpoints/organization_events_stats.py b/src/sentry/api/endpoints/organization_events_stats.py index 35f66e3354af8e..b8107b32b2d9d8 100644 --- a/src/sentry/api/endpoints/organization_events_stats.py +++ b/src/sentry/api/endpoints/organization_events_stats.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationEventsV2EndpointBase from sentry.constants import MAX_TOP_EVENTS @@ -90,6 +91,10 @@ @region_silo_endpoint class OrganizationEventsStatsEndpoint(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get_features(self, organization: Organization, request: Request) -> Mapping[str, bool]: feature_names = [ "organizations:performance-chart-interpolation", diff --git a/src/sentry/api/endpoints/organization_events_trace.py b/src/sentry/api/endpoints/organization_events_trace.py index 1e685a271f3304..fcf4e87ac8ed34 100644 --- a/src/sentry/api/endpoints/organization_events_trace.py +++ b/src/sentry/api/endpoints/organization_events_trace.py @@ -25,6 +25,7 @@ from snuba_sdk import Column, Function from sentry import constants, eventstore, features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase from sentry.api.serializers.models.event import get_tags_with_meta @@ -443,6 +444,10 @@ def query_trace_data( class OrganizationEventsTraceEndpointBase(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def has_feature(self, organization: Organization, request: HttpRequest) -> bool: return bool( features.has("organizations:performance-view", organization, actor=request.user) @@ -570,6 +575,10 @@ def get(self, request: HttpRequest, organization: Organization, trace_id: str) - @region_silo_endpoint class OrganizationEventsTraceLightEndpoint(OrganizationEventsTraceEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + @staticmethod def get_current_transaction( transactions: Sequence[SnubaTransaction], @@ -952,6 +961,10 @@ def serialize( @region_silo_endpoint class OrganizationEventsTraceMetaEndpoint(OrganizationEventsTraceEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: HttpRequest, organization: Organization, trace_id: str) -> HttpResponse: if not self.has_feature(organization, request): return Response(status=404) diff --git a/src/sentry/api/endpoints/organization_events_trends.py b/src/sentry/api/endpoints/organization_events_trends.py index cf29abef6b8b45..930748af0234e8 100644 --- a/src/sentry/api/endpoints/organization_events_trends.py +++ b/src/sentry/api/endpoints/organization_events_trends.py @@ -10,6 +10,7 @@ from snuba_sdk.function import Function from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase from sentry.api.event_search import AggregateFilter @@ -76,6 +77,9 @@ def resolve_function( class OrganizationEventsTrendsEndpointBase(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } trend_columns = { "p50": "percentile_range({column}, 0.5, {condition}, {boundary}) as {query_alias}", "p75": "percentile_range({column}, 0.75, {condition}, {boundary}) as {query_alias}", @@ -512,6 +516,10 @@ def data_fn(offset, limit): @region_silo_endpoint class OrganizationEventsTrendsStatsEndpoint(OrganizationEventsTrendsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def build_result_handler( self, request, diff --git a/src/sentry/api/endpoints/organization_events_trends_v2.py b/src/sentry/api/endpoints/organization_events_trends_v2.py index 498e93de9bb65c..215e2ebda985d1 100644 --- a/src/sentry/api/endpoints/organization_events_trends_v2.py +++ b/src/sentry/api/endpoints/organization_events_trends_v2.py @@ -15,10 +15,11 @@ from urllib3 import Retry from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase from sentry.api.paginator import GenericOffsetPaginator -from sentry.issues.grouptype import PerformanceP95TransactionDurationRegressionGroupType +from sentry.issues.grouptype import PerformanceDurationRegressionGroupType from sentry.issues.issue_occurrence import IssueEvidence, IssueOccurrence from sentry.issues.producer import produce_occurrence_to_kafka from sentry.net.http import connection_from_url @@ -74,6 +75,9 @@ def get_trends(snuba_io): @region_silo_endpoint class OrganizationEventsNewTrendsStatsEndpoint(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } enforce_rate_limit = True rate_limits = { "GET": { @@ -401,8 +405,8 @@ def send_occurrence_to_plaform(found_trending_events): project_id=project_id, event_id=uuid.uuid4().hex, fingerprint=[fingerprint_regression(qualifying_trend)], - type=PerformanceP95TransactionDurationRegressionGroupType, - issue_title=PerformanceP95TransactionDurationRegressionGroupType.description, + type=PerformanceDurationRegressionGroupType, + issue_title=PerformanceDurationRegressionGroupType.description, subtitle=f"Increased from {displayed_old_baseline}ms to {displayed_new_baseline}ms (P95)", culprit=qualifying_trend["transaction"], evidence_data=qualifying_trend, diff --git a/src/sentry/api/endpoints/organization_events_vitals.py b/src/sentry/api/endpoints/organization_events_vitals.py index 9e7cd1a5f43db9..baf933c0ed5ab0 100644 --- a/src/sentry/api/endpoints/organization_events_vitals.py +++ b/src/sentry/api/endpoints/organization_events_vitals.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase from sentry.search.events.fields import get_function_alias @@ -12,6 +13,9 @@ @region_silo_endpoint class OrganizationEventsVitalsEndpoint(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } VITALS = { "measurements.lcp": {"thresholds": [0, 2500, 4000]}, "measurements.fid": {"thresholds": [0, 100, 300]}, diff --git a/src/sentry/api/endpoints/organization_group_index.py b/src/sentry/api/endpoints/organization_group_index.py index 639577cd16da13..69d19473ee93e7 100644 --- a/src/sentry/api/endpoints/organization_group_index.py +++ b/src/sentry/api/endpoints/organization_group_index.py @@ -10,6 +10,7 @@ from sentry import features, search from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationEventPermission, OrganizationEventsEndpointBase from sentry.api.event_search import SearchFilter @@ -140,6 +141,11 @@ def inbox_search( @region_silo_endpoint class OrganizationGroupIndexEndpoint(OrganizationEventsEndpointBase): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ISSUES permission_classes = (OrganizationEventPermission,) enforce_rate_limit = True diff --git a/src/sentry/api/endpoints/organization_group_index_stats.py b/src/sentry/api/endpoints/organization_group_index_stats.py index d5901fdf4e282d..79cec70f377264 100644 --- a/src/sentry/api/endpoints/organization_group_index_stats.py +++ b/src/sentry/api/endpoints/organization_group_index_stats.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationEventPermission, OrganizationEventsEndpointBase from sentry.api.endpoints.organization_group_index import ERR_INVALID_STATS_PERIOD @@ -15,6 +16,9 @@ @region_silo_endpoint class OrganizationGroupIndexStatsEndpoint(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationEventPermission,) enforce_rate_limit = True diff --git a/src/sentry/api/endpoints/organization_index.py b/src/sentry/api/endpoints/organization_index.py index 620d498bf83acd..2b2de10d9e7e54 100644 --- a/src/sentry/api/endpoints/organization_index.py +++ b/src/sentry/api/endpoints/organization_index.py @@ -8,6 +8,7 @@ from sentry import analytics, audit_log, features, options from sentry import ratelimits as ratelimiter from sentry import roles +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.bases.organization import OrganizationPermission from sentry.api.paginator import DateTimePaginator, OffsetPaginator @@ -51,6 +52,10 @@ def validate_agreeTerms(self, value): @region_silo_endpoint class OrganizationIndexEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/organization_integration_issues.py b/src/sentry/api/endpoints/organization_integration_issues.py index eb97d7fa1c8a09..aa4eccbcc2f47b 100644 --- a/src/sentry/api/endpoints/organization_integration_issues.py +++ b/src/sentry/api/endpoints/organization_integration_issues.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization_integrations import RegionOrganizationIntegrationBaseEndpoint from sentry.integrations.mixins import IssueSyncMixin @@ -11,6 +12,10 @@ @region_silo_endpoint class OrganizationIntegrationIssuesEndpoint(RegionOrganizationIntegrationBaseEndpoint): + publish_status = { + "PUT": ApiPublishStatus.UNKNOWN, + } + def put( self, request: Request, diff --git a/src/sentry/api/endpoints/organization_integration_migrate_opsgenie.py b/src/sentry/api/endpoints/organization_integration_migrate_opsgenie.py index f6e38ab05344c1..797db7652bb499 100644 --- a/src/sentry/api/endpoints/organization_integration_migrate_opsgenie.py +++ b/src/sentry/api/endpoints/organization_integration_migrate_opsgenie.py @@ -4,6 +4,7 @@ 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 region_silo_endpoint from sentry.api.bases.organization_integrations import RegionOrganizationIntegrationBaseEndpoint from sentry.integrations.opsgenie.integration import OpsgenieIntegration @@ -12,6 +13,9 @@ @region_silo_endpoint class OrganizationIntegrationMigrateOpsgenieEndpoint(RegionOrganizationIntegrationBaseEndpoint): + publish_status = { + "PUT": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE def put( diff --git a/src/sentry/api/endpoints/organization_integration_repos.py b/src/sentry/api/endpoints/organization_integration_repos.py index eeff6049034d0d..edd6dad940fa64 100644 --- a/src/sentry/api/endpoints/organization_integration_repos.py +++ b/src/sentry/api/endpoints/organization_integration_repos.py @@ -4,6 +4,7 @@ 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 region_silo_endpoint from sentry.api.bases.organization_integrations import RegionOrganizationIntegrationBaseEndpoint from sentry.auth.exceptions import IdentityNotValid @@ -22,6 +23,9 @@ class IntegrationRepository(TypedDict): @region_silo_endpoint class OrganizationIntegrationReposEndpoint(RegionOrganizationIntegrationBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ISSUES def get( diff --git a/src/sentry/api/endpoints/organization_integration_serverless_functions.py b/src/sentry/api/endpoints/organization_integration_serverless_functions.py index b43ae161416323..2f2a403c1d52dd 100644 --- a/src/sentry/api/endpoints/organization_integration_serverless_functions.py +++ b/src/sentry/api/endpoints/organization_integration_serverless_functions.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization_integrations import RegionOrganizationIntegrationBaseEndpoint from sentry.api.serializers.rest_framework.base import CamelSnakeSerializer @@ -21,6 +22,11 @@ class ServerlessActionSerializer(CamelSnakeSerializer): @region_silo_endpoint class OrganizationIntegrationServerlessFunctionsEndpoint(RegionOrganizationIntegrationBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } + def get( self, request: Request, diff --git a/src/sentry/api/endpoints/organization_issues_count.py b/src/sentry/api/endpoints/organization_issues_count.py index 4b368940ad64dd..95bb241a349355 100644 --- a/src/sentry/api/endpoints/organization_issues_count.py +++ b/src/sentry/api/endpoints/organization_issues_count.py @@ -4,6 +4,7 @@ from sentry_sdk import start_span from sentry import features, search +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationEventsEndpointBase from sentry.api.helpers.group_index import ValidationError, validate_search_filter_permissions @@ -20,6 +21,9 @@ @region_silo_endpoint class OrganizationIssuesCountEndpoint(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } enforce_rate_limit = True rate_limits = { "GET": { diff --git a/src/sentry/api/endpoints/organization_issues_resolved_in_release.py b/src/sentry/api/endpoints/organization_issues_resolved_in_release.py index dd7108a71f6d7c..90a93203f7ee81 100644 --- a/src/sentry/api/endpoints/organization_issues_resolved_in_release.py +++ b/src/sentry/api/endpoints/organization_issues_resolved_in_release.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.helpers.releases import get_group_ids_resolved_in_release @@ -11,6 +12,9 @@ @region_silo_endpoint class OrganizationIssuesResolvedInReleaseEndpoint(OrganizationEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationPermission,) def get(self, request: Request, organization, version) -> Response: diff --git a/src/sentry/api/endpoints/organization_measurements_meta.py b/src/sentry/api/endpoints/organization_measurements_meta.py index ae6c4484729411..18d6b969d40280 100644 --- a/src/sentry/api/endpoints/organization_measurements_meta.py +++ b/src/sentry/api/endpoints/organization_measurements_meta.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry_sdk import start_span +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.models import Organization @@ -12,6 +13,10 @@ @region_silo_endpoint class OrganizationMeasurementsMeta(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization: Organization) -> Response: try: params = self.get_snuba_params(request, organization) diff --git a/src/sentry/api/endpoints/organization_member/details.py b/src/sentry/api/endpoints/organization_member/details.py index b140ea1e3a3ad4..9b336dd45cd352 100644 --- a/src/sentry/api/endpoints/organization_member/details.py +++ b/src/sentry/api/endpoints/organization_member/details.py @@ -7,6 +7,7 @@ from rest_framework.serializers import ValidationError from sentry import audit_log, features, ratelimits, roles +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationMemberEndpoint from sentry.api.bases.organization import OrganizationPermission @@ -68,6 +69,11 @@ def is_member_disabled_from_limit(self, request: Request, organization): @extend_schema(tags=["Organizations"]) @region_silo_endpoint class OrganizationMemberDetailsEndpoint(OrganizationMemberEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.PUBLIC, + "GET": ApiPublishStatus.PUBLIC, + "PUT": ApiPublishStatus.PUBLIC, + } public = {"GET", "PUT", "DELETE"} permission_classes = [RelaxedMemberPermission] diff --git a/src/sentry/api/endpoints/organization_member/index.py b/src/sentry/api/endpoints/organization_member/index.py index 502c9a4539795a..940576165f7ba1 100644 --- a/src/sentry/api/endpoints/organization_member/index.py +++ b/src/sentry/api/endpoints/organization_member/index.py @@ -8,6 +8,7 @@ from rest_framework.response import Response from sentry import audit_log, features, ratelimits, roles +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.paginator import OffsetPaginator @@ -121,6 +122,10 @@ def validate_teamRoles(self, teamRoles) -> List[Tuple[Team, str]]: @region_silo_endpoint class OrganizationMemberIndexEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (MemberPermission,) def get(self, request: Request, organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_member/requests/invite/details.py b/src/sentry/api/endpoints/organization_member/requests/invite/details.py index 7a538102b154a9..9b43f961d9e2a9 100644 --- a/src/sentry/api/endpoints/organization_member/requests/invite/details.py +++ b/src/sentry/api/endpoints/organization_member/requests/invite/details.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import roles +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationMemberEndpoint from sentry.api.bases.organization import OrganizationPermission @@ -44,6 +45,11 @@ class InviteRequestPermissions(OrganizationPermission): @region_silo_endpoint class OrganizationInviteRequestDetailsEndpoint(OrganizationMemberEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (InviteRequestPermissions,) def _get_member( diff --git a/src/sentry/api/endpoints/organization_member/requests/invite/index.py b/src/sentry/api/endpoints/organization_member/requests/invite/index.py index 9fd52b57bbdcd3..353a9815ffb0d6 100644 --- a/src/sentry/api/endpoints/organization_member/requests/invite/index.py +++ b/src/sentry/api/endpoints/organization_member/requests/invite/index.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import audit_log, roles +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.endpoints.organization_member.index import OrganizationMemberSerializer @@ -26,6 +27,10 @@ class InviteRequestPermissions(OrganizationPermission): @region_silo_endpoint class OrganizationInviteRequestIndexEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (InviteRequestPermissions,) def get(self, request: Request, organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_member/requests/join.py b/src/sentry/api/endpoints/organization_member/requests/join.py index 96aca833f7672c..c206a5c6783bf6 100644 --- a/src/sentry/api/endpoints/organization_member/requests/join.py +++ b/src/sentry/api/endpoints/organization_member/requests/join.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import ratelimits as ratelimiter +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.validators import AllowedEmailField @@ -49,6 +50,9 @@ def create_organization_join_request(organization, email, ip_address=None): @region_silo_endpoint class OrganizationJoinRequestEndpoint(OrganizationEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } # Disable authentication and permission requirements. permission_classes = [] @@ -68,8 +72,8 @@ def post(self, request: Request, organization) -> Response: # users can already join organizations with SSO enabled without an invite # so they should join that way and not through a request to the admins - providers = auth_service.get_auth_providers(organization_id=organization.id) - if len(providers) != 0: + provider = auth_service.get_auth_provider(organization_id=organization.id) + if provider is not None: return Response(status=403) ip_address = request.META["REMOTE_ADDR"] diff --git a/src/sentry/api/endpoints/organization_member/team_details.py b/src/sentry/api/endpoints/organization_member/team_details.py index d0e65f2b3efd15..4d57f95cee9a67 100644 --- a/src/sentry/api/endpoints/organization_member/team_details.py +++ b/src/sentry/api/endpoints/organization_member/team_details.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import audit_log, features, roles +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationMemberEndpoint from sentry.api.bases.organization import OrganizationPermission @@ -96,6 +97,12 @@ def _is_org_owner_or_manager(access: Access) -> bool: @extend_schema(tags=["Teams"]) @region_silo_endpoint class OrganizationMemberTeamDetailsEndpoint(OrganizationMemberEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.PUBLIC, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.PUBLIC, + } public = {"DELETE", "POST"} permission_classes = (OrganizationTeamMemberPermission,) diff --git a/src/sentry/api/endpoints/organization_member_unreleased_commits.py b/src/sentry/api/endpoints/organization_member_unreleased_commits.py index 3fc3117a5a33b0..847140110c74d1 100644 --- a/src/sentry/api/endpoints/organization_member_unreleased_commits.py +++ b/src/sentry/api/endpoints/organization_member_unreleased_commits.py @@ -1,5 +1,6 @@ from django.db import connections +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationMemberEndpoint from sentry.api.serializers import serialize @@ -41,6 +42,10 @@ @region_silo_endpoint class OrganizationMemberUnreleasedCommitsEndpoint(OrganizationMemberEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, member) -> Response: email_list = [ e.email diff --git a/src/sentry/api/endpoints/organization_metrics.py b/src/sentry/api/endpoints/organization_metrics.py index 5976f7fe7a58dc..49c839babeb1ca 100644 --- a/src/sentry/api/endpoints/organization_metrics.py +++ b/src/sentry/api/endpoints/organization_metrics.py @@ -3,6 +3,7 @@ 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 region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -40,6 +41,9 @@ def get_use_case_id(request: Request) -> UseCaseID: @region_silo_endpoint class OrganizationMetricsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """Get metric name, available operations and the metric unit""" owner = ApiOwner.TELEMETRY_EXPERIENCE @@ -54,6 +58,9 @@ def get(self, request: Request, organization) -> Response: @region_silo_endpoint class OrganizationMetricDetailsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """Get metric name, available operations, metric unit and available tags""" owner = ApiOwner.TELEMETRY_EXPERIENCE @@ -77,6 +84,9 @@ def get(self, request: Request, organization, metric_name) -> Response: @region_silo_endpoint class OrganizationMetricsTagsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """Get list of tag names for this project If the ``metric`` query param is provided, only tags for a certain metric @@ -107,6 +117,9 @@ def get(self, request: Request, organization) -> Response: @region_silo_endpoint class OrganizationMetricsTagDetailsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """Get all existing tag values for a metric""" owner = ApiOwner.TELEMETRY_EXPERIENCE @@ -136,6 +149,9 @@ def get(self, request: Request, organization, tag_name) -> Response: @region_silo_endpoint class OrganizationMetricsDataEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """Get the time series data for one or more metrics. The data can be filtered and grouped by tags. diff --git a/src/sentry/api/endpoints/organization_metrics_estimation_stats.py b/src/sentry/api/endpoints/organization_metrics_estimation_stats.py index 77aaa68c426df0..2935426c158cc2 100644 --- a/src/sentry/api/endpoints/organization_metrics_estimation_stats.py +++ b/src/sentry/api/endpoints/organization_metrics_estimation_stats.py @@ -7,6 +7,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationEventsV2EndpointBase from sentry.models import Organization @@ -27,6 +28,9 @@ class CountResult(TypedDict): @region_silo_endpoint class OrganizationMetricsEstimationStatsEndpoint(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """Gets the estimated volume of an organization's metric events.""" def get(self, request: Request, organization: Organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_metrics_meta.py b/src/sentry/api/endpoints/organization_metrics_meta.py index b75049c3a6d7fa..8b80bf45f4d421 100644 --- a/src/sentry/api/endpoints/organization_metrics_meta.py +++ b/src/sentry/api/endpoints/organization_metrics_meta.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry_sdk import set_tag +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.search.events.fields import get_function_alias @@ -14,6 +15,9 @@ @region_silo_endpoint class OrganizationMetricsCompatibility(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """Metrics data can contain less than great data like null or unparameterized transactions This endpoint will return projects that have dynamic sampling turned on, and another list of "compatible projects" @@ -61,6 +65,9 @@ def get(self, request: Request, organization) -> Response: @region_silo_endpoint class OrganizationMetricsCompatibilitySums(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """Return the total sum of metrics data, the null transactions and unparameterized transactions This is so the frontend can have an idea given its current selection of projects how good/bad the display would diff --git a/src/sentry/api/endpoints/organization_missing_org_members.py b/src/sentry/api/endpoints/organization_missing_org_members.py index 63f2dab5ae83fd..cb449de65ec27c 100644 --- a/src/sentry/api/endpoints/organization_missing_org_members.py +++ b/src/sentry/api/endpoints/organization_missing_org_members.py @@ -10,6 +10,7 @@ from rest_framework.response import Response from sentry import roles +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.serializers import Serializer, serialize @@ -30,6 +31,9 @@ class MissingMembersPermission(OrganizationPermission): @region_silo_endpoint class OrganizationMissingMembersEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (MissingMembersPermission,) def _get_missing_members(self, organization: Organization) -> QuerySet[CommitAuthor]: diff --git a/src/sentry/api/endpoints/organization_onboarding_continuation_email.py b/src/sentry/api/endpoints/organization_onboarding_continuation_email.py index 663f8211a9ff25..d54a2323b01358 100644 --- a/src/sentry/api/endpoints/organization_onboarding_continuation_email.py +++ b/src/sentry/api/endpoints/organization_onboarding_continuation_email.py @@ -5,6 +5,7 @@ from sentry import analytics from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.serializers.rest_framework.base import CamelSnakeSerializer @@ -41,6 +42,9 @@ def get_request_builder_args(user: User, organization: Organization, platforms: @region_silo_endpoint class OrganizationOnboardingContinuationEmail(OrganizationEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.GROWTH # let anyone in the org use this endpoint permission_classes = () diff --git a/src/sentry/api/endpoints/organization_onboarding_tasks.py b/src/sentry/api/endpoints/organization_onboarding_tasks.py index 50ed8113308bbd..699e20f3806e24 100644 --- a/src/sentry/api/endpoints/organization_onboarding_tasks.py +++ b/src/sentry/api/endpoints/organization_onboarding_tasks.py @@ -3,6 +3,7 @@ from rest_framework.response import Response from sentry import onboarding_tasks +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.models import OnboardingTaskStatus @@ -14,6 +15,9 @@ class OnboardingTaskPermission(OrganizationPermission): @region_silo_endpoint class OrganizationOnboardingTaskEndpoint(OrganizationEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (OnboardingTaskPermission,) def post(self, request: Request, organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_pinned_searches.py b/src/sentry/api/endpoints/organization_pinned_searches.py index 85f831d2287884..3704f3efcfbaa2 100644 --- a/src/sentry/api/endpoints/organization_pinned_searches.py +++ b/src/sentry/api/endpoints/organization_pinned_searches.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPinnedSearchPermission from sentry.api.serializers import serialize @@ -28,6 +29,10 @@ def validate_type(self, value): @region_silo_endpoint class OrganizationPinnedSearchEndpoint(OrganizationEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationPinnedSearchPermission,) def put(self, request: Request, organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_processingissues.py b/src/sentry/api/endpoints/organization_processingissues.py index c5182eebcafbb3..92608499e8e355 100644 --- a/src/sentry/api/endpoints/organization_processingissues.py +++ b/src/sentry/api/endpoints/organization_processingissues.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.helpers.processing_issues import get_processing_issues @@ -9,6 +10,10 @@ @region_silo_endpoint class OrganizationProcessingIssuesEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: """ For each Project in an Organization, list its processing issues. Can diff --git a/src/sentry/api/endpoints/organization_profiling_functions.py b/src/sentry/api/endpoints/organization_profiling_functions.py index f3db4e031bd585..aca41295a6319f 100644 --- a/src/sentry/api/endpoints/organization_profiling_functions.py +++ b/src/sentry/api/endpoints/organization_profiling_functions.py @@ -11,6 +11,7 @@ from urllib3 import Retry from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase from sentry.api.paginator import GenericOffsetPaginator @@ -75,6 +76,10 @@ class FunctionTrendsSerializer(serializers.Serializer): @region_silo_endpoint class OrganizationProfilingFunctionTrendsEndpoint(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def has_feature(self, organization, request): return features.has( "organizations:profiling-global-suspect-functions", organization, actor=request.user diff --git a/src/sentry/api/endpoints/organization_profiling_profiles.py b/src/sentry/api/endpoints/organization_profiling_profiles.py index ed00a88b093db4..38a7de90667b43 100644 --- a/src/sentry/api/endpoints/organization_profiling_profiles.py +++ b/src/sentry/api/endpoints/organization_profiling_profiles.py @@ -7,6 +7,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint # from sentry.api.bases.organization import OrganizationEndpoint @@ -37,6 +38,10 @@ def get_profiling_params(self, request: Request, organization: Organization) -> @region_silo_endpoint class OrganizationProfilingFiltersEndpoint(OrganizationProfilingBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization: Organization) -> HttpResponse: if not features.has("organizations:profiling", organization, actor=request.user): return Response(status=404) @@ -53,6 +58,10 @@ def get(self, request: Request, organization: Organization) -> HttpResponse: @region_silo_endpoint class OrganizationProfilingFlamegraphEndpoint(OrganizationProfilingBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization: Organization) -> HttpResponse: if not features.has("organizations:profiling", organization, actor=request.user): return Response(status=404) diff --git a/src/sentry/api/endpoints/organization_projects.py b/src/sentry/api/endpoints/organization_projects.py index 9952a6d4381917..8fd59d7dd4257b 100644 --- a/src/sentry/api/endpoints/organization_projects.py +++ b/src/sentry/api/endpoints/organization_projects.py @@ -5,6 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.paginator import OffsetPaginator @@ -27,6 +28,9 @@ @extend_schema(tags=["Organizations"]) @region_silo_endpoint class OrganizationProjectsEndpoint(OrganizationEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.PUBLIC, + } public = {"GET"} @extend_schema( @@ -153,6 +157,10 @@ def serialize_on_result(result): @region_silo_endpoint class OrganizationProjectsCountEndpoint(OrganizationEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: queryset = Project.objects.filter(organization=organization) diff --git a/src/sentry/api/endpoints/organization_projects_experiment.py b/src/sentry/api/endpoints/organization_projects_experiment.py index 1a916727738fcf..e025fab6456577 100644 --- a/src/sentry/api/endpoints/organization_projects_experiment.py +++ b/src/sentry/api/endpoints/organization_projects_experiment.py @@ -12,6 +12,7 @@ from sentry import audit_log, features from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.endpoints.team_projects import ProjectPostSerializer @@ -51,6 +52,9 @@ class OrgProjectPermission(OrganizationPermission): @region_silo_endpoint class OrganizationProjectsExperimentEndpoint(OrganizationEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrgProjectPermission,) logger = logging.getLogger("team-project.create") owner = ApiOwner.ENTERPRISE diff --git a/src/sentry/api/endpoints/organization_projects_sent_first_event.py b/src/sentry/api/endpoints/organization_projects_sent_first_event.py index 1656d7ec59cf45..dd29dacb6d1445 100644 --- a/src/sentry/api/endpoints/organization_projects_sent_first_event.py +++ b/src/sentry/api/endpoints/organization_projects_sent_first_event.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.serializers import serialize @@ -8,6 +9,10 @@ @region_silo_endpoint class OrganizationProjectsSentFirstEventEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: """ Verify If Any Project Within An Organization Has Received a First Event diff --git a/src/sentry/api/endpoints/organization_recent_searches.py b/src/sentry/api/endpoints/organization_recent_searches.py index 973a6f785a2370..d3ca041772bfc7 100644 --- a/src/sentry/api/endpoints/organization_recent_searches.py +++ b/src/sentry/api/endpoints/organization_recent_searches.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.serializers import serialize @@ -24,6 +25,10 @@ class OrganizationRecentSearchPermission(OrganizationPermission): @region_silo_endpoint class OrganizationRecentSearchesEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationRecentSearchPermission,) def get(self, request: Request, organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_relay_usage.py b/src/sentry/api/endpoints/organization_relay_usage.py index df2e7dbefb4e01..261eb1da9e077f 100644 --- a/src/sentry/api/endpoints/organization_relay_usage.py +++ b/src/sentry/api/endpoints/organization_relay_usage.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationEndpoint, OrganizationPermission from sentry.api.serializers import serialize @@ -10,6 +11,9 @@ @region_silo_endpoint class OrganizationRelayUsage(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationPermission,) def get(self, request: Request, organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_release_assemble.py b/src/sentry/api/endpoints/organization_release_assemble.py index 60eff285d687cd..da3d93365ce398 100644 --- a/src/sentry/api/endpoints/organization_release_assemble.py +++ b/src/sentry/api/endpoints/organization_release_assemble.py @@ -3,6 +3,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -18,6 +19,10 @@ @region_silo_endpoint class OrganizationReleaseAssembleEndpoint(OrganizationReleasesBaseEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } + def post(self, request: Request, organization, version) -> Response: """ Handle an artifact bundle and merge it into the release diff --git a/src/sentry/api/endpoints/organization_release_commits.py b/src/sentry/api/endpoints/organization_release_commits.py index a7effea70dfe46..5a292e3d9402e9 100644 --- a/src/sentry/api/endpoints/organization_release_commits.py +++ b/src/sentry/api/endpoints/organization_release_commits.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -10,6 +11,10 @@ @region_silo_endpoint class OrganizationReleaseCommitsEndpoint(OrganizationReleasesBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, version) -> Response: """ List an Organization Release's Commits diff --git a/src/sentry/api/endpoints/organization_release_details.py b/src/sentry/api/endpoints/organization_release_details.py index dc6b354c1e7d08..c7e1d2aed313f1 100644 --- a/src/sentry/api/endpoints/organization_release_details.py +++ b/src/sentry/api/endpoints/organization_release_details.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import release_health +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import ReleaseAnalyticsMixin, region_silo_endpoint from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint from sentry.api.endpoints.organization_releases import ( @@ -272,6 +273,12 @@ class OrganizationReleaseDetailsEndpoint( ReleaseAnalyticsMixin, OrganizationReleaseDetailsPaginationMixin, ): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, version) -> Response: """ Retrieve an Organization's Release diff --git a/src/sentry/api/endpoints/organization_release_file_details.py b/src/sentry/api/endpoints/organization_release_file_details.py index 1b0146b59fc6a2..8816dd5154f14c 100644 --- a/src/sentry/api/endpoints/organization_release_file_details.py +++ b/src/sentry/api/endpoints/organization_release_file_details.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint from sentry.api.endpoints.project_release_file_details import ReleaseFileDetailsMixin @@ -17,6 +18,12 @@ class ReleaseFileSerializer(serializers.Serializer): class OrganizationReleaseFileDetailsEndpoint( OrganizationReleasesBaseEndpoint, ReleaseFileDetailsMixin ): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, version, file_id) -> Response: """ Retrieve an Organization Release's File diff --git a/src/sentry/api/endpoints/organization_release_files.py b/src/sentry/api/endpoints/organization_release_files.py index e63be19c1d3bce..ddf7850254a2cc 100644 --- a/src/sentry/api/endpoints/organization_release_files.py +++ b/src/sentry/api/endpoints/organization_release_files.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint from sentry.api.endpoints.project_release_files import ReleaseFilesMixin @@ -12,6 +13,11 @@ @region_silo_endpoint class OrganizationReleaseFilesEndpoint(OrganizationReleasesBaseEndpoint, ReleaseFilesMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, version) -> Response: """ List an Organization Release's Files diff --git a/src/sentry/api/endpoints/organization_release_meta.py b/src/sentry/api/endpoints/organization_release_meta.py index 00c78b407bb6f7..b0061e35e39b88 100644 --- a/src/sentry/api/endpoints/organization_release_meta.py +++ b/src/sentry/api/endpoints/organization_release_meta.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -13,6 +14,10 @@ @region_silo_endpoint class OrganizationReleaseMetaEndpoint(OrganizationReleasesBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, version) -> Response: """ Retrieve an Organization's Release's Associated Meta Data diff --git a/src/sentry/api/endpoints/organization_release_previous_commits.py b/src/sentry/api/endpoints/organization_release_previous_commits.py index 280d1f8d29f253..1530284be8807b 100644 --- a/src/sentry/api/endpoints/organization_release_previous_commits.py +++ b/src/sentry/api/endpoints/organization_release_previous_commits.py @@ -3,6 +3,7 @@ from sentry import analytics from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -13,6 +14,9 @@ @region_silo_endpoint class OrganizationReleasePreviousCommitsEndpoint(OrganizationReleasesBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ISSUES rate_limits = RateLimitConfig(group="CLI") diff --git a/src/sentry/api/endpoints/organization_releases.py b/src/sentry/api/endpoints/organization_releases.py index d92aee4d3da19b..a49e8c32b63e18 100644 --- a/src/sentry/api/endpoints/organization_releases.py +++ b/src/sentry/api/endpoints/organization_releases.py @@ -10,6 +10,7 @@ from rest_framework.response import Response from sentry import analytics, release_health +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, ReleaseAnalyticsMixin, region_silo_endpoint from sentry.api.bases import NoProjects from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint @@ -215,6 +216,10 @@ def debounce_update_release_health_data(organization, project_ids): class OrganizationReleasesEndpoint( OrganizationReleasesBaseEndpoint, EnvironmentMixin, ReleaseAnalyticsMixin ): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } SESSION_SORTS = frozenset( [ "crash_free_sessions", @@ -581,6 +586,10 @@ def post(self, request: Request, organization) -> Response: @region_silo_endpoint class OrganizationReleasesStatsEndpoint(OrganizationReleasesBaseEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: """ List an Organization's Releases specifically for building timeseries diff --git a/src/sentry/api/endpoints/organization_repositories.py b/src/sentry/api/endpoints/organization_repositories.py index 0eea4a2a99ce19..94e1bef9e292c3 100644 --- a/src/sentry/api/endpoints/organization_repositories.py +++ b/src/sentry/api/endpoints/organization_repositories.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import ( OrganizationEndpoint, @@ -21,6 +22,10 @@ @region_silo_endpoint class OrganizationRepositoriesEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationIntegrationsLoosePermission,) rate_limits = RateLimitConfig( group="CLI", limit_overrides={"POST": SENTRY_RATELIMITER_GROUP_DEFAULTS["default"]} diff --git a/src/sentry/api/endpoints/organization_repository_commits.py b/src/sentry/api/endpoints/organization_repository_commits.py index acf293c6036e1b..31f3846259e736 100644 --- a/src/sentry/api/endpoints/organization_repository_commits.py +++ b/src/sentry/api/endpoints/organization_repository_commits.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -11,6 +12,10 @@ @region_silo_endpoint class OrganizationRepositoryCommitsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, repo_id) -> Response: """ List a Repository's Commits diff --git a/src/sentry/api/endpoints/organization_repository_details.py b/src/sentry/api/endpoints/organization_repository_details.py index 2b55c64e9ecaf4..c52ca56c3914be 100644 --- a/src/sentry/api/endpoints/organization_repository_details.py +++ b/src/sentry/api/endpoints/organization_repository_details.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationIntegrationsPermission from sentry.api.exceptions import ResourceDoesNotExist @@ -31,6 +32,10 @@ class RepositorySerializer(serializers.Serializer): @region_silo_endpoint class OrganizationRepositoryDetailsEndpoint(OrganizationEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationIntegrationsPermission,) def put(self, request: Request, organization, repo_id) -> Response: diff --git a/src/sentry/api/endpoints/organization_request_project_creation.py b/src/sentry/api/endpoints/organization_request_project_creation.py index e45de1aff24b57..aef2fe1e3cc286 100644 --- a/src/sentry/api/endpoints/organization_request_project_creation.py +++ b/src/sentry/api/endpoints/organization_request_project_creation.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization_request_change import OrganizationRequestChangeEndpoint from sentry.api.serializers.rest_framework import CamelSnakeSerializer @@ -11,6 +12,9 @@ class OrganizationRequestProjectCreationSerializer(CamelSnakeSerializer): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } target_user_email = serializers.EmailField(required=True) diff --git a/src/sentry/api/endpoints/organization_sdk_updates.py b/src/sentry/api/endpoints/organization_sdk_updates.py index ef4fb24f329218..03e93d418d5c1b 100644 --- a/src/sentry/api/endpoints/organization_sdk_updates.py +++ b/src/sentry/api/endpoints/organization_sdk_updates.py @@ -8,6 +8,7 @@ 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 region_silo_endpoint from sentry.api.bases import OrganizationEventsEndpointBase from sentry.api.bases.organization import OrganizationEndpoint @@ -66,6 +67,10 @@ def serialize(data, projects): @region_silo_endpoint class OrganizationSdkUpdatesEndpoint(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: projects = self.get_projects(request, organization) @@ -101,6 +106,9 @@ def get(self, request: Request, organization) -> Response: @region_silo_endpoint class OrganizationSdksEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.TELEMETRY_EXPERIENCE def get(self, request: Request, organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_search_details.py b/src/sentry/api/endpoints/organization_search_details.py index 480023aa89ba6b..78c284bd31bfcb 100644 --- a/src/sentry/api/endpoints/organization_search_details.py +++ b/src/sentry/api/endpoints/organization_search_details.py @@ -3,6 +3,7 @@ from rest_framework.response import Response from sentry import analytics +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationSearchPermission from sentry.api.exceptions import ResourceDoesNotExist @@ -34,6 +35,10 @@ def has_object_permission(self, request: Request, view, obj): @region_silo_endpoint class OrganizationSearchDetailsEndpoint(OrganizationEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationSearchEditPermission,) def convert_args(self, request: Request, organization_slug, search_id, *args, **kwargs): diff --git a/src/sentry/api/endpoints/organization_searches.py b/src/sentry/api/endpoints/organization_searches.py index 971eb509a475ef..e70b576ee04314 100644 --- a/src/sentry/api/endpoints/organization_searches.py +++ b/src/sentry/api/endpoints/organization_searches.py @@ -4,6 +4,7 @@ from sentry import analytics from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationSearchPermission from sentry.api.serializers import serialize @@ -17,6 +18,10 @@ @region_silo_endpoint class OrganizationSearchesEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ISSUES permission_classes = (OrganizationSearchPermission,) diff --git a/src/sentry/api/endpoints/organization_sentry_function.py b/src/sentry/api/endpoints/organization_sentry_function.py index fdcc66bcdc3c82..36d28e96738803 100644 --- a/src/sentry/api/endpoints/organization_sentry_function.py +++ b/src/sentry/api/endpoints/organization_sentry_function.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationEndpoint from sentry.api.serializers import serialize @@ -37,6 +38,10 @@ def validate_env_variables(self, env_variables): @region_silo_endpoint class OrganizationSentryFunctionEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } # Creating a new sentry function diff --git a/src/sentry/api/endpoints/organization_sentry_function_details.py b/src/sentry/api/endpoints/organization_sentry_function_details.py index 3635a4b65e69d4..2fe8129db79e83 100644 --- a/src/sentry/api/endpoints/organization_sentry_function_details.py +++ b/src/sentry/api/endpoints/organization_sentry_function_details.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationEndpoint from sentry.api.endpoints.organization_sentry_function import SentryFunctionSerializer @@ -14,6 +15,12 @@ @region_silo_endpoint class OrganizationSentryFunctionDetailsEndpoint(OrganizationEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def convert_args(self, request, organization_slug, function_slug, *args, **kwargs): args, kwargs = super().convert_args(request, organization_slug, *args, **kwargs) diff --git a/src/sentry/api/endpoints/organization_sessions.py b/src/sentry/api/endpoints/organization_sessions.py index 7db4cd1d365329..d5f222810fe85c 100644 --- a/src/sentry/api/endpoints/organization_sessions.py +++ b/src/sentry/api/endpoints/organization_sessions.py @@ -9,6 +9,7 @@ from sentry import features, release_health from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.paginator import GenericOffsetPaginator @@ -21,6 +22,9 @@ # NOTE: this currently extends `OrganizationEventsEndpointBase` for `handle_query_errors` only, which should ideally be decoupled from the base class. @region_silo_endpoint class OrganizationSessionsEndpoint(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.TELEMETRY_EXPERIENCE def get(self, request: Request, organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_shortid.py b/src/sentry/api/endpoints/organization_shortid.py index 3e2b1fd5d3bb93..771f39bfe26a78 100644 --- a/src/sentry/api/endpoints/organization_shortid.py +++ b/src/sentry/api/endpoints/organization_shortid.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -10,6 +11,10 @@ @region_silo_endpoint class ShortIdLookupEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, short_id) -> Response: """ Resolve a Short ID diff --git a/src/sentry/api/endpoints/organization_slugs.py b/src/sentry/api/endpoints/organization_slugs.py index 33f0b45fd70115..64179650c1d34f 100644 --- a/src/sentry/api/endpoints/organization_slugs.py +++ b/src/sentry/api/endpoints/organization_slugs.py @@ -4,7 +4,8 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features +from sentry import options +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.exceptions import ConflictError @@ -15,6 +16,10 @@ @region_silo_endpoint class SlugsUpdateEndpoint(OrganizationEndpoint): + publish_status = { + "PUT": ApiPublishStatus.UNKNOWN, + } + def put(self, request: Request, organization) -> Response: """ Update Project Slugs @@ -31,7 +36,7 @@ def put(self, request: Request, organization) -> Response: for project_id, slug in slugs.items(): slug = slug.lower() try: - if features.has("app:enterprise-prevent-numeric-slugs"): + if options.get("api.prevent-numeric-slugs"): validate_sentry_slug(slug) else: validate_slug(slug) diff --git a/src/sentry/api/endpoints/organization_stats.py b/src/sentry/api/endpoints/organization_stats.py index d3c8c7d5cd5658..04b03bbdbfaa98 100644 --- a/src/sentry/api/endpoints/organization_stats.py +++ b/src/sentry/api/endpoints/organization_stats.py @@ -3,6 +3,7 @@ from sentry import tsdb from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, StatsMixin, region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -12,6 +13,9 @@ @region_silo_endpoint class OrganizationStatsEndpoint(OrganizationEndpoint, EnvironmentMixin, StatsMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE def get(self, request: Request, organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_stats_v2.py b/src/sentry/api/endpoints/organization_stats_v2.py index 2972eaf5665428..bc8f34a10dbf2c 100644 --- a/src/sentry/api/endpoints/organization_stats_v2.py +++ b/src/sentry/api/endpoints/organization_stats_v2.py @@ -10,6 +10,7 @@ from typing_extensions import TypedDict from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.utils import InvalidParams as InvalidParamsApi @@ -131,6 +132,9 @@ class StatsApiResponse(TypedDict): @extend_schema(tags=["Organizations"]) @region_silo_endpoint class OrganizationStatsEndpointV2(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.PUBLIC, + } owner = ApiOwner.ENTERPRISE enforce_rate_limit = True rate_limits = { diff --git a/src/sentry/api/endpoints/organization_tagkey_values.py b/src/sentry/api/endpoints/organization_tagkey_values.py index 66f2768d42c4d4..4e154d4f9b0ef3 100644 --- a/src/sentry/api/endpoints/organization_tagkey_values.py +++ b/src/sentry/api/endpoints/organization_tagkey_values.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import tagstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.paginator import SequencePaginator @@ -19,6 +20,10 @@ def validate_sort_field(field_name: str) -> str: @region_silo_endpoint class OrganizationTagKeyValuesEndpoint(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, key) -> Response: if not TAG_KEY_RE.match(key): return Response({"detail": f'Invalid tag key format for "{key}"'}, status=400) diff --git a/src/sentry/api/endpoints/organization_tags.py b/src/sentry/api/endpoints/organization_tags.py index ece2ca5077727d..3f52b49a576113 100644 --- a/src/sentry/api/endpoints/organization_tags.py +++ b/src/sentry/api/endpoints/organization_tags.py @@ -4,6 +4,7 @@ from sentry import features, tagstore from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase from sentry.api.serializers import serialize @@ -13,6 +14,9 @@ @region_silo_endpoint class OrganizationTagsEndpoint(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.PERFORMANCE def get(self, request: Request, organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_teams.py b/src/sentry/api/endpoints/organization_teams.py index 5d907a0467ea0d..5926e06640cd31 100644 --- a/src/sentry/api/endpoints/organization_teams.py +++ b/src/sentry/api/endpoints/organization_teams.py @@ -9,6 +9,7 @@ from rest_framework.response import Response from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import ( DEFAULT_SLUG_ERROR_MESSAGE, DEFAULT_SLUG_PATTERN, @@ -67,6 +68,10 @@ def validate(self, attrs): @extend_schema(tags=["Teams"]) @region_silo_endpoint class OrganizationTeamsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.PUBLIC, + "POST": ApiPublishStatus.PUBLIC, + } public = {"GET", "POST"} permission_classes = (OrganizationTeamsPermission,) diff --git a/src/sentry/api/endpoints/organization_transaction_anomaly_detection.py b/src/sentry/api/endpoints/organization_transaction_anomaly_detection.py index 66a70fab7c8970..60fb22ccf44892 100644 --- a/src/sentry/api/endpoints/organization_transaction_anomaly_detection.py +++ b/src/sentry/api/endpoints/organization_transaction_anomaly_detection.py @@ -7,6 +7,7 @@ from urllib3 import Retry from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import OrganizationEventsEndpointBase from sentry.api.utils import get_date_range_from_params @@ -97,6 +98,10 @@ def get_time_params(start, end): @region_silo_endpoint class OrganizationTransactionAnomalyDetectionEndpoint(OrganizationEventsEndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def has_feature(self, organization, request): return features.has( "organizations:performance-anomaly-detection-ui", organization, actor=request.user diff --git a/src/sentry/api/endpoints/organization_user_details.py b/src/sentry/api/endpoints/organization_user_details.py index 15ac2a4a31cf2c..6bb627f374993a 100644 --- a/src/sentry/api/endpoints/organization_user_details.py +++ b/src/sentry/api/endpoints/organization_user_details.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.endpoints.organization_member.index import MemberPermission @@ -10,6 +11,9 @@ @region_silo_endpoint class OrganizationUserDetailsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (MemberPermission,) def get(self, request: Request, organization, user_id) -> Response: diff --git a/src/sentry/api/endpoints/organization_user_reports.py b/src/sentry/api/endpoints/organization_user_reports.py index 0bc854a5bc1551..6888fbd5f22677 100644 --- a/src/sentry/api/endpoints/organization_user_reports.py +++ b/src/sentry/api/endpoints/organization_user_reports.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects from sentry.api.bases.organization import OrganizationEndpoint, OrganizationUserReportsPermission @@ -13,6 +14,9 @@ @region_silo_endpoint class OrganizationUserReportsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationUserReportsPermission,) def get(self, request: Request, organization) -> Response: diff --git a/src/sentry/api/endpoints/organization_user_teams.py b/src/sentry/api/endpoints/organization_user_teams.py index a6e79e65aa9580..c877e2a9678cdd 100644 --- a/src/sentry/api/endpoints/organization_user_teams.py +++ b/src/sentry/api/endpoints/organization_user_teams.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.serializers import serialize @@ -11,6 +12,10 @@ @region_silo_endpoint class OrganizationUserTeamsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: """ List your Teams In the Current Organization diff --git a/src/sentry/api/endpoints/organization_users.py b/src/sentry/api/endpoints/organization_users.py index 19c49a873eb1af..5bfe20b5cddf5a 100644 --- a/src/sentry/api/endpoints/organization_users.py +++ b/src/sentry/api/endpoints/organization_users.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.serializers import serialize @@ -12,6 +13,10 @@ @region_silo_endpoint class OrganizationUsersEndpoint(OrganizationEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: """ List an Organization's Users diff --git a/src/sentry/api/endpoints/project_agnostic_rule_conditions.py b/src/sentry/api/endpoints/project_agnostic_rule_conditions.py index 398229980e377e..2a38d4de25738d 100644 --- a/src/sentry/api/endpoints/project_agnostic_rule_conditions.py +++ b/src/sentry/api/endpoints/project_agnostic_rule_conditions.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.rules import rules @@ -8,6 +9,10 @@ @region_silo_endpoint class ProjectAgnosticRuleConditionsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: """ Retrieve the list of rule conditions diff --git a/src/sentry/api/endpoints/project_app_store_connect_credentials.py b/src/sentry/api/endpoints/project_app_store_connect_credentials.py index 3ddf0a06d95043..8233703d409d9e 100644 --- a/src/sentry/api/endpoints/project_app_store_connect_credentials.py +++ b/src/sentry/api/endpoints/project_app_store_connect_credentials.py @@ -1,4 +1,5 @@ from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint """Sentry API to manage the App Store Connect credentials for a project. @@ -82,6 +83,9 @@ class AppStoreConnectCredentialsSerializer(serializers.Serializer): @region_silo_endpoint class AppStoreConnectAppsEndpoint(ProjectEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } """Retrieves available applications with provided credentials. ``POST projects/{org_slug}/{proj_slug}/appstoreconnect/apps/`` @@ -198,6 +202,9 @@ class AppStoreCreateCredentialsSerializer(serializers.Serializer): @region_silo_endpoint class AppStoreConnectCreateCredentialsEndpoint(ProjectEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } """Returns all the App Store Connect symbol source settings ready to be saved. ``POST projects/{org_slug}/{proj_slug}/appstoreconnect/`` @@ -273,6 +280,9 @@ def validate_appconnectPrivateKey( @region_silo_endpoint class AppStoreConnectUpdateCredentialsEndpoint(ProjectEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } """Updates a subset of the existing credentials. ``POST projects/{org_slug}/{proj_slug}/appstoreconnect/{id}/`` @@ -336,6 +346,9 @@ def post(self, request: Request, project: Project, credentials_id: str) -> Respo @region_silo_endpoint class AppStoreConnectRefreshEndpoint(ProjectEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } """Triggers an immediate check for new App Store Connect builds. ``POST projects/{org_slug}/{proj_slug}/appstoreconnect/{id}/refresh/`` @@ -387,6 +400,9 @@ def post(self, request: Request, project: Project, credentials_id: str) -> Respo @region_silo_endpoint class AppStoreConnectStatusEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """Returns a summary of the project's App Store Connect configuration and builds. diff --git a/src/sentry/api/endpoints/project_artifact_bundle_file_details.py b/src/sentry/api/endpoints/project_artifact_bundle_file_details.py index 1242bb40c7f7af..d21e5b8934010b 100644 --- a/src/sentry/api/endpoints/project_artifact_bundle_file_details.py +++ b/src/sentry/api/endpoints/project_artifact_bundle_file_details.py @@ -8,6 +8,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.api.endpoints.debug_files import has_download_permission @@ -47,6 +48,9 @@ def download_file_from_artifact_bundle( class ProjectArtifactBundleFileDetailsEndpoint( ProjectEndpoint, ProjectArtifactBundleFileDetailsMixin ): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) def get(self, request: Request, project, bundle_id, file_id) -> Response: diff --git a/src/sentry/api/endpoints/project_artifact_bundle_files.py b/src/sentry/api/endpoints/project_artifact_bundle_files.py index e39d0703588247..a1013d0a9b3781 100644 --- a/src/sentry/api/endpoints/project_artifact_bundle_files.py +++ b/src/sentry/api/endpoints/project_artifact_bundle_files.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.api.paginator import ChainPaginator @@ -51,6 +52,9 @@ def __getitem__(self, range): @region_silo_endpoint class ProjectArtifactBundleFilesEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) rate_limits = RateLimitConfig( group="CLI", limit_overrides={"GET": SENTRY_RATELIMITER_GROUP_DEFAULTS["default"]} diff --git a/src/sentry/api/endpoints/project_commits.py b/src/sentry/api/endpoints/project_commits.py index 8e0a69b8744f1e..b802f913f5926d 100644 --- a/src/sentry/api/endpoints/project_commits.py +++ b/src/sentry/api/endpoints/project_commits.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.api.paginator import OffsetPaginator @@ -10,6 +11,9 @@ @region_silo_endpoint class ProjectCommitsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) diff --git a/src/sentry/api/endpoints/project_create_sample.py b/src/sentry/api/endpoints/project_create_sample.py index 68cdfc034711fa..9973c03078c395 100644 --- a/src/sentry/api/endpoints/project_create_sample.py +++ b/src/sentry/api/endpoints/project_create_sample.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectEventPermission from sentry.api.serializers import serialize @@ -12,6 +13,9 @@ @region_silo_endpoint class ProjectCreateSampleEndpoint(ProjectEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } # Members should be able to create sample events. # This is the same scope that allows members to view all issues for a project. permission_classes = (ProjectEventPermission,) diff --git a/src/sentry/api/endpoints/project_create_sample_transaction.py b/src/sentry/api/endpoints/project_create_sample_transaction.py index 6a9a8d24d0817f..72d82f97b95721 100644 --- a/src/sentry/api/endpoints/project_create_sample_transaction.py +++ b/src/sentry/api/endpoints/project_create_sample_transaction.py @@ -6,6 +6,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectEventPermission from sentry.api.serializers import serialize @@ -66,6 +67,9 @@ def fix_event_data(data): @region_silo_endpoint class ProjectCreateSampleTransactionEndpoint(ProjectEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } # Members should be able to create sample events. # This is the same scope that allows members to view all issues for a project. permission_classes = (ProjectEventPermission,) diff --git a/src/sentry/api/endpoints/project_details.py b/src/sentry/api/endpoints/project_details.py index 11cb80b3e71c34..55db34420731f1 100644 --- a/src/sentry/api/endpoints/project_details.py +++ b/src/sentry/api/endpoints/project_details.py @@ -12,6 +12,7 @@ from rest_framework.response import Response from sentry import audit_log, features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import ( DEFAULT_SLUG_ERROR_MESSAGE, DEFAULT_SLUG_PATTERN, @@ -382,6 +383,11 @@ class RelaxedProjectPermission(ProjectPermission): @extend_schema(tags=["Projects"]) @region_silo_endpoint class ProjectDetailsEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.PUBLIC, + "GET": ApiPublishStatus.PUBLIC, + "PUT": ApiPublishStatus.PUBLIC, + } public = {"GET", "PUT", "DELETE"} permission_classes = [RelaxedProjectPermission] diff --git a/src/sentry/api/endpoints/project_docs_platform.py b/src/sentry/api/endpoints/project_docs_platform.py index 07d906be77ffb5..5081b94e9b2be1 100644 --- a/src/sentry/api/endpoints/project_docs_platform.py +++ b/src/sentry/api/endpoints/project_docs_platform.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -38,6 +39,10 @@ def replace_keys(html, project_key): @region_silo_endpoint class ProjectDocsPlatformEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, platform) -> Response: data = load_doc(platform) if not data: diff --git a/src/sentry/api/endpoints/project_dynamic_sampling.py b/src/sentry/api/endpoints/project_dynamic_sampling.py index e04408032ca42c..196441bfe40cf0 100644 --- a/src/sentry/api/endpoints/project_dynamic_sampling.py +++ b/src/sentry/api/endpoints/project_dynamic_sampling.py @@ -11,6 +11,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectPermission from sentry.dynamic_sampling.rules.base import get_guarded_blended_sample_rate @@ -48,6 +49,9 @@ class DynamicSamplingPermission(ProjectPermission): @region_silo_endpoint class ProjectDynamicSamplingRateEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.TELEMETRY_EXPERIENCE permission_classes = (DynamicSamplingReadPermission,) @@ -69,6 +73,9 @@ def get(self, request: Request, project: Project) -> Response: @region_silo_endpoint class ProjectDynamicSamplingDistributionEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.TELEMETRY_EXPERIENCE permission_classes = (DynamicSamplingPermission,) diff --git a/src/sentry/api/endpoints/project_environment_details.py b/src/sentry/api/endpoints/project_environment_details.py index fa7bd6ee9f3add..1ab1920d9874b4 100644 --- a/src/sentry/api/endpoints/project_environment_details.py +++ b/src/sentry/api/endpoints/project_environment_details.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -15,6 +16,11 @@ class ProjectEnvironmentSerializer(serializers.Serializer): @region_silo_endpoint class ProjectEnvironmentDetailsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, environment) -> Response: try: instance = EnvironmentProject.objects.select_related("environment").get( diff --git a/src/sentry/api/endpoints/project_environments.py b/src/sentry/api/endpoints/project_environments.py index a008758e54954a..ff5254989b4032 100644 --- a/src/sentry/api/endpoints/project_environments.py +++ b/src/sentry/api/endpoints/project_environments.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.helpers.environments import environment_visibility_filter_options @@ -10,6 +11,10 @@ @region_silo_endpoint class ProjectEnvironmentsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: """ List a Project's Environments diff --git a/src/sentry/api/endpoints/project_event_details.py b/src/sentry/api/endpoints/project_event_details.py index 9f844a3593a55d..e4e5d046193358 100644 --- a/src/sentry/api/endpoints/project_event_details.py +++ b/src/sentry/api/endpoints/project_event_details.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import eventstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.serializers import IssueEventSerializer, serialize @@ -50,6 +51,10 @@ def wrap_event_response( @region_silo_endpoint class ProjectEventDetailsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, event_id) -> Response: """ Retrieve an Event for a Project @@ -93,6 +98,10 @@ def get(self, request: Request, project, event_id) -> Response: @region_silo_endpoint class EventJsonEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, event_id) -> Response: event = eventstore.backend.get_event_by_id(project.id, event_id) diff --git a/src/sentry/api/endpoints/project_events.py b/src/sentry/api/endpoints/project_events.py index 70c3c07c9cefb4..7c8382b5c17c6a 100644 --- a/src/sentry/api/endpoints/project_events.py +++ b/src/sentry/api/endpoints/project_events.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from sentry import eventstore, features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.serializers import EventSerializer, SimpleEventSerializer, serialize @@ -14,6 +15,9 @@ @region_silo_endpoint class ProjectEventsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } enforce_rate_limit = True rate_limits = { "GET": { diff --git a/src/sentry/api/endpoints/project_filter_details.py b/src/sentry/api/endpoints/project_filter_details.py index 06635058664a1e..fdd84a00528247 100644 --- a/src/sentry/api/endpoints/project_filter_details.py +++ b/src/sentry/api/endpoints/project_filter_details.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -23,6 +24,9 @@ @extend_schema(tags=["Projects"]) @region_silo_endpoint class ProjectFilterDetailsEndpoint(ProjectEndpoint): + publish_status = { + "PUT": ApiPublishStatus.PUBLIC, + } public = {"PUT"} @extend_schema( diff --git a/src/sentry/api/endpoints/project_filters.py b/src/sentry/api/endpoints/project_filters.py index 0f13a2fd776b06..899e28cac08d13 100644 --- a/src/sentry/api/endpoints/project_filters.py +++ b/src/sentry/api/endpoints/project_filters.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.ingest import inbound_filters @@ -8,6 +9,10 @@ @region_silo_endpoint class ProjectFiltersEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: """ List a project's filters diff --git a/src/sentry/api/endpoints/project_group_index.py b/src/sentry/api/endpoints/project_group_index.py index 449b7db50b79d3..a40291ed6533ea 100644 --- a/src/sentry/api/endpoints/project_group_index.py +++ b/src/sentry/api/endpoints/project_group_index.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import analytics, eventstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectEventPermission from sentry.api.helpers.group_index import ( @@ -27,6 +28,11 @@ @region_silo_endpoint class ProjectGroupIndexEndpoint(ProjectEndpoint, EnvironmentMixin): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectEventPermission,) enforce_rate_limit = True diff --git a/src/sentry/api/endpoints/project_group_stats.py b/src/sentry/api/endpoints/project_group_stats.py index 256299845d2b26..cd5abb099af593 100644 --- a/src/sentry/api/endpoints/project_group_stats.py +++ b/src/sentry/api/endpoints/project_group_stats.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import tsdb +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, StatsMixin, region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -12,6 +13,9 @@ @region_silo_endpoint class ProjectGroupStatsEndpoint(ProjectEndpoint, EnvironmentMixin, StatsMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } enforce_rate_limit = True rate_limits = { "GET": { diff --git a/src/sentry/api/endpoints/project_grouping_configs.py b/src/sentry/api/endpoints/project_grouping_configs.py index 22c54d4c779fc6..7f1fd8636f32fe 100644 --- a/src/sentry/api/endpoints/project_grouping_configs.py +++ b/src/sentry/api/endpoints/project_grouping_configs.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import ProjectEndpoint from sentry.api.serializers import serialize @@ -9,6 +10,9 @@ @region_silo_endpoint class ProjectGroupingConfigsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """Retrieve available grouping configs with project-specific information See GroupingConfigsEndpoint diff --git a/src/sentry/api/endpoints/project_index.py b/src/sentry/api/endpoints/project_index.py index 72f98b178e47f8..a37ab73427a7fd 100644 --- a/src/sentry/api/endpoints/project_index.py +++ b/src/sentry/api/endpoints/project_index.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.bases.project import ProjectPermission from sentry.api.paginator import DateTimePaginator @@ -18,6 +19,9 @@ @region_silo_endpoint class ProjectIndexEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/project_issues_resolved_in_release.py b/src/sentry/api/endpoints/project_issues_resolved_in_release.py index 46d091c406012c..81b340370c595f 100644 --- a/src/sentry/api/endpoints/project_issues_resolved_in_release.py +++ b/src/sentry/api/endpoints/project_issues_resolved_in_release.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectPermission from sentry.api.helpers.releases import get_group_ids_resolved_in_release @@ -11,6 +12,9 @@ @region_silo_endpoint class ProjectIssuesResolvedInReleaseEndpoint(ProjectEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectPermission,) def get(self, request: Request, project, version) -> Response: diff --git a/src/sentry/api/endpoints/project_key_details.py b/src/sentry/api/endpoints/project_key_details.py index ca9936fef9ec1e..267ed764e5f017 100644 --- a/src/sentry/api/endpoints/project_key_details.py +++ b/src/sentry/api/endpoints/project_key_details.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import audit_log, features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -26,6 +27,11 @@ @extend_schema(tags=["Projects"]) @region_silo_endpoint class ProjectKeyDetailsEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.PUBLIC, + "GET": ApiPublishStatus.PUBLIC, + "PUT": ApiPublishStatus.PUBLIC, + } public = {"GET", "PUT", "DELETE"} @extend_schema( diff --git a/src/sentry/api/endpoints/project_key_stats.py b/src/sentry/api/endpoints/project_key_stats.py index b1c7cceaa8277c..4feaa77726c534 100644 --- a/src/sentry/api/endpoints/project_key_stats.py +++ b/src/sentry/api/endpoints/project_key_stats.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry_sdk.api import capture_exception +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import StatsMixin, region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -20,6 +21,9 @@ @region_silo_endpoint class ProjectKeyStatsEndpoint(ProjectEndpoint, StatsMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } enforce_rate_limit = True rate_limits = { "GET": { diff --git a/src/sentry/api/endpoints/project_keys.py b/src/sentry/api/endpoints/project_keys.py index 805736a044bd45..24f196807c59de 100644 --- a/src/sentry/api/endpoints/project_keys.py +++ b/src/sentry/api/endpoints/project_keys.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import audit_log, features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.serializers import serialize @@ -26,6 +27,10 @@ @extend_schema(tags=["Projects"]) @region_silo_endpoint class ProjectKeysEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.PUBLIC, + "POST": ApiPublishStatus.PUBLIC, + } public = {"GET", "POST"} @extend_schema( diff --git a/src/sentry/api/endpoints/project_member_index.py b/src/sentry/api/endpoints/project_member_index.py index a630721270ccca..5989c83fa0d2df 100644 --- a/src/sentry/api/endpoints/project_member_index.py +++ b/src/sentry/api/endpoints/project_member_index.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.serializers import serialize @@ -10,6 +11,10 @@ @region_silo_endpoint class ProjectMemberIndexEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: queryset = OrganizationMember.objects.filter( Q(user_is_active=True, user_id__isnull=False) | Q(user_id__isnull=True), diff --git a/src/sentry/api/endpoints/project_ownership.py b/src/sentry/api/endpoints/project_ownership.py index 9f07ccf4f3b5f3..9a8d9d578f6b07 100644 --- a/src/sentry/api/endpoints/project_ownership.py +++ b/src/sentry/api/endpoints/project_ownership.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import audit_log, features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectOwnershipPermission from sentry.api.serializers import serialize @@ -159,6 +160,10 @@ def __modify_auto_assignment(self, ownership): @region_silo_endpoint class ProjectOwnershipEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = [ProjectOwnershipPermission] def get_ownership(self, project): @@ -262,7 +267,7 @@ def put(self, request: Request, project) -> Response: actor=request.user, organization=project.organization, target_object=project.id, - event=audit_log.get_event_id("PROJECT_EDIT"), + event=audit_log.get_event_id("PROJECT_OWNERSHIPRULE_EDIT"), data={**change_data, **project.get_audit_log_data()}, ) ownership_rule_created.send_robust(project=project, sender=self.__class__) diff --git a/src/sentry/api/endpoints/project_performance_issue_settings.py b/src/sentry/api/endpoints/project_performance_issue_settings.py index 31aa6be5687cb4..7de39849de9fef 100644 --- a/src/sentry/api/endpoints/project_performance_issue_settings.py +++ b/src/sentry/api/endpoints/project_performance_issue_settings.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from sentry import audit_log, features, projectoptions +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectSettingPermission from sentry.api.permissions import SuperuserPermission @@ -167,6 +168,11 @@ def get_disabled_threshold_options(payload, current_settings): @region_silo_endpoint class ProjectPerformanceIssueSettingsEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectOwnerOrSuperUserPermissions,) def has_feature(self, project, request) -> bool: diff --git a/src/sentry/api/endpoints/project_platforms.py b/src/sentry/api/endpoints/project_platforms.py index 29d3363f735e8f..1d97f0e955b06c 100644 --- a/src/sentry/api/endpoints/project_platforms.py +++ b/src/sentry/api/endpoints/project_platforms.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.serializers import serialize @@ -9,6 +10,10 @@ @region_silo_endpoint class ProjectPlatformsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: queryset = ProjectPlatform.objects.filter(project_id=project.id) return Response(serialize(list(queryset), request.user)) diff --git a/src/sentry/api/endpoints/project_plugin_details.py b/src/sentry/api/endpoints/project_plugin_details.py index 65f0a19c2e4f30..e3f116399d80d1 100644 --- a/src/sentry/api/endpoints/project_plugin_details.py +++ b/src/sentry/api/endpoints/project_plugin_details.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -28,6 +29,13 @@ @region_silo_endpoint class ProjectPluginDetailsEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } + def _get_plugin(self, plugin_id): try: return plugins.get(plugin_id) diff --git a/src/sentry/api/endpoints/project_plugins.py b/src/sentry/api/endpoints/project_plugins.py index 11f4932a4d21c6..55e46f621dca07 100644 --- a/src/sentry/api/endpoints/project_plugins.py +++ b/src/sentry/api/endpoints/project_plugins.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.serializers import serialize @@ -10,6 +11,10 @@ @region_silo_endpoint class ProjectPluginsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: context = serialize( [plugin for plugin in plugins.configurable_for_project(project, version=None)], diff --git a/src/sentry/api/endpoints/project_processingissues.py b/src/sentry/api/endpoints/project_processingissues.py index d0a2532e139f9e..1f3ed23cc8da1a 100644 --- a/src/sentry/api/endpoints/project_processingissues.py +++ b/src/sentry/api/endpoints/project_processingissues.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.helpers.processing_issues import get_processing_issues @@ -11,6 +12,10 @@ @region_silo_endpoint class ProjectProcessingIssuesDiscardEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + } + def delete(self, request: Request, project) -> Response: """ This discards all open processing issues @@ -21,6 +26,11 @@ def delete(self, request: Request, project) -> Response: @region_silo_endpoint class ProjectProcessingIssuesEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: """ List a project's processing issues. diff --git a/src/sentry/api/endpoints/project_profiling_profile.py b/src/sentry/api/endpoints/project_profiling_profile.py index daa56f3d17c992..65af8e4d67d649 100644 --- a/src/sentry/api/endpoints/project_profiling_profile.py +++ b/src/sentry/api/endpoints/project_profiling_profile.py @@ -9,6 +9,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.paginator import GenericOffsetPaginator @@ -69,6 +70,10 @@ def get(self, request: Request, project: Project) -> Response: @region_silo_endpoint class ProjectProfilingTransactionIDProfileIDEndpoint(ProjectProfilingBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project: Project, transaction_id: str) -> HttpResponse: if not features.has("organizations:profiling", project.organization, actor=request.user): return Response(status=404) @@ -81,6 +86,10 @@ def get(self, request: Request, project: Project, transaction_id: str) -> HttpRe @region_silo_endpoint class ProjectProfilingProfileEndpoint(ProjectProfilingBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project: Project, profile_id: str) -> HttpResponse: if not features.has("organizations:profiling", project.organization, actor=request.user): return Response(status=404) @@ -128,6 +137,10 @@ def get_release(project: Project, version: str) -> Any: @region_silo_endpoint class ProjectProfilingRawProfileEndpoint(ProjectProfilingBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project: Project, profile_id: str) -> HttpResponse: if not features.has("organizations:profiling", project.organization, actor=request.user): return Response(status=404) @@ -140,6 +153,10 @@ def get(self, request: Request, project: Project, profile_id: str) -> HttpRespon @region_silo_endpoint class ProjectProfilingFlamegraphEndpoint(ProjectProfilingBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project: Project) -> HttpResponse: if not features.has("organizations:profiling", project.organization, actor=request.user): return Response(status=404) @@ -154,6 +171,9 @@ def get(self, request: Request, project: Project) -> HttpResponse: @region_silo_endpoint class ProjectProfilingFunctionsEndpoint(ProjectProfilingPaginatedBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } DEFAULT_PER_PAGE = 5 MAX_PER_PAGE = 50 @@ -208,6 +228,10 @@ def validate(self, data): @region_silo_endpoint class ProjectProfilingEventEndpoint(ProjectProfilingBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def convert_args(self, request: Request, *args, **kwargs): # disables the auto conversion of project slug inherited from the # project endpoint since this takes the project id instead of the slug diff --git a/src/sentry/api/endpoints/project_release_commits.py b/src/sentry/api/endpoints/project_release_commits.py index b3937e19f62f06..372caadaa54349 100644 --- a/src/sentry/api/endpoints/project_release_commits.py +++ b/src/sentry/api/endpoints/project_release_commits.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.api.exceptions import ResourceDoesNotExist @@ -10,6 +11,9 @@ @region_silo_endpoint class ProjectReleaseCommitsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) def get(self, request: Request, project, version) -> Response: diff --git a/src/sentry/api/endpoints/project_release_details.py b/src/sentry/api/endpoints/project_release_details.py index 7573bb854e2766..f78747837e2c93 100644 --- a/src/sentry/api/endpoints/project_release_details.py +++ b/src/sentry/api/endpoints/project_release_details.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import ReleaseAnalyticsMixin, region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.api.endpoints.organization_releases import get_stats_period_detail @@ -18,6 +19,11 @@ @region_silo_endpoint class ProjectReleaseDetailsEndpoint(ProjectEndpoint, ReleaseAnalyticsMixin): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) def get(self, request: Request, project, version) -> Response: diff --git a/src/sentry/api/endpoints/project_release_file_details.py b/src/sentry/api/endpoints/project_release_file_details.py index 64774a281155d2..d9db86895d384b 100644 --- a/src/sentry/api/endpoints/project_release_file_details.py +++ b/src/sentry/api/endpoints/project_release_file_details.py @@ -8,6 +8,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.api.endpoints.debug_files import has_download_permission @@ -201,6 +202,11 @@ def delete_releasefile(cls, release, file_id): @region_silo_endpoint class ProjectReleaseFileDetailsEndpoint(ProjectEndpoint, ReleaseFileDetailsMixin): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) def get(self, request: Request, project, version, file_id) -> Response: diff --git a/src/sentry/api/endpoints/project_release_files.py b/src/sentry/api/endpoints/project_release_files.py index bc4053b4cede74..f1dc59fa9878b4 100644 --- a/src/sentry/api/endpoints/project_release_files.py +++ b/src/sentry/api/endpoints/project_release_files.py @@ -8,6 +8,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.api.exceptions import ResourceDoesNotExist @@ -223,6 +224,10 @@ def pseudo_releasefile(url, info, dist): @region_silo_endpoint class ProjectReleaseFilesEndpoint(ProjectEndpoint, ReleaseFilesMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) rate_limits = RateLimitConfig( group="CLI", limit_overrides={"GET": SENTRY_RATELIMITER_GROUP_DEFAULTS["default"]} diff --git a/src/sentry/api/endpoints/project_release_repositories.py b/src/sentry/api/endpoints/project_release_repositories.py index 6e604ea8120edc..51a0cf1a9f03bd 100644 --- a/src/sentry/api/endpoints/project_release_repositories.py +++ b/src/sentry/api/endpoints/project_release_repositories.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.api.exceptions import ResourceDoesNotExist @@ -10,6 +11,9 @@ @region_silo_endpoint class ProjectReleaseRepositories(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) diff --git a/src/sentry/api/endpoints/project_release_setup.py b/src/sentry/api/endpoints/project_release_setup.py index 0853999091a563..3660c13796fd62 100644 --- a/src/sentry/api/endpoints/project_release_setup.py +++ b/src/sentry/api/endpoints/project_release_setup.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.models import Deploy, Group, ReleaseCommit, ReleaseProject, Repository @@ -10,6 +11,9 @@ @region_silo_endpoint class ProjectReleaseSetupCompletionEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) def get(self, request: Request, project) -> Response: diff --git a/src/sentry/api/endpoints/project_release_stats.py b/src/sentry/api/endpoints/project_release_stats.py index 475f3d3200d6f3..6f5ef66a3eb497 100644 --- a/src/sentry/api/endpoints/project_release_stats.py +++ b/src/sentry/api/endpoints/project_release_stats.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import release_health +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectEventsError, ProjectReleasePermission from sentry.api.exceptions import ResourceDoesNotExist @@ -25,6 +26,9 @@ def upsert_missing_release(project, version): @region_silo_endpoint class ProjectReleaseStatsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) def get(self, request: Request, project, version) -> Response: diff --git a/src/sentry/api/endpoints/project_releases.py b/src/sentry/api/endpoints/project_releases.py index b9ef3b75cb3c0f..cc04b8bf683f3a 100644 --- a/src/sentry/api/endpoints/project_releases.py +++ b/src/sentry/api/endpoints/project_releases.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from sentry import analytics +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.api.paginator import OffsetPaginator @@ -23,6 +24,10 @@ @region_silo_endpoint class ProjectReleasesEndpoint(ProjectEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) rate_limits = RateLimitConfig( group="CLI", limit_overrides={"GET": SENTRY_RATELIMITER_GROUP_DEFAULTS["default"]} diff --git a/src/sentry/api/endpoints/project_releases_token.py b/src/sentry/api/endpoints/project_releases_token.py index be2ea8baaa585a..912605dd1692da 100644 --- a/src/sentry/api/endpoints/project_releases_token.py +++ b/src/sentry/api/endpoints/project_releases_token.py @@ -6,6 +6,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, StrictProjectPermission from sentry.models import ProjectOption @@ -36,6 +37,10 @@ def _get_signature(project_id, plugin_id, token): @region_silo_endpoint class ProjectReleasesTokenEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (StrictProjectPermission,) def _regenerate_token(self, project): diff --git a/src/sentry/api/endpoints/project_repo_path_parsing.py b/src/sentry/api/endpoints/project_repo_path_parsing.py index 9b9b90c60dda0f..9064b645751175 100644 --- a/src/sentry/api/endpoints/project_repo_path_parsing.py +++ b/src/sentry/api/endpoints/project_repo_path_parsing.py @@ -3,6 +3,7 @@ from rest_framework.response import Response from sentry import integrations +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectPermission from sentry.api.serializers.rest_framework.base import CamelSnakeSerializer @@ -94,6 +95,9 @@ def repo_match(repo: Repository): class ProjectRepoPathParsingEndpointLoosePermission(ProjectPermission): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } """ Similar to the code_mappings endpoint, loosen permissions to all users """ diff --git a/src/sentry/api/endpoints/project_reprocessing.py b/src/sentry/api/endpoints/project_reprocessing.py index 8d6659c34df790..bd18a30d2af4e7 100644 --- a/src/sentry/api/endpoints/project_reprocessing.py +++ b/src/sentry/api/endpoints/project_reprocessing.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.reprocessing import trigger_reprocessing @@ -8,6 +9,9 @@ @region_silo_endpoint class ProjectReprocessingEndpoint(ProjectEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectReleasePermission,) def post(self, request: Request, project) -> Response: diff --git a/src/sentry/api/endpoints/project_rule_actions.py b/src/sentry/api/endpoints/project_rule_actions.py index e867b2f9dea71c..1ca7ffe664bdbe 100644 --- a/src/sentry/api/endpoints/project_rule_actions.py +++ b/src/sentry/api/endpoints/project_rule_actions.py @@ -3,6 +3,7 @@ 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 region_silo_endpoint from sentry.api.bases import ProjectAlertRulePermission, ProjectEndpoint from sentry.api.serializers.rest_framework import RuleActionSerializer @@ -15,6 +16,9 @@ @region_silo_endpoint class ProjectRuleActionsEndpoint(ProjectEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ISSUES permission_classes = (ProjectAlertRulePermission,) diff --git a/src/sentry/api/endpoints/project_rule_details.py b/src/sentry/api/endpoints/project_rule_details.py index 6855d8f84325f0..87256640d0eeff 100644 --- a/src/sentry/api/endpoints/project_rule_details.py +++ b/src/sentry/api/endpoints/project_rule_details.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.rule import RuleEndpoint from sentry.api.endpoints.project_rules import find_duplicate_rule @@ -34,6 +35,12 @@ @region_silo_endpoint class ProjectRuleDetailsEndpoint(RuleEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + @transaction_start("ProjectRuleDetailsEndpoint") def get(self, request: Request, project, rule) -> Response: """ diff --git a/src/sentry/api/endpoints/project_rule_enable.py b/src/sentry/api/endpoints/project_rule_enable.py index 95a0cabb3a489c..52197569d64911 100644 --- a/src/sentry/api/endpoints/project_rule_enable.py +++ b/src/sentry/api/endpoints/project_rule_enable.py @@ -4,6 +4,7 @@ from sentry import audit_log from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectAlertRulePermission, ProjectEndpoint from sentry.api.endpoints.project_rules import find_duplicate_rule @@ -14,6 +15,9 @@ @region_silo_endpoint class ProjectRuleEnableEndpoint(ProjectEndpoint): + publish_status = { + "PUT": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ISSUES permission_classes = (ProjectAlertRulePermission,) diff --git a/src/sentry/api/endpoints/project_rule_preview.py b/src/sentry/api/endpoints/project_rule_preview.py index 135eb7f7f7e31c..4af82eaf05fa5c 100644 --- a/src/sentry/api/endpoints/project_rule_preview.py +++ b/src/sentry/api/endpoints/project_rule_preview.py @@ -5,6 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectAlertRulePermission, ProjectEndpoint from sentry.api.serializers import GroupSerializer, serialize @@ -16,6 +17,9 @@ @region_silo_endpoint class ProjectRulePreviewEndpoint(ProjectEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectAlertRulePermission,) diff --git a/src/sentry/api/endpoints/project_rule_task_details.py b/src/sentry/api/endpoints/project_rule_task_details.py index de35e8778659b3..9c7b0930e5c4f9 100644 --- a/src/sentry/api/endpoints/project_rule_task_details.py +++ b/src/sentry/api/endpoints/project_rule_task_details.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectSettingPermission from sentry.api.serializers import serialize @@ -12,6 +13,9 @@ @region_silo_endpoint class ProjectRuleTaskDetailsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = [ProjectSettingPermission] def get(self, request: Request, project, task_uuid) -> Response: diff --git a/src/sentry/api/endpoints/project_rules.py b/src/sentry/api/endpoints/project_rules.py index 5060240b12cd9c..23a42f2b612e0d 100644 --- a/src/sentry/api/endpoints/project_rules.py +++ b/src/sentry/api/endpoints/project_rules.py @@ -7,6 +7,7 @@ from sentry import audit_log, features from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectAlertRulePermission, ProjectEndpoint from sentry.api.serializers import serialize @@ -35,7 +36,9 @@ def pre_save_rule(instance, sender, *args, **kwargs): def find_duplicate_rule(rule_data, project, rule_id=None): - matchers = [key for key in list(rule_data.keys()) if key not in ("name", "user_id")] + matchers = {key for key in list(rule_data.keys()) if key not in ("name", "user_id")} + extra_fields = ["actions", "environment"] + matchers.update(extra_fields) existing_rules = Rule.objects.exclude(id=rule_id).filter( project=project, status=ObjectStatus.ACTIVE ) @@ -45,9 +48,18 @@ def find_duplicate_rule(rule_data, project, rule_id=None): for matcher in matchers: if existing_rule.data.get(matcher) and rule_data.get(matcher): keys += 1 + if existing_rule.data[matcher] == rule_data[matcher]: matches += 1 + elif matcher in extra_fields: + if not existing_rule.data.get(matcher) and not rule_data.get(matcher): + # neither rule has the matcher + continue + else: + # one rule has the matcher and the other one doesn't + keys += 1 + if keys == matches: return existing_rule return None @@ -55,6 +67,10 @@ def find_duplicate_rule(rule_data, project, rule_id=None): @region_silo_endpoint class ProjectRulesEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ISSUES permission_classes = (ProjectAlertRulePermission,) diff --git a/src/sentry/api/endpoints/project_rules_configuration.py b/src/sentry/api/endpoints/project_rules_configuration.py index 10c2274d7e128a..065792e833671a 100644 --- a/src/sentry/api/endpoints/project_rules_configuration.py +++ b/src/sentry/api/endpoints/project_rules_configuration.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.constants import MIGRATED_CONDITIONS, SENTRY_APP_ACTIONS, TICKET_ACTIONS @@ -10,6 +11,10 @@ @region_silo_endpoint class ProjectRulesConfigurationEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: """ Retrieve the list of configuration options for a given project. @@ -23,6 +28,7 @@ def get(self, request: Request, project) -> Response: can_create_tickets = features.has( "organizations:integrations-ticket-rules", project.organization ) + has_issue_severity_alerts = features.has("projects:first-event-severity-alerting", project) # TODO: conditions need to be based on actions for rule_type, rule_cls in rules: @@ -64,6 +70,11 @@ def get(self, request: Request, project) -> Response: if rule_type.startswith("condition/"): condition_list.append(context) elif rule_type.startswith("filter/"): + if ( + context["id"] == "sentry.rules.filters.issue_severity.IssueSeverityFilter" + and not has_issue_severity_alerts + ): + continue filter_list.append(context) elif rule_type.startswith("action/"): action_list.append(context) diff --git a/src/sentry/api/endpoints/project_servicehook_details.py b/src/sentry/api/endpoints/project_servicehook_details.py index d866e5a4822336..2b8d7dbb0aa3bd 100644 --- a/src/sentry/api/endpoints/project_servicehook_details.py +++ b/src/sentry/api/endpoints/project_servicehook_details.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -15,6 +16,12 @@ @region_silo_endpoint class ProjectServiceHookDetailsEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, hook_id) -> Response: """ Retrieve a Service Hook diff --git a/src/sentry/api/endpoints/project_servicehook_stats.py b/src/sentry/api/endpoints/project_servicehook_stats.py index a6647211508aa6..d4ac2e3f4e013a 100644 --- a/src/sentry/api/endpoints/project_servicehook_stats.py +++ b/src/sentry/api/endpoints/project_servicehook_stats.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import tsdb +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import StatsMixin, region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -11,6 +12,10 @@ @region_silo_endpoint class ProjectServiceHookStatsEndpoint(ProjectEndpoint, StatsMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, hook_id) -> Response: try: hook = ServiceHook.objects.get(project_id=project.id, guid=hook_id) diff --git a/src/sentry/api/endpoints/project_servicehooks.py b/src/sentry/api/endpoints/project_servicehooks.py index 90e436bd3bc0f0..e2765a80aa2491 100644 --- a/src/sentry/api/endpoints/project_servicehooks.py +++ b/src/sentry/api/endpoints/project_servicehooks.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import audit_log, features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.serializers import serialize @@ -16,6 +17,11 @@ @region_silo_endpoint class ProjectServiceHooksEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } + def has_feature(self, request: Request, project): return features.has("projects:servicehooks", project=project, actor=request.user) diff --git a/src/sentry/api/endpoints/project_stacktrace_link.py b/src/sentry/api/endpoints/project_stacktrace_link.py index 389b114c70ae8a..00cf9055f27402 100644 --- a/src/sentry/api/endpoints/project_stacktrace_link.py +++ b/src/sentry/api/endpoints/project_stacktrace_link.py @@ -7,6 +7,7 @@ from sentry import analytics from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.serializers import serialize @@ -186,6 +187,9 @@ def get_code_mapping_configs(project: Project) -> List[RepositoryProjectPathConf @region_silo_endpoint class ProjectStacktraceLinkEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """ Returns valid links for source code providers so that users can go from the file in the stack trace to the diff --git a/src/sentry/api/endpoints/project_stacktrace_links.py b/src/sentry/api/endpoints/project_stacktrace_links.py index ea9e704a43e325..4c94f8cc5d20d0 100644 --- a/src/sentry/api/endpoints/project_stacktrace_links.py +++ b/src/sentry/api/endpoints/project_stacktrace_links.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.integrations.base import IntegrationInstallation @@ -28,6 +29,9 @@ class StacktraceLinksSerializer(serializers.Serializer): @region_silo_endpoint class ProjectStacktraceLinksEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """ Returns valid links for source code providers so that users can go from files in the stack trace to the diff --git a/src/sentry/api/endpoints/project_stats.py b/src/sentry/api/endpoints/project_stats.py index 3a62a09d316caf..3279d573a02fa2 100644 --- a/src/sentry/api/endpoints/project_stats.py +++ b/src/sentry/api/endpoints/project_stats.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import tsdb +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, StatsMixin, region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -12,6 +13,10 @@ @region_silo_endpoint class ProjectStatsEndpoint(ProjectEndpoint, EnvironmentMixin, StatsMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: """ Retrieve Event Counts for a Project diff --git a/src/sentry/api/endpoints/project_tagkey_details.py b/src/sentry/api/endpoints/project_tagkey_details.py index 940c0318418f8a..d59ba52c899581 100644 --- a/src/sentry/api/endpoints/project_tagkey_details.py +++ b/src/sentry/api/endpoints/project_tagkey_details.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import audit_log, tagstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -13,6 +14,10 @@ @region_silo_endpoint class ProjectTagKeyDetailsEndpoint(ProjectEndpoint, EnvironmentMixin): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + } enforce_rate_limit = True rate_limits = { "DELETE": { diff --git a/src/sentry/api/endpoints/project_tagkey_values.py b/src/sentry/api/endpoints/project_tagkey_values.py index 55dbeb319c5876..a7345c9456a7ca 100644 --- a/src/sentry/api/endpoints/project_tagkey_values.py +++ b/src/sentry/api/endpoints/project_tagkey_values.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import tagstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -12,6 +13,10 @@ @region_silo_endpoint class ProjectTagKeyValuesEndpoint(ProjectEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, key) -> Response: """ List a Tag's Values diff --git a/src/sentry/api/endpoints/project_tags.py b/src/sentry/api/endpoints/project_tags.py index 65a159ebb088d6..2a5e70928c726f 100644 --- a/src/sentry/api/endpoints/project_tags.py +++ b/src/sentry/api/endpoints/project_tags.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import tagstore +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.constants import DS_DENYLIST, PROTECTED_TAG_KEYS @@ -10,6 +11,10 @@ @region_silo_endpoint class ProjectTagsEndpoint(ProjectEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: try: environment_id = self._get_environment_id_from_request(request, project.organization_id) diff --git a/src/sentry/api/endpoints/project_team_details.py b/src/sentry/api/endpoints/project_team_details.py index f92eeb0c0017cd..d3b293f9b3d3de 100644 --- a/src/sentry/api/endpoints/project_team_details.py +++ b/src/sentry/api/endpoints/project_team_details.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectPermission from sentry.api.exceptions import ResourceDoesNotExist @@ -25,6 +26,10 @@ class ProjectTeamsPermission(ProjectPermission): @extend_schema(tags=["Projects"]) @region_silo_endpoint class ProjectTeamDetailsEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.PUBLIC, + "POST": ApiPublishStatus.PUBLIC, + } public = {"POST", "DELETE"} permission_classes = (ProjectTeamsPermission,) diff --git a/src/sentry/api/endpoints/project_teams.py b/src/sentry/api/endpoints/project_teams.py index 6c2f48367fb596..2769147ce46c19 100644 --- a/src/sentry/api/endpoints/project_teams.py +++ b/src/sentry/api/endpoints/project_teams.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.paginator import OffsetPaginator @@ -10,6 +11,10 @@ @region_silo_endpoint class ProjectTeamsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: """ List a Project's Teams diff --git a/src/sentry/api/endpoints/project_transaction_names.py b/src/sentry/api/endpoints/project_transaction_names.py index 586cad2470a071..4668f76cf92bc8 100644 --- a/src/sentry/api/endpoints/project_transaction_names.py +++ b/src/sentry/api/endpoints/project_transaction_names.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.utils import get_date_range_from_stats_period @@ -15,6 +16,10 @@ @region_silo_endpoint class ProjectTransactionNamesCluster(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: """Run the transaction name clusterer and return its output. diff --git a/src/sentry/api/endpoints/project_transaction_threshold.py b/src/sentry/api/endpoints/project_transaction_threshold.py index 6109baafecb4fe..c50d6dbedd06f0 100644 --- a/src/sentry/api/endpoints/project_transaction_threshold.py +++ b/src/sentry/api/endpoints/project_transaction_threshold.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectSettingPermission from sentry.api.serializers import serialize @@ -41,6 +42,11 @@ def validate_threshold(self, threshold): @region_silo_endpoint class ProjectTransactionThresholdEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectSettingPermission,) def has_feature(self, project, request): diff --git a/src/sentry/api/endpoints/project_transaction_threshold_override.py b/src/sentry/api/endpoints/project_transaction_threshold_override.py index d931e85e5f352f..30f4688383d9a5 100644 --- a/src/sentry/api/endpoints/project_transaction_threshold_override.py +++ b/src/sentry/api/endpoints/project_transaction_threshold_override.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import ProjectTransactionThresholdOverridePermission from sentry.api.bases.organization_events import OrganizationEventsV2EndpointBase @@ -56,6 +57,11 @@ def validate(self, data): @region_silo_endpoint class ProjectTransactionThresholdOverrideEndpoint(OrganizationEventsV2EndpointBase): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectTransactionThresholdOverridePermission,) def get_project(self, request: Request, organization): diff --git a/src/sentry/api/endpoints/project_transfer.py b/src/sentry/api/endpoints/project_transfer.py index f9842f4f77352f..8eafc98894d2ba 100644 --- a/src/sentry/api/endpoints/project_transfer.py +++ b/src/sentry/api/endpoints/project_transfer.py @@ -8,6 +8,7 @@ from rest_framework.response import Response from sentry import audit_log, options, roles +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectPermission from sentry.api.decorators import sudo_required @@ -25,6 +26,9 @@ class RelaxedProjectPermission(ProjectPermission): @region_silo_endpoint class ProjectTransferEndpoint(ProjectEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = [RelaxedProjectPermission] @sudo_required diff --git a/src/sentry/api/endpoints/project_user_details.py b/src/sentry/api/endpoints/project_user_details.py index 19e7e3f4f8f893..88e1a1f6aec817 100644 --- a/src/sentry/api/endpoints/project_user_details.py +++ b/src/sentry/api/endpoints/project_user_details.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.serializers import serialize @@ -11,6 +12,11 @@ @region_silo_endpoint class ProjectUserDetailsEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, user_hash) -> Response: euser = EventUser.objects.get(project_id=project.id, hash=user_hash) return Response(serialize(euser, request.user)) diff --git a/src/sentry/api/endpoints/project_user_reports.py b/src/sentry/api/endpoints/project_user_reports.py index eb1561bc35a883..a99980e7aab459 100644 --- a/src/sentry/api/endpoints/project_user_reports.py +++ b/src/sentry/api/endpoints/project_user_reports.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.authentication import DSNAuthentication from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint @@ -20,6 +21,10 @@ class Meta: @region_silo_endpoint class ProjectUserReportsEndpoint(ProjectEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = ProjectEndpoint.authentication_classes + (DSNAuthentication,) def get(self, request: Request, project) -> Response: diff --git a/src/sentry/api/endpoints/project_user_stats.py b/src/sentry/api/endpoints/project_user_stats.py index c3490ec279957d..23a58748354794 100644 --- a/src/sentry/api/endpoints/project_user_stats.py +++ b/src/sentry/api/endpoints/project_user_stats.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import tsdb +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -14,6 +15,10 @@ @region_silo_endpoint class ProjectUserStatsEndpoint(EnvironmentMixin, ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: try: environment_id = self._get_environment_id_from_request(request, project.organization_id) diff --git a/src/sentry/api/endpoints/project_users.py b/src/sentry/api/endpoints/project_users.py index bd61a42382063c..8cdfc273cc2f0e 100644 --- a/src/sentry/api/endpoints/project_users.py +++ b/src/sentry/api/endpoints/project_users.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.paginator import DateTimePaginator @@ -10,6 +11,10 @@ @region_silo_endpoint class ProjectUsersEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: """ List a Project's Users diff --git a/src/sentry/api/endpoints/prompts_activity.py b/src/sentry/api/endpoints/prompts_activity.py index 8a8d29ed6fd236..3ac97ec8ce7761 100644 --- a/src/sentry/api/endpoints/prompts_activity.py +++ b/src/sentry/api/endpoints/prompts_activity.py @@ -9,6 +9,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.models import Organization, Project, PromptsActivity from sentry.utils.prompts import prompt_config @@ -33,6 +34,10 @@ def validate_feature(self, value): @region_silo_endpoint class PromptsActivityEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (IsAuthenticated,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/relay/details.py b/src/sentry/api/endpoints/relay/details.py index a8b5f88fef6cb1..589daa02581b25 100644 --- a/src/sentry/api/endpoints/relay/details.py +++ b/src/sentry/api/endpoints/relay/details.py @@ -2,6 +2,7 @@ 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, region_silo_endpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.permissions import SuperuserPermission @@ -10,6 +11,9 @@ @region_silo_endpoint class RelayDetailsEndpoint(Endpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) owner = ApiOwner.OWNERS_INGEST diff --git a/src/sentry/api/endpoints/relay/health_check.py b/src/sentry/api/endpoints/relay/health_check.py index 0d6e88c0c43540..73a94fa307a112 100644 --- a/src/sentry/api/endpoints/relay/health_check.py +++ b/src/sentry/api/endpoints/relay/health_check.py @@ -2,11 +2,15 @@ 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, region_silo_endpoint @region_silo_endpoint class RelayHealthCheck(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """ Endpoint checked by downstream Relay when a suspected network error is encountered. This endpoint doesn't do anything besides returning an Ok, and the downstream Relay diff --git a/src/sentry/api/endpoints/relay/index.py b/src/sentry/api/endpoints/relay/index.py index 0f7a5fe38ecb98..7da849a7201bd6 100644 --- a/src/sentry/api/endpoints/relay/index.py +++ b/src/sentry/api/endpoints/relay/index.py @@ -3,6 +3,7 @@ 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, region_silo_endpoint from sentry.api.paginator import OffsetPaginator from sentry.api.permissions import SuperuserPermission @@ -12,6 +13,9 @@ @region_silo_endpoint class RelayIndexEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) owner = ApiOwner.OWNERS_INGEST diff --git a/src/sentry/api/endpoints/relay/project_configs.py b/src/sentry/api/endpoints/relay/project_configs.py index 5548a37b46f7f9..fc1b30805a21af 100644 --- a/src/sentry/api/endpoints/relay/project_configs.py +++ b/src/sentry/api/endpoints/relay/project_configs.py @@ -8,6 +8,7 @@ from sentry_sdk import Hub, set_tag, start_span, start_transaction from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.authentication import RelayAuthentication from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.permissions import RelayPermission @@ -31,6 +32,9 @@ def _sample_apm(): @region_silo_endpoint class RelayProjectConfigsEndpoint(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.OWNERS_INGEST authentication_classes = (RelayAuthentication,) permission_classes = (RelayPermission,) diff --git a/src/sentry/api/endpoints/relay/project_ids.py b/src/sentry/api/endpoints/relay/project_ids.py index 5075d7e2d7d55b..b006aa302fee32 100644 --- a/src/sentry/api/endpoints/relay/project_ids.py +++ b/src/sentry/api/endpoints/relay/project_ids.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.authentication import RelayAuthentication from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.permissions import RelayPermission @@ -10,6 +11,9 @@ @region_silo_endpoint class RelayProjectIdsEndpoint(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = (RelayAuthentication,) permission_classes = (RelayPermission,) owner = ApiOwner.OWNERS_INGEST diff --git a/src/sentry/api/endpoints/relay/public_keys.py b/src/sentry/api/endpoints/relay/public_keys.py index b29da682ebb70b..8c28d4ae293972 100644 --- a/src/sentry/api/endpoints/relay/public_keys.py +++ b/src/sentry/api/endpoints/relay/public_keys.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.authentication import RelayAuthentication from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.permissions import RelayPermission @@ -10,6 +11,9 @@ @region_silo_endpoint class RelayPublicKeysEndpoint(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = (RelayAuthentication,) permission_classes = (RelayPermission,) enforce_rate_limit = False diff --git a/src/sentry/api/endpoints/relay/register_challenge.py b/src/sentry/api/endpoints/relay/register_challenge.py index 96ba86ecbefaa8..05845c8c067979 100644 --- a/src/sentry/api/endpoints/relay/register_challenge.py +++ b/src/sentry/api/endpoints/relay/register_challenge.py @@ -6,6 +6,7 @@ from sentry import options from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.authentication import is_internal_relay, is_static_relay, relay_from_id from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.endpoints.relay.constants import RELAY_AUTH_RATE_LIMITS @@ -22,6 +23,9 @@ class RelayRegisterChallengeSerializer(RelayIdSerializer): @region_silo_endpoint class RelayRegisterChallengeEndpoint(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = () permission_classes = () owner = ApiOwner.OWNERS_INGEST diff --git a/src/sentry/api/endpoints/relay/register_response.py b/src/sentry/api/endpoints/relay/register_response.py index 63a005be9748d3..13c546d6e06e73 100644 --- a/src/sentry/api/endpoints/relay/register_response.py +++ b/src/sentry/api/endpoints/relay/register_response.py @@ -7,6 +7,7 @@ from sentry import options from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.authentication import is_internal_relay, relay_from_id from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.endpoints.relay.constants import RELAY_AUTH_RATE_LIMITS @@ -24,6 +25,9 @@ class RelayRegisterResponseSerializer(RelayIdSerializer): @region_silo_endpoint class RelayRegisterResponseEndpoint(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.OWNERS_INGEST authentication_classes = () permission_classes = () diff --git a/src/sentry/api/endpoints/release_deploys.py b/src/sentry/api/endpoints/release_deploys.py index 8bcc6fc6a51bd0..05004f96fa85be 100644 --- a/src/sentry/api/endpoints/release_deploys.py +++ b/src/sentry/api/endpoints/release_deploys.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint from sentry.api.exceptions import ParameterValidationError, ResourceDoesNotExist @@ -32,6 +33,11 @@ def validate_environment(self, value): @region_silo_endpoint class ReleaseDeploysEndpoint(OrganizationReleasesBaseEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, version) -> Response: """ List a Release's Deploys diff --git a/src/sentry/api/endpoints/rpc.py b/src/sentry/api/endpoints/rpc.py index ea6746a5ec4c0b..7fccd99510be5b 100644 --- a/src/sentry/api/endpoints/rpc.py +++ b/src/sentry/api/endpoints/rpc.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.authentication import RpcSignatureAuthentication from sentry.api.base import Endpoint, all_silo_endpoint from sentry.services.hybrid_cloud.auth import AuthenticationContext @@ -19,6 +20,9 @@ @all_silo_endpoint class RpcServiceEndpoint(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.HYBRID_CLOUD authentication_classes = (RpcSignatureAuthentication,) permission_classes = () diff --git a/src/sentry/api/endpoints/rule_snooze.py b/src/sentry/api/endpoints/rule_snooze.py index 79b0645d86e683..7c11e21baabe79 100644 --- a/src/sentry/api/endpoints/rule_snooze.py +++ b/src/sentry/api/endpoints/rule_snooze.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from sentry import analytics, audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectAlertRulePermission, ProjectEndpoint from sentry.api.serializers import Serializer, register, serialize @@ -180,11 +181,19 @@ def delete(self, request: Request, project, rule_id) -> Response: @region_silo_endpoint class RuleSnoozeEndpoint(BaseRuleSnoozeEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } rule_model = Rule rule_field = "rule" @region_silo_endpoint class MetricRuleSnoozeEndpoint(BaseRuleSnoozeEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } rule_model = AlertRule rule_field = "alert_rule" diff --git a/src/sentry/api/endpoints/setup_wizard.py b/src/sentry/api/endpoints/setup_wizard.py index 99c8081f9709b6..c96035f7f2f891 100644 --- a/src/sentry/api/endpoints/setup_wizard.py +++ b/src/sentry/api/endpoints/setup_wizard.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import ratelimits +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.serializers import serialize from sentry.cache import default_cache @@ -18,6 +19,10 @@ @region_silo_endpoint class SetupWizard(Endpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = () def delete(self, request: Request, wizard_hash=None) -> Response | None: diff --git a/src/sentry/api/endpoints/shared_group_details.py b/src/sentry/api/endpoints/shared_group_details.py index a99c6b7c02e22e..8b1470212205db 100644 --- a/src/sentry/api/endpoints/shared_group_details.py +++ b/src/sentry/api/endpoints/shared_group_details.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, EnvironmentMixin, region_silo_endpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers import ( @@ -16,6 +17,9 @@ @region_silo_endpoint class SharedGroupDetailsEndpoint(Endpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = () def get( diff --git a/src/sentry/api/endpoints/source_map_debug.py b/src/sentry/api/endpoints/source_map_debug.py index 83cdad1cc757ad..e68e196eb4e1ab 100644 --- a/src/sentry/api/endpoints/source_map_debug.py +++ b/src/sentry/api/endpoints/source_map_debug.py @@ -7,6 +7,7 @@ from typing_extensions import TypedDict from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.helpers.source_map_helper import source_map_debug @@ -29,6 +30,9 @@ class SourceMapProcessingResponse(TypedDict): @region_silo_endpoint @extend_schema(tags=["Events"]) class SourceMapDebugEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.PUBLIC, + } public = {"GET"} owner = ApiOwner.ISSUES diff --git a/src/sentry/api/endpoints/system_health.py b/src/sentry/api/endpoints/system_health.py index 9ed6f844c03e26..44136751174463 100644 --- a/src/sentry/api/endpoints/system_health.py +++ b/src/sentry/api/endpoints/system_health.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import status_checks +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, all_silo_endpoint from sentry.auth.superuser import is_active_superuser from sentry.ratelimits.config import RateLimitConfig @@ -14,6 +15,9 @@ @all_silo_endpoint class SystemHealthEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (IsAuthenticated,) rate_limits = RateLimitConfig(group="INTERNAL") diff --git a/src/sentry/api/endpoints/system_options.py b/src/sentry/api/endpoints/system_options.py index 68c297ecc0bbbd..c823812d950570 100644 --- a/src/sentry/api/endpoints/system_options.py +++ b/src/sentry/api/endpoints/system_options.py @@ -8,6 +8,7 @@ import sentry from sentry import options +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, all_silo_endpoint from sentry.api.permissions import SuperuserPermission from sentry.utils.email import is_smtp_enabled @@ -22,6 +23,10 @@ @all_silo_endpoint class SystemOptionsEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/team_alerts_triggered.py b/src/sentry/api/endpoints/team_alerts_triggered.py index af1a66a495b75e..63873991afe98d 100644 --- a/src/sentry/api/endpoints/team_alerts_triggered.py +++ b/src/sentry/api/endpoints/team_alerts_triggered.py @@ -6,6 +6,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.team import TeamEndpoint from sentry.api.paginator import OffsetPaginator @@ -24,6 +25,10 @@ @region_silo_endpoint class TeamAlertsTriggeredTotalsEndpoint(TeamEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, team) -> Response: """ Return a time-bucketed (by day) count of triggered alerts owned by a given team. @@ -117,6 +122,10 @@ def serialize(self, obj, attrs, user): @region_silo_endpoint class TeamAlertsTriggeredIndexEndpoint(TeamEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request, team) -> Response: """ Returns alert rules ordered by highest number of alerts fired this week. diff --git a/src/sentry/api/endpoints/team_all_unresolved_issues.py b/src/sentry/api/endpoints/team_all_unresolved_issues.py index da418cbe882f3b..779b00b88b2116 100644 --- a/src/sentry/api/endpoints/team_all_unresolved_issues.py +++ b/src/sentry/api/endpoints/team_all_unresolved_issues.py @@ -8,6 +8,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.team import TeamEndpoint from sentry.api.helpers.environments import get_environments @@ -107,6 +108,10 @@ def calculate_unresolved_counts(team, project_list, start, end, environment_id): @region_silo_endpoint class TeamAllUnresolvedIssuesEndpoint(TeamEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, team: Team) -> Response: """ Returns cumulative counts of unresolved groups per day within the stats period time range. diff --git a/src/sentry/api/endpoints/team_details.py b/src/sentry/api/endpoints/team_details.py index f3de010b5bfc64..3d69bc6ee3638c 100644 --- a/src/sentry/api/endpoints/team_details.py +++ b/src/sentry/api/endpoints/team_details.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import audit_log, features, roles +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import ( DEFAULT_SLUG_ERROR_MESSAGE, DEFAULT_SLUG_PATTERN, @@ -51,6 +52,12 @@ def validate_org_role(self, value): @region_silo_endpoint class TeamDetailsEndpoint(TeamEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, team) -> Response: """ Retrieve a Team diff --git a/src/sentry/api/endpoints/team_groups_old.py b/src/sentry/api/endpoints/team_groups_old.py index 823d7ff75b6487..31b7a928f79eea 100644 --- a/src/sentry/api/endpoints/team_groups_old.py +++ b/src/sentry/api/endpoints/team_groups_old.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.team import TeamEndpoint from sentry.api.helpers.environments import get_environments @@ -13,6 +14,10 @@ @region_silo_endpoint class TeamGroupsOldEndpoint(TeamEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, team) -> Response: """ Return the oldest issues owned by a team diff --git a/src/sentry/api/endpoints/team_issue_breakdown.py b/src/sentry/api/endpoints/team_issue_breakdown.py index 7985a67fd5a1d8..802f06d7785047 100644 --- a/src/sentry/api/endpoints/team_issue_breakdown.py +++ b/src/sentry/api/endpoints/team_issue_breakdown.py @@ -8,6 +8,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.team import TeamEndpoint from sentry.api.helpers.environments import get_environments @@ -22,6 +23,10 @@ @region_silo_endpoint class TeamIssueBreakdownEndpoint(TeamEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, team: Team) -> Response: """ Returns a dict of team projects, and a time-series dict of issue stat breakdowns for each. diff --git a/src/sentry/api/endpoints/team_members.py b/src/sentry/api/endpoints/team_members.py index e4d7305db95ab0..2663d1a7aabd76 100644 --- a/src/sentry/api/endpoints/team_members.py +++ b/src/sentry/api/endpoints/team_members.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.team import TeamEndpoint from sentry.api.paginator import OffsetPaginator @@ -43,6 +44,10 @@ def serialize(self, obj, attrs, user): @region_silo_endpoint class TeamMembersEndpoint(TeamEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, team) -> Response: queryset = OrganizationMemberTeam.objects.filter( Q(organizationmember__user_is_active=True, organizationmember__user_id__isnull=False) diff --git a/src/sentry/api/endpoints/team_notification_settings_details.py b/src/sentry/api/endpoints/team_notification_settings_details.py index c2c5276c224f8e..983835b9b01263 100644 --- a/src/sentry/api/endpoints/team_notification_settings_details.py +++ b/src/sentry/api/endpoints/team_notification_settings_details.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.team import TeamEndpoint from sentry.api.serializers import serialize @@ -12,6 +13,10 @@ @region_silo_endpoint class TeamNotificationSettingsDetailsEndpoint(TeamEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } """ This Notification Settings endpoint is the generic way to interact with the NotificationSettings table via the API. diff --git a/src/sentry/api/endpoints/team_projects.py b/src/sentry/api/endpoints/team_projects.py index 9e54d1a7fa4436..99009eaaf55b28 100644 --- a/src/sentry/api/endpoints/team_projects.py +++ b/src/sentry/api/endpoints/team_projects.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import ( DEFAULT_SLUG_ERROR_MESSAGE, DEFAULT_SLUG_PATTERN, @@ -69,6 +70,10 @@ class TeamProjectPermission(TeamPermission): @extend_schema(tags=["Teams"]) @region_silo_endpoint class TeamProjectsEndpoint(TeamEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.PUBLIC, + "POST": ApiPublishStatus.PUBLIC, + } public = {"GET", "POST"} permission_classes = (TeamProjectPermission,) diff --git a/src/sentry/api/endpoints/team_release_count.py b/src/sentry/api/endpoints/team_release_count.py index b9c20d240e55a2..70a9e4bf6bece0 100644 --- a/src/sentry/api/endpoints/team_release_count.py +++ b/src/sentry/api/endpoints/team_release_count.py @@ -8,6 +8,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.team import TeamEndpoint from sentry.api.utils import get_date_range_from_params @@ -16,6 +17,10 @@ @region_silo_endpoint class TeamReleaseCountEndpoint(TeamEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, team) -> Response: """ Returns a dict of team projects, and a time-series list of release counts for each. diff --git a/src/sentry/api/endpoints/team_stats.py b/src/sentry/api/endpoints/team_stats.py index 663431486cd5e3..c08cb2e2a4ed62 100644 --- a/src/sentry/api/endpoints/team_stats.py +++ b/src/sentry/api/endpoints/team_stats.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import tsdb +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, StatsMixin, region_silo_endpoint from sentry.api.bases.team import TeamEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -11,6 +12,10 @@ @region_silo_endpoint class TeamStatsEndpoint(TeamEndpoint, EnvironmentMixin, StatsMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, team) -> Response: """ Retrieve Event Counts for a Team diff --git a/src/sentry/api/endpoints/team_time_to_resolution.py b/src/sentry/api/endpoints/team_time_to_resolution.py index a1ae5a0b70c02a..e007dec0044199 100644 --- a/src/sentry/api/endpoints/team_time_to_resolution.py +++ b/src/sentry/api/endpoints/team_time_to_resolution.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.team import TeamEndpoint from sentry.api.helpers.environments import get_environments @@ -16,6 +17,10 @@ @region_silo_endpoint class TeamTimeToResolutionEndpoint(TeamEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, team) -> Response: """ Return a a time bucketed list of mean group resolution times for a given team. diff --git a/src/sentry/api/endpoints/team_unresolved_issue_age.py b/src/sentry/api/endpoints/team_unresolved_issue_age.py index df5c33b3fff6ba..8a283ad154fdbd 100644 --- a/src/sentry/api/endpoints/team_unresolved_issue_age.py +++ b/src/sentry/api/endpoints/team_unresolved_issue_age.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.team import TeamEndpoint from sentry.api.helpers.environments import get_environments @@ -26,6 +27,10 @@ @region_silo_endpoint class TeamUnresolvedIssueAgeEndpoint(TeamEndpoint, EnvironmentMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, team: Team) -> Response: """ Return a time bucketed list of how old unresolved issues are. diff --git a/src/sentry/api/endpoints/user_authenticator_details.py b/src/sentry/api/endpoints/user_authenticator_details.py index bba2159ac623fb..b279e04fa9bf28 100644 --- a/src/sentry/api/endpoints/user_authenticator_details.py +++ b/src/sentry/api/endpoints/user_authenticator_details.py @@ -5,6 +5,7 @@ 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 control_silo_endpoint from sentry.api.bases.user import OrganizationUserPermission, UserEndpoint from sentry.api.decorators import sudo_required @@ -17,6 +18,11 @@ @control_silo_endpoint class UserAuthenticatorDetailsEndpoint(UserEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE permission_classes = (OrganizationUserPermission,) diff --git a/src/sentry/api/endpoints/user_authenticator_enroll.py b/src/sentry/api/endpoints/user_authenticator_enroll.py index 68ad15d32940d6..7e2984994a9dbf 100644 --- a/src/sentry/api/endpoints/user_authenticator_enroll.py +++ b/src/sentry/api/endpoints/user_authenticator_enroll.py @@ -10,6 +10,7 @@ from sentry import ratelimits as ratelimiter from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.decorators import email_verification_required, sudo_required @@ -106,6 +107,10 @@ def get_serializer_field_metadata(serializer, fields=None): @control_silo_endpoint class UserAuthenticatorEnrollEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE @sudo_required diff --git a/src/sentry/api/endpoints/user_authenticator_index.py b/src/sentry/api/endpoints/user_authenticator_index.py index 60a6b3aa1a1614..0ad9f565be0722 100644 --- a/src/sentry/api/endpoints/user_authenticator_index.py +++ b/src/sentry/api/endpoints/user_authenticator_index.py @@ -2,6 +2,7 @@ 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 control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.serializers import serialize @@ -10,6 +11,9 @@ @control_silo_endpoint class UserAuthenticatorIndexEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE def get(self, request: Request, user) -> Response: diff --git a/src/sentry/api/endpoints/user_details.py b/src/sentry/api/endpoints/user_details.py index 41b2d9a7950f3d..00c7546d61bdc9 100644 --- a/src/sentry/api/endpoints/user_details.py +++ b/src/sentry/api/endpoints/user_details.py @@ -12,6 +12,7 @@ from sentry import roles from sentry.api import client +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.decorators import sudo_required @@ -130,6 +131,12 @@ class DeleteUserSerializer(serializers.Serializer): @control_silo_endpoint class UserDetailsEndpoint(UserEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, user) -> Response: """ Retrieve User Details diff --git a/src/sentry/api/endpoints/user_emails.py b/src/sentry/api/endpoints/user_emails.py index c31d0e52f4faa4..7ea10f8a1871be 100644 --- a/src/sentry/api/endpoints/user_emails.py +++ b/src/sentry/api/endpoints/user_emails.py @@ -4,6 +4,7 @@ from django.db.models import Q from rest_framework import serializers +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.decorators import sudo_required @@ -58,6 +59,13 @@ def add_email(email, user): @control_silo_endpoint class UserEmailsEndpoint(UserEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, user) -> Response: """ Get list of emails diff --git a/src/sentry/api/endpoints/user_emails_confirm.py b/src/sentry/api/endpoints/user_emails_confirm.py index def1fa6bb1837b..fdd9e0126816eb 100644 --- a/src/sentry/api/endpoints/user_emails_confirm.py +++ b/src/sentry/api/endpoints/user_emails_confirm.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.validators import AllowedEmailField @@ -35,6 +36,9 @@ class EmailSerializer(serializers.Serializer): @control_silo_endpoint class UserEmailsConfirmEndpoint(UserEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } rate_limits = { "POST": { RateLimitCategory.USER: RateLimit(10, 60), diff --git a/src/sentry/api/endpoints/user_identity.py b/src/sentry/api/endpoints/user_identity.py index e0190c6487d234..979762abb2ce4d 100644 --- a/src/sentry/api/endpoints/user_identity.py +++ b/src/sentry/api/endpoints/user_identity.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.paginator import OffsetPaginator @@ -10,6 +11,10 @@ @control_silo_endpoint class UserIdentityEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, user) -> Response: """ Retrieve all of a users' identities (NOT AuthIdentities) diff --git a/src/sentry/api/endpoints/user_identity_config.py b/src/sentry/api/endpoints/user_identity_config.py index f74e20709099fb..2703b4601513d1 100644 --- a/src/sentry/api/endpoints/user_identity_config.py +++ b/src/sentry/api/endpoints/user_identity_config.py @@ -6,6 +6,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.serializers import serialize @@ -76,6 +77,10 @@ def get_org_identity_status(obj: AuthIdentity) -> Status: @control_silo_endpoint class UserIdentityConfigEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, user) -> Response: """ Retrieve all of a user's SocialIdentity, Identity, and AuthIdentity values @@ -91,6 +96,11 @@ def get(self, request: Request, user) -> Response: @control_silo_endpoint class UserIdentityConfigDetailsEndpoint(UserEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + } + @staticmethod def _get_identity(user, category, identity_id) -> Optional[UserIdentityConfig]: identity_id = int(identity_id) diff --git a/src/sentry/api/endpoints/user_identity_details.py b/src/sentry/api/endpoints/user_identity_details.py index e0f9b36546dffb..60b3654c1761a0 100644 --- a/src/sentry/api/endpoints/user_identity_details.py +++ b/src/sentry/api/endpoints/user_identity_details.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.models import AuthIdentity @@ -8,6 +9,10 @@ @control_silo_endpoint class UserIdentityDetailsEndpoint(UserEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + } + def delete(self, request: Request, user, identity_id) -> Response: AuthIdentity.objects.filter(user=user, id=identity_id).delete() return Response(status=204) diff --git a/src/sentry/api/endpoints/user_index.py b/src/sentry/api/endpoints/user_index.py index ed7de66bfcb4ac..bd6e3fdbd0164a 100644 --- a/src/sentry/api/endpoints/user_index.py +++ b/src/sentry/api/endpoints/user_index.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.api.paginator import DateTimePaginator from sentry.api.permissions import SuperuserPermission @@ -13,6 +14,9 @@ @control_silo_endpoint class UserIndexEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/api/endpoints/user_ips.py b/src/sentry/api/endpoints/user_ips.py index f5d30410e97e78..92a6e0cfe32a59 100644 --- a/src/sentry/api/endpoints/user_ips.py +++ b/src/sentry/api/endpoints/user_ips.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.decorators import sudo_required @@ -11,6 +12,10 @@ @control_silo_endpoint class UserIPsEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + @sudo_required def get(self, request: Request, user) -> Response: """ diff --git a/src/sentry/api/endpoints/user_notification_details.py b/src/sentry/api/endpoints/user_notification_details.py index b6c5453f25ed37..b57a15646ad035 100644 --- a/src/sentry/api/endpoints/user_notification_details.py +++ b/src/sentry/api/endpoints/user_notification_details.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.fields.empty_integer import EmptyIntegerField @@ -74,6 +75,11 @@ class UserNotificationDetailsSerializer(serializers.Serializer): @control_silo_endpoint class UserNotificationDetailsEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, user) -> Response: serialized = serialize(user, request.user, UserNotificationsSerializer()) return Response(serialized) diff --git a/src/sentry/api/endpoints/user_notification_fine_tuning.py b/src/sentry/api/endpoints/user_notification_fine_tuning.py index c152f98d4ec6dc..b0c7217f085cd0 100644 --- a/src/sentry/api/endpoints/user_notification_fine_tuning.py +++ b/src/sentry/api/endpoints/user_notification_fine_tuning.py @@ -5,6 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.serializers import serialize @@ -28,6 +29,11 @@ @control_silo_endpoint class UserNotificationFineTuningEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, user, notification_type) -> Response: try: notification_type = FineTuningAPIKey(notification_type) diff --git a/src/sentry/api/endpoints/user_notification_settings_details.py b/src/sentry/api/endpoints/user_notification_settings_details.py index c747cb38ea2c27..3d959a41c75c6b 100644 --- a/src/sentry/api/endpoints/user_notification_settings_details.py +++ b/src/sentry/api/endpoints/user_notification_settings_details.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.serializers import serialize @@ -12,6 +13,10 @@ @control_silo_endpoint class UserNotificationSettingsDetailsEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } """ This Notification Settings endpoint is the generic way to interact with the NotificationSettings table via the API. diff --git a/src/sentry/api/endpoints/user_notification_settings_options.py b/src/sentry/api/endpoints/user_notification_settings_options.py index ffea0a99c05677..2d553af4e00d9d 100644 --- a/src/sentry/api/endpoints/user_notification_settings_options.py +++ b/src/sentry/api/endpoints/user_notification_settings_options.py @@ -3,6 +3,7 @@ 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 control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.exceptions import ParameterValidationError @@ -16,6 +17,10 @@ @control_silo_endpoint class UserNotificationSettingsOptionsEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.PRIVATE, + "PUT": ApiPublishStatus.PRIVATE, + } owner = ApiOwner.ISSUES # TODO(Steve): Make not private when we launch new system private = True diff --git a/src/sentry/api/endpoints/user_notification_settings_options_detail.py b/src/sentry/api/endpoints/user_notification_settings_options_detail.py index 0f5d0793ec0823..5b9d22b5eeec93 100644 --- a/src/sentry/api/endpoints/user_notification_settings_options_detail.py +++ b/src/sentry/api/endpoints/user_notification_settings_options_detail.py @@ -3,6 +3,7 @@ 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 control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.models import User @@ -11,6 +12,9 @@ @control_silo_endpoint class UserNotificationSettingsOptionsDetailEndpoint(UserEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.PRIVATE, + } owner = ApiOwner.ISSUES # TODO(Steve): Make not private when we launch new system private = True diff --git a/src/sentry/api/endpoints/user_notification_settings_providers.py b/src/sentry/api/endpoints/user_notification_settings_providers.py index 50118c41a35ef3..4848a6753edce9 100644 --- a/src/sentry/api/endpoints/user_notification_settings_providers.py +++ b/src/sentry/api/endpoints/user_notification_settings_providers.py @@ -4,6 +4,7 @@ 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 control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.exceptions import ParameterValidationError @@ -21,6 +22,10 @@ @control_silo_endpoint class UserNotificationSettingsProvidersEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.PRIVATE, + "PUT": ApiPublishStatus.PRIVATE, + } owner = ApiOwner.ISSUES # TODO(Steve): Make not private when we launch new system private = True diff --git a/src/sentry/api/endpoints/user_organizationintegrations.py b/src/sentry/api/endpoints/user_organizationintegrations.py index 3d5dd11f189eb1..a0a4692ff61d10 100644 --- a/src/sentry/api/endpoints/user_organizationintegrations.py +++ b/src/sentry/api/endpoints/user_organizationintegrations.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.paginator import OffsetPaginator @@ -12,6 +13,10 @@ @control_silo_endpoint class UserOrganizationIntegrationsEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, user) -> Response: """ Retrieve all of a users' organization integrations diff --git a/src/sentry/api/endpoints/user_organizations.py b/src/sentry/api/endpoints/user_organizations.py index 5fa434847dac3d..6e7bab58a1bfff 100644 --- a/src/sentry/api/endpoints/user_organizations.py +++ b/src/sentry/api/endpoints/user_organizations.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from typing_extensions import override +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.bases.user import UserPermission from sentry.api.exceptions import ResourceDoesNotExist @@ -17,6 +18,9 @@ @region_silo_endpoint class UserOrganizationsEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (UserPermission,) @override diff --git a/src/sentry/api/endpoints/user_password.py b/src/sentry/api/endpoints/user_password.py index 4986ede68695c4..81c67c927c157f 100644 --- a/src/sentry/api/endpoints/user_password.py +++ b/src/sentry/api/endpoints/user_password.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.auth import password_validation @@ -44,6 +45,10 @@ def validate(self, attrs): @control_silo_endpoint class UserPasswordEndpoint(UserEndpoint): + publish_status = { + "PUT": ApiPublishStatus.UNKNOWN, + } + def put(self, request: Request, user) -> Response: # pass some context to serializer otherwise when we create a new serializer instance, # user.password gets set to new plaintext password from request and diff --git a/src/sentry/api/endpoints/user_permission_details.py b/src/sentry/api/endpoints/user_permission_details.py index 65d3ec02e3ce62..0c62f66ab4d071 100644 --- a/src/sentry/api/endpoints/user_permission_details.py +++ b/src/sentry/api/endpoints/user_permission_details.py @@ -6,6 +6,7 @@ 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 control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.decorators import sudo_required @@ -17,6 +18,11 @@ @control_silo_endpoint class UserPermissionDetailsEndpoint(UserEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE permission_classes = (SuperuserPermission,) diff --git a/src/sentry/api/endpoints/user_permissions.py b/src/sentry/api/endpoints/user_permissions.py index d6231ad9518a2a..30023075cf5a71 100644 --- a/src/sentry/api/endpoints/user_permissions.py +++ b/src/sentry/api/endpoints/user_permissions.py @@ -2,6 +2,7 @@ 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 control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.permissions import SuperuserPermission @@ -10,6 +11,9 @@ @control_silo_endpoint class UserPermissionsEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE permission_classes = (SuperuserPermission,) diff --git a/src/sentry/api/endpoints/user_permissions_config.py b/src/sentry/api/endpoints/user_permissions_config.py index b16ca7f5402fa2..b5a14e89657360 100644 --- a/src/sentry/api/endpoints/user_permissions_config.py +++ b/src/sentry/api/endpoints/user_permissions_config.py @@ -3,6 +3,7 @@ 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 control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.permissions import SuperuserPermission @@ -10,6 +11,9 @@ @control_silo_endpoint class UserPermissionsConfigEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.ENTERPRISE permission_classes = (SuperuserPermission,) diff --git a/src/sentry/api/endpoints/user_role_details.py b/src/sentry/api/endpoints/user_role_details.py index f63303b25db9a2..7bd2a2de7436e4 100644 --- a/src/sentry/api/endpoints/user_role_details.py +++ b/src/sentry/api/endpoints/user_role_details.py @@ -5,6 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.decorators import sudo_required @@ -17,6 +18,11 @@ @control_silo_endpoint class UserUserRoleDetailsEndpoint(UserEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request, user, role_name) -> Response: diff --git a/src/sentry/api/endpoints/user_roles.py b/src/sentry/api/endpoints/user_roles.py index 13d3c30248cd45..171ebdd24668fb 100644 --- a/src/sentry/api/endpoints/user_roles.py +++ b/src/sentry/api/endpoints/user_roles.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.permissions import SuperuserPermission @@ -10,6 +11,9 @@ @control_silo_endpoint class UserUserRolesEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request, user) -> Response: diff --git a/src/sentry/api/endpoints/user_social_identities_index.py b/src/sentry/api/endpoints/user_social_identities_index.py index 24627164948029..518da0c54c85cc 100644 --- a/src/sentry/api/endpoints/user_social_identities_index.py +++ b/src/sentry/api/endpoints/user_social_identities_index.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.api.serializers import serialize @@ -9,6 +10,10 @@ @control_silo_endpoint class UserSocialIdentitiesIndexEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, user) -> Response: """ List Account's Identities diff --git a/src/sentry/api/endpoints/user_social_identity_details.py b/src/sentry/api/endpoints/user_social_identity_details.py index 39ec435a088ae5..90778461a89d04 100644 --- a/src/sentry/api/endpoints/user_social_identity_details.py +++ b/src/sentry/api/endpoints/user_social_identity_details.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from social_auth.backends import get_backend @@ -13,6 +14,10 @@ @control_silo_endpoint class UserSocialIdentityDetailsEndpoint(UserEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + } + def delete(self, request: Request, user, identity_id) -> Response: """ Disconnect a Identity from Account diff --git a/src/sentry/api/endpoints/user_subscriptions.py b/src/sentry/api/endpoints/user_subscriptions.py index 5984391e9250d9..ddb11b0ef6ed74 100644 --- a/src/sentry/api/endpoints/user_subscriptions.py +++ b/src/sentry/api/endpoints/user_subscriptions.py @@ -3,6 +3,7 @@ from rest_framework import serializers from sentry import newsletter +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.user import UserEndpoint from sentry.models import User, UserEmail @@ -23,6 +24,12 @@ class NewsletterValidator(serializers.Serializer): @control_silo_endpoint class UserSubscriptionsEndpoint(UserEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, user) -> Response: """ Retrieve Account Subscriptions diff --git a/src/sentry/api/endpoints/userroles_details.py b/src/sentry/api/endpoints/userroles_details.py index 4b942c7d13737f..d487684ee9c40f 100644 --- a/src/sentry/api/endpoints/userroles_details.py +++ b/src/sentry/api/endpoints/userroles_details.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.api.decorators import sudo_required from sentry.api.permissions import SuperuserPermission @@ -16,6 +17,11 @@ @control_silo_endpoint class UserRoleDetailsEndpoint(Endpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) @sudo_required diff --git a/src/sentry/api/endpoints/userroles_index.py b/src/sentry/api/endpoints/userroles_index.py index 5f89471fbbcd49..7ec06dd6403f6b 100644 --- a/src/sentry/api/endpoints/userroles_index.py +++ b/src/sentry/api/endpoints/userroles_index.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.api.decorators import sudo_required from sentry.api.permissions import SuperuserPermission @@ -16,6 +17,10 @@ @control_silo_endpoint class UserRolesEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (SuperuserPermission,) def get(self, request: Request) -> Response: diff --git a/src/sentry/apidocs/hooks.py b/src/sentry/apidocs/hooks.py index 0a8eacea213a84..2a3156f4f38b0d 100644 --- a/src/sentry/apidocs/hooks.py +++ b/src/sentry/apidocs/hooks.py @@ -5,9 +5,10 @@ from sentry.apidocs.build import OPENAPI_TAGS from sentry.apidocs.utils import SentryApiBuildError -HTTP_METHODS_SET = Set[ - Literal["GET", "POST", "PUT", "OPTIONS", "HEAD", "DELETE", "TRACE", "CONNECT", "PATCH"] +HTTP_METHOD_NAME = Literal[ + "GET", "POST", "PUT", "OPTIONS", "HEAD", "DELETE", "TRACE", "CONNECT", "PATCH" ] +HTTP_METHODS_SET = Set[HTTP_METHOD_NAME] class EndpointRegistryType(TypedDict): @@ -70,7 +71,6 @@ def __get_explicit_endpoints() -> List[Tuple[str, str, str, Any]]: def custom_preprocessing_hook(endpoints: Any) -> Any: # TODO: organize method, rename - filtered = [] for (path, path_regex, method, callback) in endpoints: diff --git a/src/sentry/audit_log/events.py b/src/sentry/audit_log/events.py index d4fd9d874056e9..db348513d4fbcd 100644 --- a/src/sentry/audit_log/events.py +++ b/src/sentry/audit_log/events.py @@ -168,6 +168,16 @@ def render(self, audit_log_entry: AuditLogEntry): return render_project_action(audit_log_entry, "disable") +class ProjectOwnershipRuleEditAuditLogEvent(AuditLogEvent): + def __init__(self): + super().__init__( + event_id=179, name="PROJECT_OWNERSHIPRULE_EDIT", api_name="project.ownership-rule.edit" + ) + + def render(self, audit_log_entry: AuditLogEntry): + return "modified ownership rules" + + class SSOEditAuditLogEvent(AuditLogEvent): def __init__(self): super().__init__(event_id=62, name="SSO_EDIT", api_name="sso.edit") diff --git a/src/sentry/audit_log/register.py b/src/sentry/audit_log/register.py index f896a8392706bf..478b4b63a17604 100644 --- a/src/sentry/audit_log/register.py +++ b/src/sentry/audit_log/register.py @@ -393,3 +393,4 @@ template="removed org auth token {name}", ) ) +default_manager.add(events.ProjectOwnershipRuleEditAuditLogEvent()) diff --git a/src/sentry/backup/exports.py b/src/sentry/backup/exports.py index bcabedbae961be..d5ec3a45eeaefd 100644 --- a/src/sentry/backup/exports.py +++ b/src/sentry/backup/exports.py @@ -8,10 +8,18 @@ from django.core.serializers.json import DjangoJSONEncoder from sentry.backup.dependencies import sorted_dependencies -from sentry.backup.scopes import RelocationScope +from sentry.backup.scopes import ExportScope UTC_0 = timezone(timedelta(hours=0)) +__all__ = ( + "DatetimeSafeDjangoJSONEncoder", + "OldExportConfig", + "export_in_user_scope", + "export_in_organization_scope", + "export_in_global_scope", +) + class DatetimeSafeDjangoJSONEncoder(DjangoJSONEncoder): """A wrapper around the default `DjangoJSONEncoder` that always retains milliseconds, even when @@ -41,8 +49,14 @@ class OldExportConfig(NamedTuple): use_natural_foreign_keys: bool = False -def exports(dest, old_config: OldExportConfig, indent: int, printer=click.echo): - """Exports core data for the Sentry installation.""" +def _export(dest, scope: ExportScope, old_config: OldExportConfig, indent: int, printer=click.echo): + """ + Exports core data for the Sentry installation. + + It is generally preferable to avoid calling this function directly, as there are certain combinations of input parameters that should not be used together. Instead, use one of the other wrapper functions in this file, named `export_in_XXX_scope()`. + """ + + allowed_relocation_scopes = scope.value def yield_objects(): # Collate the objects to be serialized. @@ -56,9 +70,11 @@ def yield_objects(): # print(f"Deduced rel scope for {model.__name__}: {inferred_relocation_scope.name}\n") includable = old_config.include_non_sentry_models if hasattr(model, "__relocation_scope__"): - # TODO(getsentry/team-ospo#166): Make this check more precise once we pipe the - # `scope` argument through. - if getattr(model, "__relocation_scope__") != RelocationScope.Excluded: + # TODO(getsentry/team-ospo#186): This won't be sufficient once we're trying to get + # relocation scopes that may vary on a per-instance, rather than + # per-model-definition, basis. We'll probably need to make use of something like + # Django annotations to efficiently filter down models. + if getattr(model, "__relocation_scope__") in allowed_relocation_scopes: includable = True if ( @@ -81,3 +97,25 @@ def yield_objects(): use_natural_foreign_keys=old_config.use_natural_foreign_keys, cls=DatetimeSafeDjangoJSONEncoder, ) + + +def export_in_user_scope(src, printer=click.echo): + """ + Perform an export in the `User` scope, meaning that only models with `RelocationScope.User` will be exported from the provided `src` file. + """ + return _export(src, ExportScope.User, OldExportConfig(), 2, printer) + + +def export_in_organization_scope(src, printer=click.echo): + """ + Perform an export in the `Organization` scope, meaning that only models with `RelocationScope.User` or `RelocationScope.Organization` will be exported from the provided `src` file. + """ + return _export(src, ExportScope.Organization, OldExportConfig(), 2, printer) + + +def export_in_global_scope(src, printer=click.echo): + """ + Perform an export in the `Global` scope, meaning that all models will be exported from the + provided source file. + """ + return _export(src, ExportScope.Global, OldExportConfig(), 2, printer) diff --git a/src/sentry/backup/imports.py b/src/sentry/backup/imports.py index d1825b72f55b5f..4c52c4f097e440 100644 --- a/src/sentry/backup/imports.py +++ b/src/sentry/backup/imports.py @@ -10,6 +10,15 @@ from sentry.backup.dependencies import PrimaryKeyMap, normalize_model_name from sentry.backup.helpers import EXCLUDED_APPS +from sentry.backup.scopes import ImportScope +from sentry.silo import unguarded_write + +__all__ = ( + "OldImportConfig", + "import_in_user_scope", + "import_in_organization_scope", + "import_in_global_scope", +) class OldImportConfig(NamedTuple): @@ -27,12 +36,19 @@ class OldImportConfig(NamedTuple): use_natural_foreign_keys: bool = False -def imports(src, old_config: OldImportConfig, printer=click.echo): - """Imports core data for the Sentry installation.""" +def _import(src, scope: ImportScope, old_config: OldImportConfig, printer=click.echo): + """ + Imports core data for a Sentry installation. + + It is generally preferable to avoid calling this function directly, as there are certain combinations of input parameters that should not be used together. Instead, use one of the other wrapper functions in this file, named `import_in_XXX_scope()`. + """ try: # Import / export only works in monolith mode with a consolidated db. - with transaction.atomic("default"): + # TODO(getsentry/team-ospo#185): the `unguarded_write` is temporary until we get and RPC + # service up for writing to control silo models. + with unguarded_write(using="default"), transaction.atomic("default"): + allowed_relocation_scopes = scope.value pk_map = PrimaryKeyMap() for obj in serializers.deserialize( "json", src, stream=True, use_natural_keys=old_config.use_natural_foreign_keys @@ -43,9 +59,9 @@ def imports(src, old_config: OldImportConfig, printer=click.echo): # to roll out the new API to self-hosted. if old_config.use_update_instead_of_create: obj.save() - else: + elif o.get_relocation_scope() in allowed_relocation_scopes: o = obj.object - written = o.write_relocation_import(pk_map, obj) + written = o.write_relocation_import(pk_map, obj, scope) if written is not None: old_pk, new_pk = written model_name = normalize_model_name(o) @@ -72,3 +88,27 @@ def imports(src, old_config: OldImportConfig, printer=click.echo): with connection.cursor() as cursor: cursor.execute(sequence_reset_sql.getvalue()) + + +def import_in_user_scope(src, printer=click.echo): + """ + Perform an import in the `User` scope, meaning that only models with `RelocationScope.User` will be imported from the provided `src` file. + """ + return _import(src, ImportScope.User, OldImportConfig(), printer) + + +def import_in_organization_scope(src, printer=click.echo): + """ + Perform an import in the `Organization` scope, meaning that only models with `RelocationScope.User` or `RelocationScope.Organization` will be imported from the provided `src` file. + """ + return _import(src, ImportScope.Organization, OldImportConfig(), printer) + + +def import_in_global_scope(src, printer=click.echo): + """ + Perform an import in the `Global` scope, meaning that all models will be imported from the + provided source file. Because a `Global` import is really only useful when restoring to a fresh + Sentry instance, some behaviors in this scope are different from the others. In particular, + superuser privileges are not sanitized. + """ + return _import(src, ImportScope.Global, OldImportConfig(), printer) diff --git a/src/sentry/backup/mixins.py b/src/sentry/backup/mixins.py new file mode 100644 index 00000000000000..2abf021a8548e7 --- /dev/null +++ b/src/sentry/backup/mixins.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from typing import Optional, Tuple + +from django.core.serializers.base import DeserializedObject + +from sentry.backup.dependencies import PrimaryKeyMap +from sentry.backup.scopes import ImportScope + + +class SanitizeUserImportsMixin: + """ + The only realistic reason to do a `Global`ly-scoped import is when restoring some full-instance + backup to a clean install. In this case, one may want to import so-called "superusers": users + with powerful various instance-wide permissions generally reserved for admins and instance + maintainers. Thus, for security reasons, running this import in any `ImportScope` other than + `Global` will sanitize user imports by ignoring imports of the `UserPermission`, `UserRole`, and + `UserRoleUser` models. + """ + + def write_relocation_import( + self, pk_map: PrimaryKeyMap, obj: DeserializedObject, scope: ImportScope + ) -> Optional[Tuple[int, int]]: + if scope != ImportScope.Global: + return None + + return super().write_relocation_import(pk_map, obj, scope) # type: ignore[misc] diff --git a/src/sentry/backup/scopes.py b/src/sentry/backup/scopes.py index 8f1cc3f4c80fd6..58ab7d4d078a97 100644 --- a/src/sentry/backup/scopes.py +++ b/src/sentry/backup/scopes.py @@ -13,9 +13,36 @@ class RelocationScope(Enum): # to a specific user. Global = auto() + # For all models that transitively depend on either `User` or `Organization` root models, and + # nothing else. + Organization = auto() + # Any `Control`-silo model that is either a `User*` model, or directly owner by one, is in this # scope. User = auto() - # For all `Region`-siloed models tied to a specific `Organization`. - Organization = auto() + +@unique +class ExportScope(Enum): + """ + When executing the `sentry export` command, these scopes specify which of the above + `RelocationScope`s should be included in the final export. The basic idea is that each of these + scopes is inclusive of its predecessor in terms of which `RelocationScope`s it accepts. + """ + + User = {RelocationScope.User} + Organization = {RelocationScope.User, RelocationScope.Organization} + Global = {RelocationScope.User, RelocationScope.Organization, RelocationScope.Global} + + +@unique +class ImportScope(Enum): + """ + When executing the `sentry import` command, these scopes specify which of the above + `RelocationScope`s should be included in the final upload. The basic idea is that each of these + scopes is inclusive of its predecessor in terms of which `RelocationScope`s it accepts. + """ + + User = {RelocationScope.User} + Organization = {RelocationScope.User, RelocationScope.Organization} + Global = {RelocationScope.User, RelocationScope.Organization, RelocationScope.Global} diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index e60c505bc43a87..6b43fbd0007d69 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -1327,8 +1327,6 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: # Sentry and internal client configuration SENTRY_FEATURES = { - # Prevents entirely numeric resource slugs - "app:enterprise-prevent-numeric-slugs": False, # Enables user registration. "auth:register": True, # Enables actionable items alerts and endpoint @@ -1369,10 +1367,6 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: "organizations:customer-domains": False, # Allow disabling github integrations when broken is detected "organizations:github-disable-on-broken": False, - # Allow disabling integrations when broken is detected - "organizations:slack-fatal-disable-on-broken": False, - # Allow disabling sentryapps when broken is detected - "organizations:disable-sentryapps-on-broken": False, # Enable the 'discover' interface. "organizations:discover": False, # Enables events endpoint rate limit diff --git a/src/sentry/constants.py b/src/sentry/constants.py index bb12d26b2546ab..4dd5647d62caa5 100644 --- a/src/sentry/constants.py +++ b/src/sentry/constants.py @@ -273,6 +273,7 @@ def get_all_languages() -> List[str]: "sentry.rules.filters.assigned_to.AssignedToFilter", "sentry.rules.filters.latest_release.LatestReleaseFilter", "sentry.rules.filters.issue_category.IssueCategoryFilter", + "sentry.rules.filters.issue_severity.IssueSeverityFilter", # The following filters are duplicates of their respective conditions and are conditionally shown if the user has issue alert-filters "sentry.rules.filters.event_attribute.EventAttributeFilter", "sentry.rules.filters.tagged_event.TaggedEventFilter", diff --git a/src/sentry/data_export/endpoints/data_export.py b/src/sentry/data_export/endpoints/data_export.py index bd0ad8ab1a45a2..337bdd1a1bfa75 100644 --- a/src/sentry/data_export/endpoints/data_export.py +++ b/src/sentry/data_export/endpoints/data_export.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.organization import OrganizationDataExportPermission, OrganizationEndpoint from sentry.api.serializers import serialize @@ -112,6 +113,9 @@ def validate(self, data): @region_silo_endpoint class DataExportEndpoint(OrganizationEndpoint, EnvironmentMixin): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationDataExportPermission,) def post(self, request: Request, organization) -> Response: diff --git a/src/sentry/data_export/endpoints/data_export_details.py b/src/sentry/data_export/endpoints/data_export_details.py index daf45dec3ab0b1..22a796e5faffb6 100644 --- a/src/sentry/data_export/endpoints/data_export_details.py +++ b/src/sentry/data_export/endpoints/data_export_details.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import pending_silo_endpoint from sentry.api.bases.organization import OrganizationDataExportPermission, OrganizationEndpoint from sentry.api.serializers import serialize @@ -16,6 +17,9 @@ @pending_silo_endpoint class DataExportDetailsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationDataExportPermission,) def get(self, request: Request, organization: Organization, data_export_id: str) -> Response: diff --git a/src/sentry/db/models/base.py b/src/sentry/db/models/base.py index 048f2bf573e545..9403df24c60f28 100644 --- a/src/sentry/db/models/base.py +++ b/src/sentry/db/models/base.py @@ -9,7 +9,7 @@ from django.utils import timezone from sentry.backup.dependencies import PrimaryKeyMap, dependencies, normalize_model_name -from sentry.backup.scopes import RelocationScope +from sentry.backup.scopes import ImportScope, RelocationScope from sentry.silo import SiloLimit, SiloMode from .fields.bounded import BoundedBigAutoField @@ -108,7 +108,7 @@ def get_relocation_scope(self) -> RelocationScope: return self.__relocation_scope__ - def _normalize_before_relocation_import(self, pk_map: PrimaryKeyMap) -> int: + def _normalize_before_relocation_import(self, pk_map: PrimaryKeyMap, _: ImportScope) -> int: """ A helper function that normalizes a deserialized model. Note that this modifies the model in place, so it should generally be done inside of the companion `write_relocation_import` method, to avoid data skew or corrupted local state. @@ -136,13 +136,13 @@ def _normalize_before_relocation_import(self, pk_map: PrimaryKeyMap) -> int: return old_pk def write_relocation_import( - self, pk_map: PrimaryKeyMap, obj: DeserializedObject + self, pk_map: PrimaryKeyMap, obj: DeserializedObject, scope: ImportScope ) -> Optional[Tuple[int, int]]: """ Writes a deserialized model to the database. If this write is successful, this method will return a tuple of the old and new `pk`s. """ - old_pk = self._normalize_before_relocation_import(pk_map) + old_pk = self._normalize_before_relocation_import(pk_map, scope) obj.save(force_insert=True) return (old_pk, self.pk) diff --git a/src/sentry/db/models/utils.py b/src/sentry/db/models/utils.py index d5d4d1cbc3a327..7250fb0d37595a 100644 --- a/src/sentry/db/models/utils.py +++ b/src/sentry/db/models/utils.py @@ -9,7 +9,7 @@ from django.utils.crypto import get_random_string from django.utils.text import slugify -from sentry import features +from sentry import options from sentry.db.exceptions import CannotResolveExpression COMBINED_EXPRESSION_CALLBACKS = { @@ -80,7 +80,7 @@ def slugify_instance( # Don't further mutate if the value is unique if not base_qs.filter(**{f"{field_name}__iexact": base_value}).exists(): - if features.has("app:enterprise-prevent-numeric-slugs"): + if options.get("api.prevent-numeric-slugs"): # if feature flag is on, we only return if the slug is not entirely numeric if not base_value.isdigit(): return diff --git a/src/sentry/discover/endpoints/discover_homepage_query.py b/src/sentry/discover/endpoints/discover_homepage_query.py index 391c8c8a78a3ce..490a7ad0d45d23 100644 --- a/src/sentry/discover/endpoints/discover_homepage_query.py +++ b/src/sentry/discover/endpoints/discover_homepage_query.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -22,6 +23,11 @@ def get_homepage_query(organization, user): @region_silo_endpoint class DiscoverHomepageQueryEndpoint(OrganizationEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = ( IsAuthenticated, diff --git a/src/sentry/discover/endpoints/discover_key_transactions.py b/src/sentry/discover/endpoints/discover_key_transactions.py index ff89f1ff239071..a17f83c9f2cce4 100644 --- a/src/sentry/discover/endpoints/discover_key_transactions.py +++ b/src/sentry/discover/endpoints/discover_key_transactions.py @@ -8,6 +8,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import KeyTransactionBase from sentry.api.bases.organization import OrganizationPermission @@ -32,6 +33,11 @@ class KeyTransactionPermission(OrganizationPermission): @region_silo_endpoint class KeyTransactionEndpoint(KeyTransactionBase): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (KeyTransactionPermission,) def get(self, request: Request, organization) -> Response: @@ -139,6 +145,9 @@ def delete(self, request: Request, organization) -> Response: @region_silo_endpoint class KeyTransactionListEndpoint(KeyTransactionBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (KeyTransactionPermission,) def get(self, request: Request, organization) -> Response: diff --git a/src/sentry/discover/endpoints/discover_saved_queries.py b/src/sentry/discover/endpoints/discover_saved_queries.py index 0492c5b2866f18..886d3a627ae256 100644 --- a/src/sentry/discover/endpoints/discover_saved_queries.py +++ b/src/sentry/discover/endpoints/discover_saved_queries.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEndpoint from sentry.api.paginator import GenericOffsetPaginator @@ -18,6 +19,10 @@ @region_silo_endpoint class DiscoverSavedQueriesEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (DiscoverSavedQueryPermission,) def has_feature(self, organization, request): diff --git a/src/sentry/discover/endpoints/discover_saved_query_detail.py b/src/sentry/discover/endpoints/discover_saved_query_detail.py index 19f7ab90638895..c6c1a11ca96519 100644 --- a/src/sentry/discover/endpoints/discover_saved_query_detail.py +++ b/src/sentry/discover/endpoints/discover_saved_query_detail.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -16,6 +17,11 @@ @region_silo_endpoint class DiscoverSavedQueryDetailEndpoint(OrganizationEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (DiscoverSavedQueryPermission,) def has_feature(self, organization, request): @@ -110,6 +116,9 @@ def delete(self, request: Request, organization, query_id) -> Response: @region_silo_endpoint class DiscoverSavedQueryVisitEndpoint(OrganizationEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (DiscoverSavedQueryPermission,) def has_feature(self, organization, request): diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index d5b51fc6d4af52..ca2fcbc803a792 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -458,7 +458,7 @@ def save_error_events( project: Project, job: Job, projects: ProjectsMapping, - metric_tags: Dict[str, str], + metric_tags: MutableTags, raw: bool = False, cache_key: Optional[str] = None, ) -> Event: @@ -2356,10 +2356,14 @@ def _calculate_event_grouping( Main entrypoint for modifying/enhancing and grouping an event, writes hashes back into event payload. """ - metric_tags = { + load_stacktrace_from_cache = bool(event.org_can_load_stacktrace_from_cache) + metric_tags: MutableTags = { "grouping_config": grouping_config["id"], "platform": event.platform or "unknown", + "loading_from_cache": load_stacktrace_from_cache, } + # This will help us differentiate when a transaction uses caching vs not + sentry_sdk.set_tag("stacktrace.loaded_from_cache", load_stacktrace_from_cache) with metrics.timer("event_manager.normalize_stacktraces_for_grouping", tags=metric_tags): with sentry_sdk.start_span(op="event_manager.normalize_stacktraces_for_grouping"): diff --git a/src/sentry/features/__init__.py b/src/sentry/features/__init__.py index 19f3be2eb25618..2f8f6462d4fcd8 100644 --- a/src/sentry/features/__init__.py +++ b/src/sentry/features/__init__.py @@ -56,7 +56,6 @@ # fmt: off # Features that don't use resource scoping -default_manager.add("app:enterprise-prevent-numeric-slugs", SystemFeature, FeatureHandlerStrategy.REMOTE) default_manager.add("auth:register", SystemFeature, FeatureHandlerStrategy.INTERNAL) default_manager.add("organizations:create", SystemFeature, FeatureHandlerStrategy.INTERNAL) @@ -74,7 +73,6 @@ default_manager.add("organizations:dashboards-mep", OrganizationFeature, FeatureHandlerStrategy.REMOTE) default_manager.add("organizations:dashboards-rh-widget", OrganizationFeature, FeatureHandlerStrategy.REMOTE) default_manager.add("organizations:dashboards-import", OrganizationFeature, FeatureHandlerStrategy.REMOTE) -default_manager.add("organizations:disable-sentryapps-on-broken", OrganizationFeature, FeatureHandlerStrategy.REMOTE) default_manager.add("organizations:discover", OrganizationFeature, FeatureHandlerStrategy.INTERNAL) default_manager.add("organizations:discover-events-rate-limit", OrganizationFeature, FeatureHandlerStrategy.REMOTE) default_manager.add("organizations:enterprise-data-secrecy", OrganizationFeature, FeatureHandlerStrategy.INTERNAL) @@ -268,7 +266,6 @@ default_manager.add("organizations:ds-sliding-window-org", OrganizationFeature, FeatureHandlerStrategy.INTERNAL) default_manager.add("organizations:ds-org-recalibration", OrganizationFeature, FeatureHandlerStrategy.INTERNAL) default_manager.add("organizations:github-disable-on-broken", OrganizationFeature, FeatureHandlerStrategy.REMOTE) -default_manager.add("organizations:slack-fatal-disable-on-broken", OrganizationFeature, FeatureHandlerStrategy.REMOTE) default_manager.add("organizations:sourcemaps-bundle-flat-file-indexing", OrganizationFeature, FeatureHandlerStrategy.REMOTE) default_manager.add("organizations:sourcemaps-upload-release-as-artifact-bundle", OrganizationFeature, FeatureHandlerStrategy.REMOTE) default_manager.add("organizations:recap-server", OrganizationFeature, FeatureHandlerStrategy.INTERNAL) diff --git a/src/sentry/grouping/enhancer/__init__.py b/src/sentry/grouping/enhancer/__init__.py index 67b2893d4d2239..cc92a04396ce39 100644 --- a/src/sentry/grouping/enhancer/__init__.py +++ b/src/sentry/grouping/enhancer/__init__.py @@ -511,11 +511,7 @@ def _update_frames_from_cached_values( Returns if the merged has correctly happened. """ frames_changed = False - # XXX: Test the fallback value - with metrics.timer( - f"{DATADOG_KEY}.cache.get.timer", - ): - changed_frames_values = cache.get(cache_key, {}) + changed_frames_values = cache.get(cache_key, {}) # This helps tracking changes in the hit/miss ratio of the cache metrics.incr( diff --git a/src/sentry/incidents/endpoints/organization_alert_rule_available_action_index.py b/src/sentry/incidents/endpoints/organization_alert_rule_available_action_index.py index e639dc7f52e28a..f9efd4cbc78b18 100644 --- a/src/sentry/incidents/endpoints/organization_alert_rule_available_action_index.py +++ b/src/sentry/incidents/endpoints/organization_alert_rule_available_action_index.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.constants import SentryAppStatus @@ -72,6 +73,10 @@ def build_action_response( @region_silo_endpoint class OrganizationAlertRuleAvailableActionIndexEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: """ Fetches actions that an alert rule can perform for an organization diff --git a/src/sentry/incidents/endpoints/organization_alert_rule_details.py b/src/sentry/incidents/endpoints/organization_alert_rule_details.py index b8704609b8c461..f9222d0b2724fe 100644 --- a/src/sentry/incidents/endpoints/organization_alert_rule_details.py +++ b/src/sentry/incidents/endpoints/organization_alert_rule_details.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.serializers import serialize from sentry.api.serializers.models.alert_rule import DetailedAlertRuleSerializer @@ -131,6 +132,12 @@ def remove_alert_rule(request: Request, organization, alert_rule): @region_silo_endpoint class OrganizationAlertRuleDetailsEndpoint(OrganizationAlertRuleEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, alert_rule) -> Response: """ Fetch a metric alert rule. diff --git a/src/sentry/incidents/endpoints/organization_alert_rule_index.py b/src/sentry/incidents/endpoints/organization_alert_rule_index.py index 5c1fd061cb03df..4abae5c8ddbcde 100644 --- a/src/sentry/incidents/endpoints/organization_alert_rule_index.py +++ b/src/sentry/incidents/endpoints/organization_alert_rule_index.py @@ -9,6 +9,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.bases.organization import OrganizationAlertRulePermission, OrganizationEndpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -120,6 +121,10 @@ def create_metric_alert(self, request, organization, project=None): @region_silo_endpoint class OrganizationCombinedRuleIndexEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization) -> Response: """ Fetches (metric) alert rules and legacy (issue alert) rules for an organization @@ -244,6 +249,10 @@ def get(self, request: Request, organization) -> Response: @region_silo_endpoint class OrganizationAlertRuleIndexEndpoint(OrganizationEndpoint, AlertRuleIndexMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationAlertRulePermission,) def get(self, request: Request, organization) -> Response: diff --git a/src/sentry/incidents/endpoints/organization_incident_activity_index.py b/src/sentry/incidents/endpoints/organization_incident_activity_index.py index bf02ef4b94f97d..83bf1154a2603c 100644 --- a/src/sentry/incidents/endpoints/organization_incident_activity_index.py +++ b/src/sentry/incidents/endpoints/organization_incident_activity_index.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.incident import IncidentEndpoint, IncidentPermission from sentry.api.paginator import OffsetPaginator @@ -10,6 +11,9 @@ @region_silo_endpoint class OrganizationIncidentActivityIndexEndpoint(IncidentEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (IncidentPermission,) def get(self, request: Request, organization, incident) -> Response: diff --git a/src/sentry/incidents/endpoints/organization_incident_comment_details.py b/src/sentry/incidents/endpoints/organization_incident_comment_details.py index a31bb8e16f28da..11ac5c8f276c0e 100644 --- a/src/sentry/incidents/endpoints/organization_incident_comment_details.py +++ b/src/sentry/incidents/endpoints/organization_incident_comment_details.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.incident import IncidentEndpoint, IncidentPermission from sentry.api.exceptions import ResourceDoesNotExist @@ -46,6 +47,10 @@ def convert_args(self, request: Request, activity_id, *args, **kwargs): @region_silo_endpoint class OrganizationIncidentCommentDetailsEndpoint(CommentDetailsEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (IncidentPermission,) def delete(self, request: Request, organization, incident, activity) -> Response: diff --git a/src/sentry/incidents/endpoints/organization_incident_comment_index.py b/src/sentry/incidents/endpoints/organization_incident_comment_index.py index 552c1942f4ebd0..871632e21cf520 100644 --- a/src/sentry/incidents/endpoints/organization_incident_comment_index.py +++ b/src/sentry/incidents/endpoints/organization_incident_comment_index.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.incident import IncidentEndpoint, IncidentPermission from sentry.api.fields.actor import ActorField @@ -22,6 +23,9 @@ class CommentSerializer(serializers.Serializer, MentionsMixin): @region_silo_endpoint class OrganizationIncidentCommentIndexEndpoint(IncidentEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (IncidentPermission,) def post(self, request: Request, organization, incident) -> Response: diff --git a/src/sentry/incidents/endpoints/organization_incident_details.py b/src/sentry/incidents/endpoints/organization_incident_details.py index 7a9c674b058864..7c24c20a075619 100644 --- a/src/sentry/incidents/endpoints/organization_incident_details.py +++ b/src/sentry/incidents/endpoints/organization_incident_details.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.incident import IncidentEndpoint, IncidentPermission from sentry.api.serializers import serialize @@ -28,6 +29,10 @@ def validate_status(self, value): @region_silo_endpoint class OrganizationIncidentDetailsEndpoint(IncidentEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } permission_classes = (IncidentPermission,) def get(self, request: Request, organization, incident) -> Response: diff --git a/src/sentry/incidents/endpoints/organization_incident_index.py b/src/sentry/incidents/endpoints/organization_incident_index.py index 33ad0f9a26ae09..d01fa8dcee1eab 100644 --- a/src/sentry/incidents/endpoints/organization_incident_index.py +++ b/src/sentry/incidents/endpoints/organization_incident_index.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.incident import IncidentPermission from sentry.api.bases.organization import OrganizationEndpoint @@ -27,6 +28,9 @@ @region_silo_endpoint class OrganizationIncidentIndexEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (IncidentPermission,) def get(self, request: Request, organization) -> Response: diff --git a/src/sentry/incidents/endpoints/organization_incident_seen.py b/src/sentry/incidents/endpoints/organization_incident_seen.py index 28abb3cb824acd..011c82d1b2c971 100644 --- a/src/sentry/incidents/endpoints/organization_incident_seen.py +++ b/src/sentry/incidents/endpoints/organization_incident_seen.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.incident import IncidentEndpoint, IncidentPermission from sentry.incidents.logic import set_incident_seen @@ -8,6 +9,9 @@ @region_silo_endpoint class OrganizationIncidentSeenEndpoint(IncidentEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (IncidentPermission,) def post(self, request: Request, organization, incident) -> Response: diff --git a/src/sentry/incidents/endpoints/organization_incident_subscription_index.py b/src/sentry/incidents/endpoints/organization_incident_subscription_index.py index 36d3f0db548d62..ed21abda4f6a6c 100644 --- a/src/sentry/incidents/endpoints/organization_incident_subscription_index.py +++ b/src/sentry/incidents/endpoints/organization_incident_subscription_index.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.incident import IncidentEndpoint, IncidentPermission from sentry.incidents.logic import subscribe_to_incident, unsubscribe_from_incident @@ -19,6 +20,10 @@ class IncidentSubscriptionPermission(IncidentPermission): @region_silo_endpoint class OrganizationIncidentSubscriptionIndexEndpoint(IncidentEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (IncidentSubscriptionPermission,) def post(self, request: Request, organization, incident) -> Response: diff --git a/src/sentry/incidents/endpoints/project_alert_rule_details.py b/src/sentry/incidents/endpoints/project_alert_rule_details.py index dbe35d321eee79..a14112e0d48dea 100644 --- a/src/sentry/incidents/endpoints/project_alert_rule_details.py +++ b/src/sentry/incidents/endpoints/project_alert_rule_details.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.incidents.endpoints.bases import ProjectAlertRuleEndpoint from sentry.incidents.endpoints.organization_alert_rule_details import ( @@ -12,6 +13,12 @@ @region_silo_endpoint class ProjectAlertRuleDetailsEndpoint(ProjectAlertRuleEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + "PUT": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, alert_rule) -> Response: """ Fetch a metric alert rule. @deprecated. Use OrganizationAlertRuleDetailsEndpoint instead. diff --git a/src/sentry/incidents/endpoints/project_alert_rule_index.py b/src/sentry/incidents/endpoints/project_alert_rule_index.py index 054c42cee8e9fa..2d34a8324cae41 100644 --- a/src/sentry/incidents/endpoints/project_alert_rule_index.py +++ b/src/sentry/incidents/endpoints/project_alert_rule_index.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectAlertRulePermission, ProjectEndpoint from sentry.api.paginator import CombinedQuerysetIntermediary, CombinedQuerysetPaginator @@ -15,6 +16,10 @@ @region_silo_endpoint class ProjectCombinedRuleIndexEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project) -> Response: """ Fetches alert rules and legacy rules for a project @@ -45,6 +50,10 @@ def get(self, request: Request, project) -> Response: @region_silo_endpoint class ProjectAlertRuleIndexEndpoint(ProjectEndpoint, AlertRuleIndexMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = (ProjectAlertRulePermission,) def get(self, request: Request, project) -> Response: diff --git a/src/sentry/incidents/endpoints/project_alert_rule_task_details.py b/src/sentry/incidents/endpoints/project_alert_rule_task_details.py index 6d73859989f556..81726374403841 100644 --- a/src/sentry/incidents/endpoints/project_alert_rule_task_details.py +++ b/src/sentry/incidents/endpoints/project_alert_rule_task_details.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectSettingPermission from sentry.api.serializers import serialize @@ -11,6 +12,9 @@ @region_silo_endpoint class ProjectAlertRuleTaskDetailsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = [ProjectSettingPermission] def get(self, request: Request, project, task_uuid) -> Response: diff --git a/src/sentry/integrations/bitbucket/descriptor.py b/src/sentry/integrations/bitbucket/descriptor.py index 9e09926c507416..e41c16c1bec35f 100644 --- a/src/sentry/integrations/bitbucket/descriptor.py +++ b/src/sentry/integrations/bitbucket/descriptor.py @@ -1,6 +1,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.integrations.bitbucket.integration import scopes from sentry.utils.http import absolute_uri @@ -10,6 +11,9 @@ @control_silo_endpoint class BitbucketDescriptorEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } authentication_classes = () permission_classes = () diff --git a/src/sentry/integrations/bitbucket/installed.py b/src/sentry/integrations/bitbucket/installed.py index 220ab270dd5fc1..cc4f1d05fc4bf8 100644 --- a/src/sentry/integrations/bitbucket/installed.py +++ b/src/sentry/integrations/bitbucket/installed.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.integrations.pipeline import ensure_integration @@ -10,6 +11,9 @@ @control_silo_endpoint class BitbucketInstalledEndpoint(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = () permission_classes = () diff --git a/src/sentry/integrations/bitbucket/search.py b/src/sentry/integrations/bitbucket/search.py index ac99bd085d89d0..b65790b0604d01 100644 --- a/src/sentry/integrations/bitbucket/search.py +++ b/src/sentry/integrations/bitbucket/search.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.integration import IntegrationEndpoint from sentry.integrations.bitbucket.integration import BitbucketIntegration @@ -14,6 +15,10 @@ @control_silo_endpoint class BitbucketSearchEndpoint(IntegrationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, organization, integration_id, **kwds) -> Response: try: integration = Integration.objects.get( diff --git a/src/sentry/integrations/bitbucket/uninstalled.py b/src/sentry/integrations/bitbucket/uninstalled.py index 2715adcf780ab3..bdd3674e01ad5e 100644 --- a/src/sentry/integrations/bitbucket/uninstalled.py +++ b/src/sentry/integrations/bitbucket/uninstalled.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.constants import ObjectStatus from sentry.integrations.utils import AtlassianConnectValidationError, get_integration_from_jwt @@ -12,6 +13,9 @@ @control_silo_endpoint class BitbucketUninstalledEndpoint(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = () permission_classes = () diff --git a/src/sentry/integrations/bitbucket/webhook.py b/src/sentry/integrations/bitbucket/webhook.py index a46e2e57167a11..791156da0670c3 100644 --- a/src/sentry/integrations/bitbucket/webhook.py +++ b/src/sentry/integrations/bitbucket/webhook.py @@ -9,6 +9,7 @@ from django.views.decorators.csrf import csrf_exempt from rest_framework.request import Request +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.integrations.bitbucket.constants import BITBUCKET_IP_RANGES, BITBUCKET_IPS from sentry.models import Commit, CommitAuthor, Organization, Repository @@ -107,6 +108,9 @@ def __call__(self, organization, event): @region_silo_endpoint class BitbucketWebhookEndpoint(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } permission_classes = () _handlers = {"repo:push": PushEventWebhook} diff --git a/src/sentry/integrations/discord/webhooks/base.py b/src/sentry/integrations/discord/webhooks/base.py index 3ee9c128742f56..de97397cbef693 100644 --- a/src/sentry/integrations/discord/webhooks/base.py +++ b/src/sentry/integrations/discord/webhooks/base.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import analytics +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.integrations.discord.requests.base import DiscordRequest, DiscordRequestError from sentry.integrations.discord.webhooks.command import DiscordCommandHandler @@ -16,6 +17,9 @@ @region_silo_endpoint class DiscordInteractionsEndpoint(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } """ All Discord -> Sentry communication will come through our interactions endpoint. We need to figure out what Discord is sending us and direct the diff --git a/src/sentry/integrations/github/search.py b/src/sentry/integrations/github/search.py index d685c99c7b6da9..496721fe96c9b1 100644 --- a/src/sentry/integrations/github/search.py +++ b/src/sentry/integrations/github/search.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.integration import IntegrationEndpoint from sentry.integrations.github.integration import build_repository_query @@ -13,6 +14,9 @@ @control_silo_endpoint class GithubSharedSearchEndpoint(IntegrationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """NOTE: This endpoint is a shared search endpoint for Github and Github Enterprise integrations.""" def get( diff --git a/src/sentry/integrations/github/webhook.py b/src/sentry/integrations/github/webhook.py index 0544f7a406d9a3..806ed030bc7ef8 100644 --- a/src/sentry/integrations/github/webhook.py +++ b/src/sentry/integrations/github/webhook.py @@ -15,6 +15,7 @@ from rest_framework.request import Request from sentry import analytics, features, options +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.constants import ObjectStatus from sentry.integrations.utils.scope import clear_tags_and_context @@ -552,6 +553,9 @@ def handle(self, request: Request) -> HttpResponse: @region_silo_endpoint class GitHubIntegrationsWebhookEndpoint(GitHubWebhookBase): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } _handlers = { "push": PushEventWebhook, "pull_request": PullRequestEventWebhook, diff --git a/src/sentry/integrations/github_enterprise/webhook.py b/src/sentry/integrations/github_enterprise/webhook.py index 9abe7164f2a1d4..a4361c327e77ad 100644 --- a/src/sentry/integrations/github_enterprise/webhook.py +++ b/src/sentry/integrations/github_enterprise/webhook.py @@ -10,6 +10,7 @@ from django.views.decorators.csrf import csrf_exempt from rest_framework.request import Request +from sentry.api.api_publish_status import ApiPublishStatus from sentry.integrations.github.webhook import ( InstallationEventWebhook, PullRequestEventWebhook, @@ -177,6 +178,9 @@ def handle(self, request: Request) -> HttpResponse: @region_silo_endpoint class GitHubEnterpriseWebhookEndpoint(GitHubEnterpriseWebhookBase): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } _handlers = { "push": GitHubEnterprisePushEventWebhook, "pull_request": GitHubEnterprisePullRequestEventWebhook, diff --git a/src/sentry/integrations/gitlab/search.py b/src/sentry/integrations/gitlab/search.py index e9207512376713..fd05f9ba4854b8 100644 --- a/src/sentry/integrations/gitlab/search.py +++ b/src/sentry/integrations/gitlab/search.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.integration import IntegrationEndpoint from sentry.models import Integration @@ -12,6 +13,10 @@ @control_silo_endpoint class GitlabIssueSearchEndpoint(IntegrationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get( self, request: Request, organization: RpcOrganization, integration_id: int, **kwds: Any ) -> Response: diff --git a/src/sentry/integrations/gitlab/webhooks.py b/src/sentry/integrations/gitlab/webhooks.py index 8961dc9b56df90..b21d00c3e52cad 100644 --- a/src/sentry/integrations/gitlab/webhooks.py +++ b/src/sentry/integrations/gitlab/webhooks.py @@ -12,6 +12,7 @@ from django.views.decorators.csrf import csrf_exempt from rest_framework.request import Request +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.integrations.utils.scope import clear_tags_and_context from sentry.models import Commit, CommitAuthor, Organization, PullRequest, Repository @@ -232,6 +233,9 @@ def _get_external_id(self, request, extra) -> Tuple[str, str] | HttpResponse: @region_silo_endpoint class GitlabWebhookEndpoint(Endpoint, GitlabWebhookMixin): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = () permission_classes = () provider = "gitlab" diff --git a/src/sentry/integrations/jira/endpoints/descriptor.py b/src/sentry/integrations/jira/endpoints/descriptor.py index 80803a964f5004..1a91be77c0ac75 100644 --- a/src/sentry/integrations/jira/endpoints/descriptor.py +++ b/src/sentry/integrations/jira/endpoints/descriptor.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.utils.assets import get_frontend_app_asset_url from sentry.utils.http import absolute_uri @@ -19,6 +20,9 @@ @control_silo_endpoint class JiraDescriptorEndpoint(Endpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """ Provides the metadata needed by Jira to setup an instance of the Sentry integration within Jira. Only used by on-prem orgs and devs setting up local instances of the integration. (Sentry SAAS diff --git a/src/sentry/integrations/jira/endpoints/search.py b/src/sentry/integrations/jira/endpoints/search.py index 70a71ea8e056c8..74253b2da16b63 100644 --- a/src/sentry/integrations/jira/endpoints/search.py +++ b/src/sentry/integrations/jira/endpoints/search.py @@ -5,6 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.integration import IntegrationEndpoint from sentry.models import Integration @@ -17,6 +18,9 @@ @control_silo_endpoint class JiraSearchEndpoint(IntegrationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """ Called by our front end when it needs to make requests to Jira's API for data. """ diff --git a/src/sentry/integrations/jira/webhooks/installed.py b/src/sentry/integrations/jira/webhooks/installed.py index 87e7b3825843d2..e8bb3e57cd6dd6 100644 --- a/src/sentry/integrations/jira/webhooks/installed.py +++ b/src/sentry/integrations/jira/webhooks/installed.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.integrations.pipeline import ensure_integration from sentry.integrations.utils import authenticate_asymmetric_jwt, verify_claims @@ -15,6 +16,9 @@ @control_silo_endpoint class JiraSentryInstalledWebhook(JiraWebhookBase): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } """ Webhook hit by Jira whenever someone installs the Sentry integration in their Jira instance. """ diff --git a/src/sentry/integrations/jira/webhooks/issue_updated.py b/src/sentry/integrations/jira/webhooks/issue_updated.py index 510b69bbec7216..890435a70e2345 100644 --- a/src/sentry/integrations/jira/webhooks/issue_updated.py +++ b/src/sentry/integrations/jira/webhooks/issue_updated.py @@ -9,6 +9,7 @@ from rest_framework.response import Response from sentry_sdk import Scope +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.integrations.utils import get_integration_from_jwt from sentry.integrations.utils.scope import bind_org_context_from_integration @@ -22,6 +23,9 @@ @region_silo_endpoint class JiraIssueUpdatedWebhook(JiraWebhookBase): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } """ Webhook hit by Jira whenever an issue is updated in Jira's database. """ diff --git a/src/sentry/integrations/jira/webhooks/uninstalled.py b/src/sentry/integrations/jira/webhooks/uninstalled.py index 7bb9008767d850..a40e3f3d12380e 100644 --- a/src/sentry/integrations/jira/webhooks/uninstalled.py +++ b/src/sentry/integrations/jira/webhooks/uninstalled.py @@ -2,6 +2,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.constants import ObjectStatus from sentry.integrations.utils import get_integration_from_jwt @@ -13,6 +14,9 @@ @control_silo_endpoint class JiraSentryUninstalledWebhook(JiraWebhookBase): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } """ Webhook hit by Jira whenever someone uninstalls the Sentry integration from their Jira instance. """ diff --git a/src/sentry/integrations/jira_server/search.py b/src/sentry/integrations/jira_server/search.py index a16e709dadfa0b..4c326363c5a226 100644 --- a/src/sentry/integrations/jira_server/search.py +++ b/src/sentry/integrations/jira_server/search.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.integration import IntegrationEndpoint from sentry.models import Integration @@ -15,6 +16,9 @@ @control_silo_endpoint class JiraServerSearchEndpoint(IntegrationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } provider = "jira_server" def _get_integration(self, organization, integration_id): diff --git a/src/sentry/integrations/jira_server/webhooks.py b/src/sentry/integrations/jira_server/webhooks.py index 28dc9ec761d8e8..f8aeb6aa403458 100644 --- a/src/sentry/integrations/jira_server/webhooks.py +++ b/src/sentry/integrations/jira_server/webhooks.py @@ -5,6 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.integrations.jira_server.utils import handle_assignee_change, handle_status_change from sentry.integrations.utils.scope import clear_tags_and_context @@ -44,6 +45,9 @@ def get_integration_from_token(token): @control_silo_endpoint class JiraServerIssueUpdatedWebhook(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = () permission_classes = () diff --git a/src/sentry/integrations/msteams/webhook.py b/src/sentry/integrations/msteams/webhook.py index d061d9349e87d1..2a76155509e474 100644 --- a/src/sentry/integrations/msteams/webhook.py +++ b/src/sentry/integrations/msteams/webhook.py @@ -10,6 +10,7 @@ from sentry import analytics, audit_log, eventstore, features, options from sentry.api import client +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.models import ApiKey, Group, Rule from sentry.models.activity import ActivityIntegration @@ -160,6 +161,9 @@ def get_integration_from_payload(self, request: HttpRequest) -> RpcIntegration | @region_silo_endpoint class MsTeamsWebhookEndpoint(Endpoint, MsTeamsWebhookMixin): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = () permission_classes = () provider = "msteams" diff --git a/src/sentry/integrations/opsgenie/integration.py b/src/sentry/integrations/opsgenie/integration.py index 9b369857716945..de3769a2b4effc 100644 --- a/src/sentry/integrations/opsgenie/integration.py +++ b/src/sentry/integrations/opsgenie/integration.py @@ -29,9 +29,8 @@ DESCRIPTION = """ Trigger alerts in Opsgenie from Sentry. -Opsgenie is a cloud-based service for dev & ops teams, providing reliable alerts, on-call schedule management and escalations. -Opsgenie integrates with monitoring tools & services and ensures that the right people are notified via email, SMS, phone calls, -and iOS & Android push notifications. +Opsgenie is a cloud-based service for dev and ops teams, providing reliable alerts, on-call schedule management, and escalations. +Opsgenie integrates with monitoring tools and services to ensure that the right people are notified via email, SMS, phone, and iOS/Android push notifications. """ @@ -71,14 +70,14 @@ class InstallationForm(forms.Form): ) provider = forms.CharField( label=_("Account Name"), - help_text=_("Example: 'example' for https://example.app.opsgenie.com/"), + help_text=_("Example: 'acme' for https://acme.app.opsgenie.com/"), widget=forms.TextInput(), ) api_key = forms.CharField( label=("Opsgenie Integration Key"), help_text=_( - "Optionally add your first integration key for sending alerts. You can rename this key later." + "Optionally, add your first integration key for sending alerts. You can rename this key later." ), widget=forms.TextInput(), required=False, @@ -120,8 +119,7 @@ def get_organization_config(self) -> Sequence[Any]: "name": "team_table", "type": "table", "label": "Opsgenie integrations", - "help": "If integration keys need to be updated, deleted, or added manually please do so here. Your keys must be associated with a 'Sentry' Integration in Opsgenie. \ - Alert rules will need to be individually updated for any key additions or deletions.", + "help": "Your keys have to be associated with a Sentry integration in Opsgenie. You can update, delete, or add them here. You’ll need to update alert rules individually for any added or deleted keys.", "addButtonText": "", "columnLabels": { "team": "Label", diff --git a/src/sentry/integrations/request_buffer.py b/src/sentry/integrations/request_buffer.py index bcec68987537f6..7aab420b30dbdb 100644 --- a/src/sentry/integrations/request_buffer.py +++ b/src/sentry/integrations/request_buffer.py @@ -5,7 +5,7 @@ from sentry.utils import redis BUFFER_SIZE = 30 # 30 days -KEY_EXPIRY = 60 * 60 * 24 * 30 # 30 days +KEY_EXPIRY = 60 * 60 * 24 * 8 # 8 days BROKEN_RANGE_DAYS = 7 # 7 days diff --git a/src/sentry/integrations/slack/webhooks/action.py b/src/sentry/integrations/slack/webhooks/action.py index b87f93d8f6181f..22b9d85756296e 100644 --- a/src/sentry/integrations/slack/webhooks/action.py +++ b/src/sentry/integrations/slack/webhooks/action.py @@ -11,6 +11,7 @@ from sentry import analytics from sentry.api import ApiClient, client +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.api.helpers.group_index import update_groups from sentry.auth.access import from_member @@ -130,6 +131,9 @@ def _is_message(data: Mapping[str, Any]) -> bool: @region_silo_endpoint class SlackActionEndpoint(Endpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = () permission_classes = () slack_request_class = SlackActionRequest diff --git a/src/sentry/integrations/slack/webhooks/command.py b/src/sentry/integrations/slack/webhooks/command.py index 039b1de6161264..6aa2a8c720a5b0 100644 --- a/src/sentry/integrations/slack/webhooks/command.py +++ b/src/sentry/integrations/slack/webhooks/command.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.integrations.slack.message_builder.disconnected import SlackDisconnectedMessageBuilder from sentry.integrations.slack.requests.base import SlackDMRequest, SlackRequestError @@ -45,6 +46,9 @@ def is_team_linked_to_channel(organization: Organization, slack_request: SlackDM @region_silo_endpoint class SlackCommandsEndpoint(SlackDMEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = () permission_classes = () slack_request_class = SlackCommandRequest diff --git a/src/sentry/integrations/slack/webhooks/event.py b/src/sentry/integrations/slack/webhooks/event.py index 553a51d2f1ea59..2cfccab45641e3 100644 --- a/src/sentry/integrations/slack/webhooks/event.py +++ b/src/sentry/integrations/slack/webhooks/event.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import analytics, features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.integrations.slack.client import SlackClient from sentry.integrations.slack.message_builder.help import SlackHelpMessageBuilder @@ -29,6 +30,9 @@ @region_silo_endpoint class SlackEventEndpoint(SlackDMEndpoint): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } """ XXX(dcramer): a lot of this is copied from sentry-plugins right now, and will need refactoring """ diff --git a/src/sentry/integrations/vercel/webhook.py b/src/sentry/integrations/vercel/webhook.py index 33e8797ed73623..d913fb93b6b783 100644 --- a/src/sentry/integrations/vercel/webhook.py +++ b/src/sentry/integrations/vercel/webhook.py @@ -13,6 +13,7 @@ from sentry_sdk import configure_scope from sentry import VERSION, audit_log, http, options +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, control_silo_endpoint from sentry.models import ( Integration, @@ -127,6 +128,10 @@ def get_payload_and_token( @control_silo_endpoint class VercelWebhookEndpoint(Endpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = () permission_classes = () provider = "vercel" diff --git a/src/sentry/integrations/vsts/search.py b/src/sentry/integrations/vsts/search.py index 33763a6f7d4ef8..31a2c05ef550a3 100644 --- a/src/sentry/integrations/vsts/search.py +++ b/src/sentry/integrations/vsts/search.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.bases.integration import IntegrationEndpoint from sentry.models import Integration @@ -12,6 +13,10 @@ @control_silo_endpoint class VstsSearchEndpoint(IntegrationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get( self, request: Request, organization: RpcOrganization, integration_id: int, **kwds: Any ) -> Response: diff --git a/src/sentry/integrations/vsts/webhooks.py b/src/sentry/integrations/vsts/webhooks.py index 9ff1040e7eb71a..47019631b37f02 100644 --- a/src/sentry/integrations/vsts/webhooks.py +++ b/src/sentry/integrations/vsts/webhooks.py @@ -8,6 +8,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import Endpoint, region_silo_endpoint from sentry.integrations.mixins import IssueSyncMixin from sentry.integrations.utils import sync_group_assignee_inbound @@ -32,6 +33,9 @@ def get_external_id(self, request: Request) -> str: @region_silo_endpoint class WorkItemWebhook(Endpoint, VstsWebhookMixin): + publish_status = { + "POST": ApiPublishStatus.UNKNOWN, + } authentication_classes = () permission_classes = () diff --git a/src/sentry/issues/grouptype.py b/src/sentry/issues/grouptype.py index cb922a7198dddf..1114df108d2642 100644 --- a/src/sentry/issues/grouptype.py +++ b/src/sentry/issues/grouptype.py @@ -319,9 +319,9 @@ class PerformanceHTTPOverheadGroupType(PerformanceGroupTypeDefaults, GroupType): @dataclass(frozen=True) -class PerformanceP95TransactionDurationRegressionGroupType(PerformanceGroupTypeDefaults, GroupType): +class PerformanceDurationRegressionGroupType(PerformanceGroupTypeDefaults, GroupType): type_id = 1017 - slug = "performance_p95_transaction_duration_regression" + slug = "performance_duration_regression" description = "Duration Regression" noise_config = NoiseConfig(ignore_limit=0) category = GroupCategory.PERFORMANCE.value diff --git a/src/sentry/migrations/0539_add_last_state_change_monitorenv.py b/src/sentry/migrations/0539_add_last_state_change_monitorenv.py new file mode 100644 index 00000000000000..7d49bc6ea6b771 --- /dev/null +++ b/src/sentry/migrations/0539_add_last_state_change_monitorenv.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.20 on 2023-08-29 23:33 + +from django.db import migrations, models + +from sentry.new_migrations.migrations import CheckedMigration + + +class Migration(CheckedMigration): + # This flag is used to mark that a migration shouldn't be automatically run in production. For + # the most part, this should only be used for operations where it's safe to run the migration + # after your code has deployed. So this should not be used for most operations that alter the + # schema of a table. + # Here are some things that make sense to mark as dangerous: + # - Large data migrations. Typically we want these to be run manually by ops so that they can + # be monitored and not block the deploy for a long period of time while they run. + # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to + # have ops run this and not block the deploy. Note that while adding an index is a schema + # change, it's completely safe to run the operation after the code has deployed. + is_dangerous = False + + dependencies = [ + ("sentry", "0538_remove_name_data_from_rule"), + ] + + operations = [ + migrations.AddField( + model_name="monitorenvironment", + name="last_state_change", + field=models.DateTimeField(null=True), + ), + ] diff --git a/src/sentry/models/actor.py b/src/sentry/models/actor.py index ab932f71af003e..f7f35c881a2bf6 100644 --- a/src/sentry/models/actor.py +++ b/src/sentry/models/actor.py @@ -10,7 +10,7 @@ from rest_framework import serializers from sentry.backup.dependencies import PrimaryKeyMap -from sentry.backup.scopes import RelocationScope +from sentry.backup.scopes import ImportScope, RelocationScope from sentry.db.models import Model, region_silo_only_model from sentry.db.models.fields.foreignkey import FlexibleForeignKey from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey @@ -143,8 +143,8 @@ def get_actor_identifier(self): return self.get_actor_tuple().get_actor_identifier() # TODO(hybrid-cloud): actor refactor. Remove this method when done. - def _normalize_before_relocation_import(self, pk_map: PrimaryKeyMap) -> int: - old_pk = super()._normalize_before_relocation_import(pk_map) + def _normalize_before_relocation_import(self, pk_map: PrimaryKeyMap, scope: ImportScope) -> int: + old_pk = super()._normalize_before_relocation_import(pk_map, scope) # `Actor` and `Team` have a direct circular dependency between them for the time being due # to an ongoing refactor (that is, `Actor` foreign keys directly into `Team`, and `Team` diff --git a/src/sentry/models/outbox.py b/src/sentry/models/outbox.py index 03a07f011c7fac..0bbe280c374f0c 100644 --- a/src/sentry/models/outbox.py +++ b/src/sentry/models/outbox.py @@ -132,6 +132,7 @@ class WebhookProviderIdentifier(IntEnum): GITHUB_ENTERPRISE = 8 BITBUCKET_SERVER = 9 LEGACY_PLUGIN = 10 + GETSENTRY = 11 def _ensure_not_null(k: str, v: Any) -> Any: diff --git a/src/sentry/models/team.py b/src/sentry/models/team.py index d265e0768f3048..3674f5ae20cb0c 100644 --- a/src/sentry/models/team.py +++ b/src/sentry/models/team.py @@ -12,7 +12,7 @@ from sentry.app import env from sentry.backup.dependencies import PrimaryKeyMap -from sentry.backup.scopes import RelocationScope +from sentry.backup.scopes import ImportScope, RelocationScope from sentry.constants import ObjectStatus from sentry.db.models import ( BaseManager, @@ -372,9 +372,9 @@ def get_member_actor_ids(self): # TODO(hybrid-cloud): actor refactor. Remove this method when done. def write_relocation_import( - self, pk_map: PrimaryKeyMap, obj: DeserializedObject + self, pk_map: PrimaryKeyMap, obj: DeserializedObject, scope: ImportScope ) -> Optional[Tuple[int, int]]: - written = super().write_relocation_import(pk_map, obj) + written = super().write_relocation_import(pk_map, obj, scope) if written is not None: (_, new_pk) = written diff --git a/src/sentry/models/user.py b/src/sentry/models/user.py index bdf3e59075ccfb..219a91773df8cc 100644 --- a/src/sentry/models/user.py +++ b/src/sentry/models/user.py @@ -15,7 +15,8 @@ from bitfield import TypedClassBitField from sentry.auth.authenticators import available_authenticators -from sentry.backup.scopes import RelocationScope +from sentry.backup.dependencies import PrimaryKeyMap +from sentry.backup.scopes import ImportScope, RelocationScope from sentry.db.models import ( BaseManager, BaseModel, @@ -369,6 +370,16 @@ def get_orgs_require_2fa(self): def clear_lost_passwords(self): LostPasswordHash.objects.filter(user=self).delete() + def _normalize_before_relocation_import(self, pk_map: PrimaryKeyMap, scope: ImportScope) -> int: + old_pk = super()._normalize_before_relocation_import(pk_map, scope) + if scope != ImportScope.Global: + self.is_staff = False + self.is_superuser = False + + # TODO(getsentry/team-ospo#181): Handle usernames that already exist. + + return old_pk + # HACK(dcramer): last_login needs nullable for Django 1.8 User._meta.get_field("last_login").null = True diff --git a/src/sentry/models/useremail.py b/src/sentry/models/useremail.py index 2cea5f0512d526..6e28e40845a199 100644 --- a/src/sentry/models/useremail.py +++ b/src/sentry/models/useremail.py @@ -12,7 +12,7 @@ from django.utils.translation import gettext_lazy as _ from sentry.backup.dependencies import PrimaryKeyMap -from sentry.backup.scopes import RelocationScope +from sentry.backup.scopes import ImportScope, RelocationScope from sentry.db.models import ( BaseManager, FlexibleForeignKey, @@ -88,9 +88,9 @@ def get_primary_email(cls, user: User) -> UserEmail: # with `sentry.user` simultaneously? Will need to make more robust user handling logic, and to # test what happens when a UserEmail already exists. def write_relocation_import( - self, pk_map: PrimaryKeyMap, _: DeserializedObject + self, pk_map: PrimaryKeyMap, obj: DeserializedObject, scope: ImportScope ) -> Optional[Tuple[int, int]]: - old_pk = super()._normalize_before_relocation_import(pk_map) + old_pk = super()._normalize_before_relocation_import(pk_map, scope) (useremail, _) = self.__class__.objects.get_or_create( user=self.user, email=self.email, defaults=model_to_dict(self) ) diff --git a/src/sentry/models/userpermission.py b/src/sentry/models/userpermission.py index 365f4544738a6e..9c83e384300160 100644 --- a/src/sentry/models/userpermission.py +++ b/src/sentry/models/userpermission.py @@ -2,12 +2,13 @@ from django.db import models +from sentry.backup.mixins import SanitizeUserImportsMixin from sentry.backup.scopes import RelocationScope from sentry.db.models import FlexibleForeignKey, Model, control_silo_only_model, sane_repr @control_silo_only_model -class UserPermission(Model): +class UserPermission(SanitizeUserImportsMixin, Model): """ Permissions are applied to administrative users and control explicit scope-like permissions within the API. diff --git a/src/sentry/models/userrole.py b/src/sentry/models/userrole.py index 316a452096b41e..cdb8f0f6589807 100644 --- a/src/sentry/models/userrole.py +++ b/src/sentry/models/userrole.py @@ -3,6 +3,7 @@ from django.conf import settings from django.db import models +from sentry.backup.mixins import SanitizeUserImportsMixin from sentry.backup.scopes import RelocationScope from sentry.db.models import ArrayField, DefaultFieldsModel, control_silo_only_model, sane_repr from sentry.db.models.fields.foreignkey import FlexibleForeignKey @@ -11,7 +12,7 @@ @control_silo_only_model -class UserRole(DefaultFieldsModel): +class UserRole(SanitizeUserImportsMixin, DefaultFieldsModel): """ Roles are applied to administrative users and apply a set of `UserPermission`. """ @@ -41,7 +42,7 @@ def permissions_for_user(cls, user_id: int) -> FrozenSet[str]: @control_silo_only_model -class UserRoleUser(DefaultFieldsModel): +class UserRoleUser(SanitizeUserImportsMixin, DefaultFieldsModel): __relocation_scope__ = RelocationScope.User user = FlexibleForeignKey("sentry.User") diff --git a/src/sentry/monitors/endpoints/monitor_ingest_checkin_details.py b/src/sentry/monitors/endpoints/monitor_ingest_checkin_details.py index 21138882534e9a..027d4a56444cde 100644 --- a/src/sentry/monitors/endpoints/monitor_ingest_checkin_details.py +++ b/src/sentry/monitors/endpoints/monitor_ingest_checkin_details.py @@ -6,6 +6,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.serializers import serialize from sentry.apidocs.constants import ( @@ -30,6 +31,9 @@ @region_silo_endpoint @extend_schema(tags=["Crons"]) class MonitorIngestCheckInDetailsEndpoint(MonitorIngestEndpoint): + publish_status = { + "PUT": ApiPublishStatus.PUBLIC, + } public = {"PUT"} @extend_schema( diff --git a/src/sentry/monitors/endpoints/monitor_ingest_checkin_index.py b/src/sentry/monitors/endpoints/monitor_ingest_checkin_index.py index b9a53f3a5c3c8d..e20e958cf4df0a 100644 --- a/src/sentry/monitors/endpoints/monitor_ingest_checkin_index.py +++ b/src/sentry/monitors/endpoints/monitor_ingest_checkin_index.py @@ -10,6 +10,7 @@ from rest_framework.response import Response from sentry import ratelimits +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.serializers import serialize from sentry.apidocs.constants import RESPONSE_BAD_REQUEST, RESPONSE_NOT_FOUND, RESPONSE_UNAUTHORIZED @@ -43,6 +44,9 @@ @region_silo_endpoint @extend_schema(tags=["Crons"]) class MonitorIngestCheckInIndexEndpoint(MonitorIngestEndpoint): + publish_status = { + "POST": ApiPublishStatus.PUBLIC, + } public = {"POST"} rate_limits = RateLimitConfig( diff --git a/src/sentry/monitors/endpoints/organization_monitor_details.py b/src/sentry/monitors/endpoints/organization_monitor_details.py index 31a734b0cf8f3d..f7e6a00f5ba4e8 100644 --- a/src/sentry/monitors/endpoints/organization_monitor_details.py +++ b/src/sentry/monitors/endpoints/organization_monitor_details.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.exceptions import ParameterValidationError from sentry.api.helpers.environments import get_environments @@ -33,6 +34,11 @@ @region_silo_endpoint @extend_schema(tags=["Crons"]) class OrganizationMonitorDetailsEndpoint(MonitorEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.PUBLIC, + "GET": ApiPublishStatus.PUBLIC, + "PUT": ApiPublishStatus.PUBLIC, + } public = {"GET", "PUT", "DELETE"} @extend_schema( diff --git a/src/sentry/monitors/endpoints/organization_monitor_index.py b/src/sentry/monitors/endpoints/organization_monitor_index.py index 77d6e8f04ee080..6ab316c050aaba 100644 --- a/src/sentry/monitors/endpoints/organization_monitor_index.py +++ b/src/sentry/monitors/endpoints/organization_monitor_index.py @@ -4,6 +4,7 @@ from drf_spectacular.utils import extend_schema from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects from sentry.api.bases.organization import OrganizationEndpoint @@ -65,6 +66,10 @@ def map_value_to_constant(constant, value): @region_silo_endpoint @extend_schema(tags=["Crons"]) class OrganizationMonitorIndexEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.PUBLIC, + "POST": ApiPublishStatus.PUBLIC, + } public = {"GET", "POST"} permission_classes = (OrganizationMonitorPermission,) diff --git a/src/sentry/monitors/endpoints/organization_monitor_index_stats.py b/src/sentry/monitors/endpoints/organization_monitor_index_stats.py index d4e84700d9f281..2f5d7e2be50b45 100644 --- a/src/sentry/monitors/endpoints/organization_monitor_index_stats.py +++ b/src/sentry/monitors/endpoints/organization_monitor_index_stats.py @@ -9,6 +9,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import StatsMixin, region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.helpers.environments import get_environments @@ -29,6 +30,10 @@ def normalize_to_epoch(timestamp: datetime, seconds: int): @region_silo_endpoint class OrganizationMonitorIndexStatsEndpoint(OrganizationEndpoint, StatsMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + # TODO(epurkhiser): probably convert to snuba def get(self, request: Request, organization) -> Response: # Do not restirct rollups allowing us to define custom resolutions. diff --git a/src/sentry/monitors/endpoints/organization_monitor_stats.py b/src/sentry/monitors/endpoints/organization_monitor_stats.py index d3c87b53375392..1f8cf98ccdd71f 100644 --- a/src/sentry/monitors/endpoints/organization_monitor_stats.py +++ b/src/sentry/monitors/endpoints/organization_monitor_stats.py @@ -8,6 +8,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import StatsMixin, region_silo_endpoint from sentry.api.helpers.environments import get_environments from sentry.monitors.models import CheckInStatus, MonitorCheckIn @@ -29,6 +30,10 @@ def normalize_to_epoch(timestamp: datetime, seconds: int): @region_silo_endpoint class OrganizationMonitorStatsEndpoint(MonitorEndpoint, StatsMixin): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + # TODO(dcramer): probably convert to tsdb def get(self, request: Request, organization, project, monitor) -> Response: args = self._parse_args(request) diff --git a/src/sentry/monitors/models.py b/src/sentry/monitors/models.py index 7c10768c4bc160..c13e131f35d799 100644 --- a/src/sentry/monitors/models.py +++ b/src/sentry/monitors/models.py @@ -78,16 +78,31 @@ class MonitorEnvironmentValidationFailed(Exception): pass -def get_next_schedule(last_checkin, schedule_type, schedule): +def get_next_schedule(reference_ts: datetime, schedule_type, schedule): + """ + Given the schedule type and schedule, determine the next timestamp for a + schedule from the reference_ts + + Examples: + + >>> get_next_schedule('05:30', ScheduleType.CRONTAB, '0 * * * *') + >>> 06:00 + + >>> get_next_schedule('05:30', ScheduleType.CRONTAB, '30 * * * *') + >>> 06:30 + + >>> get_next_schedule('05:35', ScheduleType.INTERVAL, [2, 'hour']) + >>> 07:35 + """ if schedule_type == ScheduleType.CRONTAB: - itr = croniter(schedule, last_checkin) + itr = croniter(schedule, reference_ts) next_schedule = itr.get_next(datetime) elif schedule_type == ScheduleType.INTERVAL: interval, unit_name = schedule rule = rrule.rrule( - freq=SCHEDULE_INTERVAL_MAP[unit_name], interval=interval, dtstart=last_checkin, count=2 + freq=SCHEDULE_INTERVAL_MAP[unit_name], interval=interval, dtstart=reference_ts, count=2 ) - if rule[0] > last_checkin: + if rule[0] > reference_ts: next_schedule = rule[0] else: next_schedule = rule[1] @@ -524,6 +539,11 @@ class MonitorEnvironment(Model): auto-generated missed check-ins. """ + last_state_change = models.DateTimeField(null=True) + """ + The last time that the monitor changed state. Used for issue fingerprinting. + """ + objects = MonitorEnvironmentManager() class Meta: diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index 3df0a47b49c11f..0912eb694ea562 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -269,6 +269,13 @@ flags=FLAG_ALLOW_EMPTY | FLAG_PRIORITIZE_DISK | FLAG_AUTOMATOR_MODIFIABLE, ) +register( + "api.prevent-numeric-slugs", + default=False, + type=Bool, + flags=FLAG_AUTOMATOR_MODIFIABLE, +) + # Beacon register("beacon.anonymous", type=Bool, flags=FLAG_REQUIRED) diff --git a/src/sentry/plugins/bases/issue2.py b/src/sentry/plugins/bases/issue2.py index 8bf6de362c47b1..bcaed3e634c7a8 100644 --- a/src/sentry/plugins/bases/issue2.py +++ b/src/sentry/plugins/bases/issue2.py @@ -6,6 +6,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.serializers.models.plugin import PluginSerializer @@ -27,6 +28,10 @@ # TODO(dcramer): remove this in favor of GroupEndpoint @region_silo_endpoint class IssueGroupActionEndpoint(PluginGroupEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } view_method_name = None plugin = None diff --git a/src/sentry/plugins/endpoints.py b/src/sentry/plugins/endpoints.py index 1a62d89ee83dd9..6996971b61ad97 100644 --- a/src/sentry/plugins/endpoints.py +++ b/src/sentry/plugins/endpoints.py @@ -1,3 +1,4 @@ +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint __all__ = ["PluginProjectEndpoint", "PluginGroupEndpoint"] @@ -35,6 +36,10 @@ def respond(self, *args, **kwargs): @region_silo_endpoint class PluginGroupEndpoint(GroupEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + "POST": ApiPublishStatus.UNKNOWN, + } plugin = None view = None diff --git a/src/sentry/relay/config/metric_extraction.py b/src/sentry/relay/config/metric_extraction.py index 2ba9ceff864ecc..97884dce74dd5d 100644 --- a/src/sentry/relay/config/metric_extraction.py +++ b/src/sentry/relay/config/metric_extraction.py @@ -1,6 +1,6 @@ import logging from dataclasses import dataclass -from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple, TypedDict, Union +from typing import Any, Dict, List, Literal, Optional, Sequence, Set, Tuple, TypedDict, Union from sentry import features, options from sentry.api.endpoints.project_transaction_threshold import DEFAULT_THRESHOLD @@ -8,6 +8,7 @@ from sentry.models import ( DashboardWidgetQuery, DashboardWidgetTypes, + Organization, Project, ProjectTransactionThreshold, ProjectTransactionThresholdOverride, @@ -21,6 +22,7 @@ should_use_on_demand_metrics, ) from sentry.snuba.models import SnubaQuery +from sentry.utils import metrics logger = logging.getLogger(__name__) @@ -53,30 +55,64 @@ def get_metric_extraction_config(project: Project) -> Optional[MetricExtractionC - Performance alert rules with advanced filter expressions. - On-demand metrics widgets. """ - if not features.has("organizations:on-demand-metrics-extraction", project.organization): - return None + # For efficiency purposes, we fetch the flags in batch and propagate them downstream. + enabled_features = _on_demand_metrics_feature_flags(project.organization) - alert_specs = _get_alert_metric_specs(project) - widget_specs = _get_widget_metric_specs(project) + prefilling = ( + "organizations:on-demand-metrics-prefill" in enabled_features + and "organizations:enable-on-demand-metrics-prefill" in enabled_features + ) - metrics = _merge_metric_specs(alert_specs, widget_specs) + alert_specs = _get_alert_metric_specs(project, enabled_features, prefilling) + widget_specs = _get_widget_metric_specs(project, enabled_features, prefilling) - if not metrics: + metric_specs = _merge_metric_specs(alert_specs, widget_specs) + if not metric_specs: return None return { "version": _METRIC_EXTRACTION_VERSION, - "metrics": metrics, + "metrics": metric_specs, } -def _get_alert_metric_specs(project: Project) -> List[HashedMetricSpec]: +def _on_demand_metrics_feature_flags(organization: Organization) -> Set[str]: + feature_names = [ + "organizations:on-demand-metrics-extraction", + "organizations:on-demand-metrics-extraction-experimental", + "organizations:on-demand-metrics-prefill", + "organizations:enable-on-demand-metrics-prefill", + ] + + enabled_features = set() + for feature in feature_names: + if features.has(feature, organization=organization): + enabled_features.add(feature) + + return enabled_features + + +def _get_alert_metric_specs( + project: Project, enabled_features: Set[str], prefilling: bool +) -> List[HashedMetricSpec]: + if not ("organizations:on-demand-metrics-extraction" in enabled_features or prefilling): + return [] + + metrics.incr( + "on_demand_metrics.get_alerts", + tags={"prefilling": prefilling}, + ) + + datasets = [Dataset.PerformanceMetrics.value] + if prefilling: + datasets.append(Dataset.Transactions.value) + alert_rules = ( AlertRule.objects.fetch_for_project(project) .filter( organization=project.organization, status=AlertRuleStatus.PENDING.value, - snuba_query__dataset=Dataset.PerformanceMetrics.value, + snuba_query__dataset__in=datasets, ) .select_related("snuba_query") ) @@ -84,7 +120,11 @@ def _get_alert_metric_specs(project: Project) -> List[HashedMetricSpec]: specs = [] for alert in alert_rules: alert_snuba_query = alert.snuba_query - if result := _convert_snuba_query_to_metric(project, alert.snuba_query): + metrics.incr( + "on_demand_metrics.before_alert_spec_generation", + tags={"prefilling": prefilling, "dataset": alert_snuba_query.dataset}, + ) + if result := _convert_snuba_query_to_metric(project, alert_snuba_query, prefilling): _log_on_demand_metric_spec( project_id=project.id, spec_for="alert", @@ -92,6 +132,11 @@ def _get_alert_metric_specs(project: Project) -> List[HashedMetricSpec]: id=alert.id, field=alert_snuba_query.aggregate, query=alert_snuba_query.query, + prefilling=prefilling, + ) + metrics.incr( + "on_demand_metrics.on_demand_spec.for_alert", + tags={"prefilling": prefilling}, ) specs.append(result) @@ -105,12 +150,20 @@ def _get_alert_metric_specs(project: Project) -> List[HashedMetricSpec]: return specs -def _get_widget_metric_specs(project: Project) -> List[HashedMetricSpec]: - if not features.has( - "organizations:on-demand-metrics-extraction-experimental", project.organization +def _get_widget_metric_specs( + project: Project, enabled_features: Set[str], prefilling: bool +) -> List[HashedMetricSpec]: + if not ( + "organizations:on-demand-metrics-extraction" in enabled_features + and "organizations:on-demand-metrics-extraction-experimental" in enabled_features ): return [] + metrics.incr( + "on_demand_metrics.get_widgets", + tags={"prefilling": prefilling}, + ) + # fetch all queries of all on demand metrics widgets of this organization widget_queries = DashboardWidgetQuery.objects.filter( widget__dashboard__organization=project.organization, @@ -119,7 +172,7 @@ def _get_widget_metric_specs(project: Project) -> List[HashedMetricSpec]: specs = [] for widget in widget_queries: - for result in _convert_widget_query_to_metric(project, widget): + for result in _convert_widget_query_to_metric(project, widget, prefilling): specs.append(result) max_widget_specs = options.get("on_demand.max_widget_specs") or _MAX_ON_DEMAND_WIDGETS @@ -154,23 +207,19 @@ def _merge_metric_specs( def _convert_snuba_query_to_metric( - project: Project, snuba_query: SnubaQuery + project: Project, snuba_query: SnubaQuery, prefilling: bool ) -> Optional[HashedMetricSpec]: """ If the passed snuba_query is a valid query for on-demand metric extraction, returns a tuple of (hash, MetricSpec) for the query. Otherwise, returns None. """ return _convert_aggregate_and_query_to_metric( - project, - snuba_query.dataset, - snuba_query.aggregate, - snuba_query.query, + project, snuba_query.dataset, snuba_query.aggregate, snuba_query.query, prefilling ) def _convert_widget_query_to_metric( - project: Project, - widget_query: DashboardWidgetQuery, + project: Project, widget_query: DashboardWidgetQuery, prefilling: bool ) -> Sequence[HashedMetricSpec]: """ Converts a passed metrics widget query to one or more MetricSpecs. @@ -182,6 +231,10 @@ def _convert_widget_query_to_metric( return metrics_specs for aggregate in widget_query.aggregates: + metrics.incr( + "on_demand_metrics.before_widget_spec_generation", + tags={"prefilling": prefilling}, + ) if result := _convert_aggregate_and_query_to_metric( project, # there is an internal check to make sure we extract metrics oly for performance dataset @@ -189,6 +242,7 @@ def _convert_widget_query_to_metric( Dataset.PerformanceMetrics.value, aggregate, widget_query.conditions, + prefilling, ): _log_on_demand_metric_spec( project_id=project.id, @@ -197,6 +251,11 @@ def _convert_widget_query_to_metric( id=widget_query.id, field=aggregate, query=widget_query.conditions, + prefilling=prefilling, + ) + metrics.incr( + "on_demand_metrics.on_demand_spec.for_widget", + tags={"prefilling": prefilling}, ) metrics_specs.append(result) @@ -204,13 +263,13 @@ def _convert_widget_query_to_metric( def _convert_aggregate_and_query_to_metric( - project: Project, dataset: str, aggregate: str, query: str + project: Project, dataset: str, aggregate: str, query: str, prefilling: bool ) -> Optional[HashedMetricSpec]: """ Converts an aggregate and a query to a metric spec with its hash value. """ try: - if not should_use_on_demand_metrics(dataset, aggregate, query): + if not should_use_on_demand_metrics(dataset, aggregate, query, prefilling): return None on_demand_spec = OnDemandMetricSpec( @@ -220,7 +279,11 @@ def _convert_aggregate_and_query_to_metric( return on_demand_spec.query_hash, on_demand_spec.to_metric_spec(project) except Exception as e: - logger.error(e, exc_info=True) + # Since prefilling might include several non-ondemand-compatible alerts, we want to not trigger errors in the + # Sentry console. + if not prefilling: + logger.error(e, exc_info=True) + return None @@ -231,6 +294,7 @@ def _log_on_demand_metric_spec( id: int, field: str, query: str, + prefilling: bool, ) -> None: spec_query_hash, spec_dict = spec @@ -244,6 +308,7 @@ def _log_on_demand_metric_spec( "spec_for": spec_for, "spec_query_hash": spec_query_hash, "spec": spec_dict, + "prefilling": prefilling, }, ) diff --git a/src/sentry/relay/globalconfig.py b/src/sentry/relay/globalconfig.py index 80f5bf067ebb7a..916fbc442c3b33 100644 --- a/src/sentry/relay/globalconfig.py +++ b/src/sentry/relay/globalconfig.py @@ -1,9 +1,10 @@ +from sentry.relay.config.measurements import get_measurements_config from sentry.utils import metrics @metrics.wraps("relay.globalconfig.get") def get_global_config(): """Return the global configuration for Relay.""" - # TODO(iker): Add entries for the global config as needed, empty during - # development. - return {} + return { + "measurements": get_measurements_config(), + } diff --git a/src/sentry/replays/endpoints/organization_replay_count.py b/src/sentry/replays/endpoints/organization_replay_count.py index 2fa8fb963fdc70..c86cb095e71fc0 100644 --- a/src/sentry/replays/endpoints/organization_replay_count.py +++ b/src/sentry/replays/endpoints/organization_replay_count.py @@ -5,6 +5,7 @@ from snuba_sdk import Request from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects from sentry.api.bases.organization_events import OrganizationEventsV2EndpointBase @@ -25,6 +26,9 @@ @region_silo_endpoint class OrganizationReplayCountEndpoint(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """ Get all the replay ids associated with a set of issues/transactions in discover, then verify that they exist in the replays dataset, and return the count. diff --git a/src/sentry/replays/endpoints/organization_replay_details.py b/src/sentry/replays/endpoints/organization_replay_details.py index 13c1d673c937b3..4c79827260ef80 100644 --- a/src/sentry/replays/endpoints/organization_replay_details.py +++ b/src/sentry/replays/endpoints/organization_replay_details.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import NoProjects, OrganizationEndpoint from sentry.apidocs.constants import RESPONSE_BAD_REQUEST, RESPONSE_FORBIDDEN @@ -21,6 +22,9 @@ @region_silo_endpoint @extend_schema(tags=["Replays"]) class OrganizationReplayDetailsEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.PUBLIC, + } """ The same data as ProjectReplayDetails, except no project is required. This works as we'll query for this replay_id across all projects in the diff --git a/src/sentry/replays/endpoints/organization_replay_events_meta.py b/src/sentry/replays/endpoints/organization_replay_events_meta.py index f78c2e02eaa1d0..cc323e9e4aa3dd 100644 --- a/src/sentry/replays/endpoints/organization_replay_events_meta.py +++ b/src/sentry/replays/endpoints/organization_replay_events_meta.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase from sentry.api.paginator import GenericOffsetPaginator @@ -13,6 +14,9 @@ @region_silo_endpoint class OrganizationReplayEventsMetaEndpoint(OrganizationEventsV2EndpointBase): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } """The generic Events endpoints require that the `organizations:global-views` feature be enabled before they return across multiple projects. diff --git a/src/sentry/replays/endpoints/organization_replay_index.py b/src/sentry/replays/endpoints/organization_replay_index.py index 941bed427b7017..58dec5ec142e66 100644 --- a/src/sentry/replays/endpoints/organization_replay_index.py +++ b/src/sentry/replays/endpoints/organization_replay_index.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import NoProjects, OrganizationEndpoint from sentry.api.event_search import parse_search_query @@ -26,6 +27,9 @@ @region_silo_endpoint @extend_schema(tags=["Replays"]) class OrganizationReplayIndexEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.PUBLIC, + } public = {"GET"} def get_replay_filter_params(self, request, organization): diff --git a/src/sentry/replays/endpoints/organization_replay_selector_index.py b/src/sentry/replays/endpoints/organization_replay_selector_index.py index b5e5865e818f53..8b53fbf6093456 100644 --- a/src/sentry/replays/endpoints/organization_replay_selector_index.py +++ b/src/sentry/replays/endpoints/organization_replay_selector_index.py @@ -23,6 +23,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import NoProjects, OrganizationEndpoint from sentry.api.event_search import SearchConfig @@ -36,6 +37,9 @@ @region_silo_endpoint class OrganizationReplaySelectorIndexEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } owner = ApiOwner.REPLAY def get_replay_filter_params(self, request, organization): diff --git a/src/sentry/replays/endpoints/project_replay_clicks_index.py b/src/sentry/replays/endpoints/project_replay_clicks_index.py index 187bbe3933a851..64e62ac4c8aced 100644 --- a/src/sentry/replays/endpoints/project_replay_clicks_index.py +++ b/src/sentry/replays/endpoints/project_replay_clicks_index.py @@ -25,6 +25,7 @@ from snuba_sdk.orderby import Direction from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.event_search import ParenExpression, SearchFilter, parse_search_query @@ -47,6 +48,10 @@ @region_silo_endpoint class ProjectReplayClicksIndexEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project: Project, replay_id: str) -> Response: if not features.has( "organizations:session-replay", project.organization, actor=request.user diff --git a/src/sentry/replays/endpoints/project_replay_details.py b/src/sentry/replays/endpoints/project_replay_details.py index 23d528aec201b9..e3a92875708be7 100644 --- a/src/sentry/replays/endpoints/project_replay_details.py +++ b/src/sentry/replays/endpoints/project_replay_details.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectPermission from sentry.models.project import Project @@ -24,6 +25,10 @@ class ReplayDetailsPermission(ProjectPermission): @region_silo_endpoint class ProjectReplayDetailsEndpoint(ProjectEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (ReplayDetailsPermission,) diff --git a/src/sentry/replays/endpoints/project_replay_recording_segment_details.py b/src/sentry/replays/endpoints/project_replay_recording_segment_details.py index d2059460896277..f4ab236b08600b 100644 --- a/src/sentry/replays/endpoints/project_replay_recording_segment_details.py +++ b/src/sentry/replays/endpoints/project_replay_recording_segment_details.py @@ -8,6 +8,7 @@ from rest_framework.request import Request from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.replays.lib.storage import RecordingSegmentStorageMeta, make_filename @@ -16,6 +17,10 @@ @region_silo_endpoint class ProjectReplayRecordingSegmentDetailsEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def get(self, request: Request, project, replay_id, segment_id) -> HttpResponseBase: if not features.has( "organizations:session-replay", project.organization, actor=request.user diff --git a/src/sentry/replays/endpoints/project_replay_recording_segment_index.py b/src/sentry/replays/endpoints/project_replay_recording_segment_index.py index 1fc3c94cc603de..127b1af58240a6 100644 --- a/src/sentry/replays/endpoints/project_replay_recording_segment_index.py +++ b/src/sentry/replays/endpoints/project_replay_recording_segment_index.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from sentry import features +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.paginator import GenericOffsetPaginator @@ -14,6 +15,10 @@ @region_silo_endpoint class ProjectReplayRecordingSegmentIndexEndpoint(ProjectEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + def __init__(self, **options) -> None: storage.initialize_client() super().__init__(**options) diff --git a/src/sentry/rules/filters/issue_severity.py b/src/sentry/rules/filters/issue_severity.py new file mode 100644 index 00000000000000..99d17e83d8716e --- /dev/null +++ b/src/sentry/rules/filters/issue_severity.py @@ -0,0 +1,70 @@ +from collections import OrderedDict +from typing import Any, Dict, Optional + +from django import forms + +from sentry import features +from sentry.eventstore.models import GroupEvent +from sentry.issues.grouptype import GroupCategory +from sentry.models import Group +from sentry.rules import EventState, MatchType +from sentry.rules.filters import EventFilter +from sentry.types.condition_activity import ConditionActivity + +SEVERITY_MATCH_CHOICES = { + MatchType.GREATER_OR_EQUAL: "greater than or equal to", + MatchType.LESS_OR_EQUAL: "less than or equal to", +} +CATEGORY_CHOICES = OrderedDict([(f"{gc.value}", str(gc.name).title()) for gc in GroupCategory]) + + +class IssueSeverityForm(forms.Form): + value = forms.NumberInput() + + +class IssueSeverityFilter(EventFilter): + id = "sentry.rules.filters.issue_severity.IssueSeverityFilter" + form_cls = IssueSeverityForm + form_fields = { + "value": {"type": "number", "placeholder": 0.5}, + "match": {"type": "choice", "choices": list(SEVERITY_MATCH_CHOICES.items())}, + } + rule_type = "filter/event" + label = "The issue's severity is {match} {value}" + prompt = "The issue's serverity is ..." + + def _passes(self, group: Optional[Group]) -> bool: + has_issue_severity_alerts = features.has( + "projects:first-event-severity-alerting", self.project + ) + + if not has_issue_severity_alerts or not group: + return False + + try: + severity = float(group.get_event_metadata().get("severity", "")) + value = float(self.get_option("value")) + except (KeyError, TypeError, ValueError): + return False + + match = self.get_option("match") + + if match == MatchType.GREATER_OR_EQUAL: + return severity >= value + elif match == MatchType.LESS_OR_EQUAL: + return severity <= value + + return False + + def passes(self, event: GroupEvent, state: EventState) -> bool: + return self._passes(event.group) + + def passes_activity( + self, condition_activity: ConditionActivity, event_map: Dict[str, Any] + ) -> bool: + try: + group = Group.objects.get_from_cache(id=condition_activity.group_id) + except Group.DoesNotExist: + return False + + return self._passes(group) 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 ff44f8c937c45c..1b115b5c94a0a3 100644 --- a/src/sentry/rules/history/endpoints/project_rule_group_history.py +++ b/src/sentry/rules/history/endpoints/project_rule_group_history.py @@ -8,6 +8,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.rule import RuleEndpoint from sentry.api.serializers import Serializer, serialize @@ -53,6 +54,10 @@ def serialize( @extend_schema(tags=["issue_alerts"]) @region_silo_endpoint class ProjectRuleGroupHistoryIndexEndpoint(RuleEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + @extend_schema( operation_id="Retrieve a group firing history for an issue alert", parameters=[GlobalParams.ORG_SLUG, GlobalParams.PROJECT_SLUG, IssueAlertParams], diff --git a/src/sentry/rules/history/endpoints/project_rule_stats.py b/src/sentry/rules/history/endpoints/project_rule_stats.py index b25ea0e1ed2130..c4bdc29da1f3a8 100644 --- a/src/sentry/rules/history/endpoints/project_rule_stats.py +++ b/src/sentry/rules/history/endpoints/project_rule_stats.py @@ -7,6 +7,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.rule import RuleEndpoint from sentry.api.serializers import Serializer, serialize @@ -37,6 +38,10 @@ def serialize( @extend_schema(tags=["issue_alerts"]) @region_silo_endpoint class ProjectRuleStatsIndexEndpoint(RuleEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } + @extend_schema( operation_id="Retrieve firing starts for an issue alert rule for a given time range. Results are returned in hourly buckets.", parameters=[GlobalParams.ORG_SLUG, GlobalParams.PROJECT_SLUG, IssueAlertParams], diff --git a/src/sentry/runner/commands/backup.py b/src/sentry/runner/commands/backup.py index 4285a187d1be08..ca6f0ce064b197 100644 --- a/src/sentry/runner/commands/backup.py +++ b/src/sentry/runner/commands/backup.py @@ -2,8 +2,9 @@ import click -from sentry.backup.exports import OldExportConfig, exports -from sentry.backup.imports import OldImportConfig, imports +from sentry.backup.exports import OldExportConfig, _export +from sentry.backup.imports import OldImportConfig, _import +from sentry.backup.scopes import ExportScope, ImportScope from sentry.runner.decorators import configuration @@ -14,8 +15,9 @@ def import_(src, silent): """Imports core data for a Sentry installation.""" - imports( + _import( src, + ImportScope.Global, OldImportConfig( use_update_instead_of_create=True, use_natural_foreign_keys=True, @@ -40,8 +42,9 @@ def export(dest, silent, indent, exclude): else: exclude = exclude.lower().split(",") - exports( + _export( dest, + ExportScope.Global, OldExportConfig( include_non_sentry_models=True, excluded_models=set(exclude), diff --git a/src/sentry/scim/endpoints/members.py b/src/sentry/scim/endpoints/members.py index 4bf6f223c77ff8..17a8e8ab13b9b5 100644 --- a/src/sentry/scim/endpoints/members.py +++ b/src/sentry/scim/endpoints/members.py @@ -15,6 +15,7 @@ from typing_extensions import TypedDict from sentry import audit_log, roles +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organizationmember import OrganizationMemberEndpoint from sentry.api.endpoints.organization_member.index import OrganizationMemberSerializer @@ -118,10 +119,10 @@ def _scim_member_serializer_with_expansion(organization): care about this and rely on the behavior of setting "active" to false to delete a member. """ - auth_providers = auth_service.get_auth_providers(organization_id=organization.id) + auth_provider = auth_service.get_auth_provider(organization_id=organization.id) expand = ["active"] - if any(ap.provider == ACTIVE_DIRECTORY_PROVIDER_NAME for ap in auth_providers): + if auth_provider and auth_provider.provider == ACTIVE_DIRECTORY_PROVIDER_NAME: expand = [] return OrganizationMemberSCIMSerializer(expand=expand) @@ -141,6 +142,12 @@ def resolve_maybe_bool_value(value): @region_silo_endpoint class OrganizationSCIMMemberDetails(SCIMEndpoint, OrganizationMemberEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.PUBLIC, + "GET": ApiPublishStatus.PUBLIC, + "PUT": ApiPublishStatus.UNKNOWN, + "PATCH": ApiPublishStatus.PUBLIC, + } permission_classes = (OrganizationSCIMMemberPermission,) public = {"GET", "DELETE", "PATCH"} @@ -367,6 +374,10 @@ class SCIMListResponseDict(TypedDict): @region_silo_endpoint class OrganizationSCIMMemberIndex(SCIMEndpoint): + publish_status = { + "GET": ApiPublishStatus.PUBLIC, + "POST": ApiPublishStatus.PUBLIC, + } permission_classes = (OrganizationSCIMMemberPermission,) public = {"GET", "POST"} diff --git a/src/sentry/scim/endpoints/schemas.py b/src/sentry/scim/endpoints/schemas.py index 18628bd897dd41..e446f31c1c1bef 100644 --- a/src/sentry/scim/endpoints/schemas.py +++ b/src/sentry/scim/endpoints/schemas.py @@ -3,6 +3,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from .constants import SCIM_SCHEMA_GROUP, SCIM_SCHEMA_USER @@ -188,6 +189,9 @@ @region_silo_endpoint class OrganizationSCIMSchemaIndex(SCIMEndpoint): + publish_status = { + "GET": ApiPublishStatus.UNKNOWN, + } permission_classes = (OrganizationSCIMMemberPermission,) def get(self, request: Request, *args: Any, **kwds: Any) -> Response: diff --git a/src/sentry/scim/endpoints/teams.py b/src/sentry/scim/endpoints/teams.py index 6fa8e5aee9b71a..4227534b7e209e 100644 --- a/src/sentry/scim/endpoints/teams.py +++ b/src/sentry/scim/endpoints/teams.py @@ -13,6 +13,7 @@ from typing_extensions import TypedDict from sentry import audit_log +from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.endpoints.organization_teams import CONFLICTING_SLUG_ERROR, TeamPostSerializer from sentry.api.endpoints.team_details import TeamDetailsEndpoint, TeamSerializer @@ -97,6 +98,10 @@ class SCIMListResponseDict(TypedDict): @extend_schema(tags=["SCIM"]) @region_silo_endpoint class OrganizationSCIMTeamIndex(SCIMEndpoint): + publish_status = { + "GET": ApiPublishStatus.PUBLIC, + "POST": ApiPublishStatus.PUBLIC, + } permission_classes = (OrganizationSCIMTeamPermission,) public = {"GET", "POST"} @@ -228,6 +233,12 @@ def post(self, request: Request, organization: Organization, **kwds: Any) -> Res @extend_schema(tags=["SCIM"]) @region_silo_endpoint class OrganizationSCIMTeamDetails(SCIMEndpoint, TeamDetailsEndpoint): + publish_status = { + "DELETE": ApiPublishStatus.PUBLIC, + "GET": ApiPublishStatus.PUBLIC, + "PUT": ApiPublishStatus.UNKNOWN, + "PATCH": ApiPublishStatus.PUBLIC, + } permission_classes = (OrganizationSCIMTeamPermission,) public = {"GET", "PATCH", "DELETE"} diff --git a/src/sentry/scim/endpoints/utils.py b/src/sentry/scim/endpoints/utils.py index 480cfcb8b3e517..46675852247699 100644 --- a/src/sentry/scim/endpoints/utils.py +++ b/src/sentry/scim/endpoints/utils.py @@ -88,13 +88,13 @@ def validate_filter(self, filter): class OrganizationSCIMPermission(OrganizationPermission): - def has_object_permission(self, request: Request, view, organization: Organization): + def has_object_permission(self, request: Request, view, organization: Organization) -> bool: result = super().has_object_permission(request, view, organization) # The scim endpoints should only be used in conjunction with a SAML2 integration if not result: return result - providers = auth_service.get_auth_providers(organization_id=organization.id) - return any(p.flags.scim_enabled for p in providers) + provider = auth_service.get_auth_provider(organization_id=organization.id) + return provider is not None and provider.flags.scim_enabled class OrganizationSCIMMemberPermission(OrganizationSCIMPermission): diff --git a/src/sentry/services/hybrid_cloud/auth/impl.py b/src/sentry/services/hybrid_cloud/auth/impl.py index c469a02652e2d2..7327eb3ae09c16 100644 --- a/src/sentry/services/hybrid_cloud/auth/impl.py +++ b/src/sentry/services/hybrid_cloud/auth/impl.py @@ -1,7 +1,7 @@ from __future__ import annotations import base64 -from typing import Any, List, Mapping +from typing import Any, List, Mapping, Optional from django.contrib.auth.models import AnonymousUser from django.db import router, transaction @@ -222,10 +222,16 @@ def get_org_ids_with_scim( ) def get_auth_providers(self, organization_id: int) -> List[RpcAuthProvider]: - return [ - serialize_auth_provider(auth_provider) - for auth_provider in AuthProvider.objects.filter(organization_id=organization_id) - ] + # DEPRECATED. TODO: Delete after usages are removed from getsentry. + auth_provider = self.get_auth_provider(organization_id) + return [auth_provider] if auth_provider else [] + + def get_auth_provider(self, organization_id: int) -> Optional[RpcAuthProvider]: + try: + auth_provider = AuthProvider.objects.get(organization_id=organization_id) + except AuthProvider.DoesNotExist: + return None + return serialize_auth_provider(auth_provider) def change_scim( self, *, user_id: int, provider_id: int, enabled: bool, allow_unlinked: bool diff --git a/src/sentry/services/hybrid_cloud/auth/service.py b/src/sentry/services/hybrid_cloud/auth/service.py index 5b05ae4684635b..fe014423938a2b 100644 --- a/src/sentry/services/hybrid_cloud/auth/service.py +++ b/src/sentry/services/hybrid_cloud/auth/service.py @@ -75,9 +75,13 @@ def get_org_ids_with_scim(self) -> List[int]: @rpc_method @abc.abstractmethod def get_auth_providers(self, *, organization_id: int) -> List[RpcAuthProvider]: + """DEPRECATED. TODO: Delete after usages are removed from getsentry.""" + + @rpc_method + @abc.abstractmethod + def get_auth_provider(self, *, organization_id: int) -> Optional[RpcAuthProvider]: """ - This method returns a list of auth providers for an org - :return: + This method returns the auth provider for an org, if one exists """ pass diff --git a/src/sentry/shared_integrations/client/base.py b/src/sentry/shared_integrations/client/base.py index af40254035a095..6cae4bef684bf7 100644 --- a/src/sentry/shared_integrations/client/base.py +++ b/src/sentry/shared_integrations/client/base.py @@ -499,13 +499,7 @@ def disable_integration(self, buffer) -> None: extra=extra, ) - if ( - ( - features.has("organizations:slack-fatal-disable-on-broken", org) - and rpc_integration.provider == "slack" - ) - and buffer.is_integration_fatal_broken() - ) or ( + if (rpc_integration.provider == "slack" and buffer.is_integration_fatal_broken()) or ( features.has("organizations:github-disable-on-broken", org) and rpc_integration.provider == "github" ): diff --git a/src/sentry/snuba/metrics/extraction.py b/src/sentry/snuba/metrics/extraction.py index 7538b2673e8b9c..ef902c40fe0b57 100644 --- a/src/sentry/snuba/metrics/extraction.py +++ b/src/sentry/snuba/metrics/extraction.py @@ -260,10 +260,19 @@ def is_on_demand_snuba_query(snuba_query: SnubaQuery) -> bool: def should_use_on_demand_metrics( - dataset: Optional[Union[str, Dataset]], aggregate: str, query: Optional[str] + dataset: Optional[Union[str, Dataset]], + aggregate: str, + query: Optional[str], + prefilling: bool = False, ) -> bool: """On-demand metrics are used if the aggregate and query are supported by on-demand metrics but not standard""" - if not dataset or Dataset(dataset) != Dataset.PerformanceMetrics: + supported_datasets = [Dataset.PerformanceMetrics] + # In case we are running a prefill, we want to support also transactions, since our goal is to start extracting + # metrics that will be needed after a query is converted from using transactions to metrics. + if prefilling: + supported_datasets.append(Dataset.Transactions) + + if not dataset or Dataset(dataset) not in supported_datasets: return False aggregate_supported_by = _get_aggregate_supported_by(aggregate) diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index 87614c974497cf..a98b1f13cea347 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -27,7 +27,7 @@ from sentry.utils.locking import UnableToAcquireLock from sentry.utils.locking.manager import LockManager from sentry.utils.retries import ConditionalRetryPolicy, exponential_delay -from sentry.utils.safe import safe_execute +from sentry.utils.safe import get_path, safe_execute from sentry.utils.sdk import bind_organization_context, set_current_event_project from sentry.utils.services import build_instance_from_options @@ -851,8 +851,8 @@ def _get_replay_id(event): # It comes as a tag on js errors. # TODO: normalize this upstream in relay and javascript SDK. and eventually remove the tag # logic. - context_replay_id = event.data.get("contexts", {}).get("replay", {}).get("replay_id") + context_replay_id = get_path(event.data, "contexts", "replay", "replay_id") return context_replay_id or event.get_tag("replayId") if job["is_reprocessed"]: diff --git a/src/sentry/testutils/cases.py b/src/sentry/testutils/cases.py index 7bf0584006a7ef..01ee73b1692bc9 100644 --- a/src/sentry/testutils/cases.py +++ b/src/sentry/testutils/cases.py @@ -7,7 +7,7 @@ from contextlib import contextmanager from datetime import datetime, timedelta, timezone from io import BytesIO -from typing import Dict, List, Literal, Optional, Sequence, Union +from typing import Any, Dict, List, Literal, Optional, Sequence, Union from unittest import mock from urllib.parse import urlencode from uuid import uuid4 @@ -93,6 +93,7 @@ from sentry.notifications.types import NotificationSettingOptionValues, NotificationSettingTypes from sentry.plugins.base import plugins from sentry.replays.models import ReplayRecordingSegment +from sentry.rules.base import RuleBase from sentry.search.events.constants import ( METRIC_FRUSTRATED_TAG_VALUE, METRIC_SATISFACTION_TAG_KEY, @@ -113,6 +114,7 @@ from sentry.testutils.helpers.notifications import TEST_ISSUE_OCCURRENCE from sentry.testutils.helpers.slack import install_slack from sentry.testutils.pytest.selenium import Browser +from sentry.types.condition_activity import ConditionActivity, ConditionActivityType from sentry.types.integrations import ExternalProviders from sentry.utils import json from sentry.utils.auth import SsoSession @@ -790,6 +792,24 @@ def get_state(self, **kwargs): kwargs.setdefault("has_reappeared", True) return EventState(**kwargs) + def get_condition_activity(self, **kwargs) -> ConditionActivity: + kwargs.setdefault("group_id", self.event.group.id) + kwargs.setdefault("type", ConditionActivityType.CREATE_ISSUE) + kwargs.setdefault("timestamp", self.event.datetime) + return ConditionActivity(**kwargs) + + def passes_activity( + self, + rule: RuleBase, + condition_activity: Optional[ConditionActivity] = None, + event_map: Optional[Dict[str, Any]] = None, + ): + if condition_activity is None: + condition_activity = self.get_condition_activity() + if event_map is None: + event_map = {} + return rule.passes_activity(condition_activity, event_map) + def assertPasses(self, rule, event=None, **kwargs): if event is None: event = self.event diff --git a/src/sentry/testutils/helpers/backups.py b/src/sentry/testutils/helpers/backups.py index 1c3f75a324b1f4..8ff04f16a7666f 100644 --- a/src/sentry/testutils/helpers/backups.py +++ b/src/sentry/testutils/helpers/backups.py @@ -14,9 +14,14 @@ from sentry.backup.comparators import ComparatorMap from sentry.backup.dependencies import sorted_dependencies -from sentry.backup.exports import OldExportConfig, exports +from sentry.backup.exports import ( + export_in_global_scope, + export_in_organization_scope, + export_in_user_scope, +) from sentry.backup.findings import ComparatorFindings -from sentry.backup.imports import OldImportConfig, imports +from sentry.backup.imports import OldImportConfig, _import +from sentry.backup.scopes import ExportScope, ImportScope from sentry.backup.validate import validate from sentry.db.models.fields.bounded import BoundedBigAutoField from sentry.incidents.models import ( @@ -92,12 +97,21 @@ def __init__(self, info: ComparatorFindings): self.info = info -def export_to_file(path: Path) -> JSONData: +def export_to_file(path: Path, scope: ExportScope) -> JSONData: """Helper function that exports the current state of the database to the specified file.""" json_file_path = str(path) with open(json_file_path, "w+") as tmp_file: - exports(tmp_file, OldExportConfig(), 2, NOOP_PRINTER) + # These functions are just thin wrappers, but its best to exercise them directly anyway in + # case that ever changes. + if scope == ExportScope.Global: + export_in_global_scope(tmp_file, NOOP_PRINTER) + elif scope == ExportScope.Organization: + export_in_organization_scope(tmp_file, NOOP_PRINTER) + elif scope == ExportScope.User: + export_in_user_scope(tmp_file, NOOP_PRINTER) + else: + raise AssertionError(f"Unknown `ExportScope`: `{scope.name}`") with open(json_file_path) as tmp_file: output = json.load(tmp_file) @@ -142,7 +156,7 @@ def import_export_then_validate(method_name: str, *, reset_pks: bool = True) -> # Export the current state of the database into the "expected" temporary file, then # parse it into a JSON object for comparison. - expect = export_to_file(tmp_expect) + expect = export_to_file(tmp_expect, ExportScope.Global) # Write the contents of the "expected" JSON file into the now clean database. # TODO(Hybrid-Cloud): Review whether this is the correct route to apply in this case. @@ -153,10 +167,10 @@ def import_export_then_validate(method_name: str, *, reset_pks: bool = True) -> clear_database_but_keep_sequences() with open(tmp_expect) as tmp_file: - imports(tmp_file, OldImportConfig(), NOOP_PRINTER) + _import(tmp_file, ImportScope.Global, OldImportConfig(), NOOP_PRINTER) # Validate that the "expected" and "actual" JSON matches. - actual = export_to_file(tmp_actual) + actual = export_to_file(tmp_actual, ExportScope.Global) res = validate(expect, actual) if res.findings: raise ValidationError(res) @@ -197,9 +211,11 @@ def import_export_from_fixture_then_validate( # TODO(Hybrid-Cloud): Review whether this is the correct route to apply in this case. with unguarded_write(using="default"), open(fixture_file_path) as fixture_file: - imports(fixture_file, OldImportConfig(), NOOP_PRINTER) + _import(fixture_file, ImportScope.Global, OldImportConfig(), NOOP_PRINTER) - res = validate(expect, export_to_file(tmp_path.joinpath("tmp_test_file.json")), map) + res = validate( + expect, export_to_file(tmp_path.joinpath("tmp_test_file.json"), ExportScope.Global), map + ) if res.findings: raise ValidationError(res) diff --git a/src/sentry/testutils/helpers/task_runner.py b/src/sentry/testutils/helpers/task_runner.py index 66427cd131d868..4f80425e78d526 100644 --- a/src/sentry/testutils/helpers/task_runner.py +++ b/src/sentry/testutils/helpers/task_runner.py @@ -12,9 +12,11 @@ def TaskRunner(): prev = settings.CELERY_ALWAYS_EAGER settings.CELERY_ALWAYS_EAGER = True current_app.conf.CELERY_ALWAYS_EAGER = True - yield - current_app.conf.CELERY_ALWAYS_EAGER = prev - settings.CELERY_ALWAYS_EAGER = prev + try: + yield + finally: + current_app.conf.CELERY_ALWAYS_EAGER = prev + settings.CELERY_ALWAYS_EAGER = prev @contextmanager diff --git a/src/sentry/utils/platform_categories.py b/src/sentry/utils/platform_categories.py index 9b77ba34176ca4..5f774ab828b207 100644 --- a/src/sentry/utils/platform_categories.py +++ b/src/sentry/utils/platform_categories.py @@ -103,6 +103,12 @@ "python-starlette", "python-sanic", "python-celery", + "python-aiohttp", + "python-chalice", + "python-falcon", + "python-quart", + "python-tryton", + "python-wsgi", "python-bottle", "python-pylons", "python-pyramid", @@ -119,6 +125,7 @@ "python-awslambda", "python-azurefunctions", "python-gcpfunctions", + "python-serverless", "node-awslambda", "node-azurefunctions", "node-gcpfunctions", diff --git a/src/sentry/utils/sentry_apps/webhooks.py b/src/sentry/utils/sentry_apps/webhooks.py index 6920fc553136d5..ff24d8f3e8c8d3 100644 --- a/src/sentry/utils/sentry_apps/webhooks.py +++ b/src/sentry/utils/sentry_apps/webhooks.py @@ -7,7 +7,7 @@ from requests.exceptions import ConnectionError, Timeout from rest_framework import status -from sentry import audit_log, features, options +from sentry import audit_log, options from sentry.http import safe_urlopen from sentry.integrations.notify_disable import notify_disable from sentry.integrations.request_buffer import IntegrationRequestBuffer @@ -46,16 +46,15 @@ def check_broken(sentryapp: SentryApp, org_id: str): buffer = IntegrationRequestBuffer(redis_key) if buffer.is_integration_broken(): org = Organization.objects.get(id=org_id) - if features.has("organizations:disable-sentryapps-on-broken", org): - sentryapp._disable() - notify_disable(org, sentryapp.name, redis_key, sentryapp.slug, sentryapp.webhook_url) - buffer.clear() - create_system_audit_entry( - organization=org, - target_object=org.id, - event=audit_log.get_event_id("INTERNAL_INTEGRATION_DISABLED"), - data={"name": sentryapp.name}, - ) + sentryapp._disable() + notify_disable(org, sentryapp.name, redis_key, sentryapp.slug, sentryapp.webhook_url) + buffer.clear() + create_system_audit_entry( + organization=org, + target_object=org.id, + event=audit_log.get_event_id("INTERNAL_INTEGRATION_DISABLED"), + data={"name": sentryapp.name}, + ) extra = { "sentryapp_webhook": sentryapp.webhook_url, "sentryapp_slug": sentryapp.slug, diff --git a/src/sentry/web/frontend/organization_auth_settings.py b/src/sentry/web/frontend/organization_auth_settings.py index b77f35da7ec90d..1409013805801d 100644 --- a/src/sentry/web/frontend/organization_auth_settings.py +++ b/src/sentry/web/frontend/organization_auth_settings.py @@ -214,13 +214,13 @@ def handle_existing_provider( return self.respond("sentry/organization-auth-provider-settings.html", context) def handle(self, request: Request, organization: RpcOrganization) -> HttpResponseBase: # type: ignore[override] - providers = auth_service.get_auth_providers(organization_id=organization.id) - if providers: + provider = auth_service.get_auth_provider(organization_id=organization.id) + if provider: # if the org has SSO set up already, allow them to modify the existing provider # regardless if the feature flag is set up. This allows orgs who might no longer # have the SSO feature to be able to turn it off return self.handle_existing_provider( - request=request, organization=organization, auth_provider=providers[0] + request=request, organization=organization, auth_provider=provider ) if request.method == "POST": diff --git a/static/app/actionCreators/modal.tsx b/static/app/actionCreators/modal.tsx index d6cef1ed3eb1b7..108d78dafadb6f 100644 --- a/static/app/actionCreators/modal.tsx +++ b/static/app/actionCreators/modal.tsx @@ -11,7 +11,9 @@ import type { Event, Group, IssueOwnership, + MissingMember, Organization, + OrgRole, Project, SentryApp, Team, @@ -246,6 +248,23 @@ export async function openInviteMembersModal({ openModal(deps => , {modalCss, onClose}); } +type InviteMissingMembersModalOptions = { + allowedRoles: OrgRole[]; + missingMembers: {integration: string; users: MissingMember[]}; + onClose: () => void; + organization: Organization; +}; + +export async function openInviteMissingMembersModal({ + onClose, + ...args +}: InviteMissingMembersModalOptions) { + const mod = await import('sentry/components/modals/inviteMissingMembersModal'); + const {default: Modal, modalCss} = mod; + + openModal(deps => , {modalCss, onClose}); +} + export async function openWidgetBuilderOverwriteModal( options: OverwriteWidgetModalProps ) { diff --git a/static/app/components/button.tsx b/static/app/components/button.tsx index e3a84a849240f0..d9f4c3f5538cbe 100644 --- a/static/app/components/button.tsx +++ b/static/app/components/button.tsx @@ -118,7 +118,7 @@ interface BaseButtonProps extends CommonButtonProps, ElementProps * When set the button acts as an anchor link. Use with `external` to have * the link open in a new tab. * - * @deprecated Use LnikButton instead + * @deprecated Use LinkButton instead */ href?: string; /** diff --git a/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx b/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx new file mode 100644 index 00000000000000..8ebb915e44d75a --- /dev/null +++ b/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx @@ -0,0 +1,94 @@ +import {Event} from 'sentry/types'; +import EventView from 'sentry/utils/discover/eventView'; +import TrendsDiscoverQuery from 'sentry/utils/performance/trends/trendsDiscoverQuery'; +import {useLocation} from 'sentry/utils/useLocation'; +import useOrganization from 'sentry/utils/useOrganization'; +import {TrendsChart} from 'sentry/views/performance/landing/widgets/widgets/trendsWidget'; +import { + NormalizedTrendsTransaction, + TrendChangeType, + TrendFunctionField, +} from 'sentry/views/performance/trends/types'; + +import {DataSection} from '../styles'; + +function camelToUnderscore(key: string) { + return key.replace(/([A-Z\d])/g, '_$1').toLowerCase(); +} + +type EventBreakpointChartProps = { + event: Event; +}; + +function EventBreakpointChart({event}: EventBreakpointChartProps) { + const organization = useOrganization(); + const location = useLocation(); + + const eventView = EventView.fromLocation(location); + eventView.query = `event.type:transaction transaction:"${event?.occurrence?.evidenceData?.transaction}"`; + eventView.fields = [{field: 'transaction'}, {field: 'project'}]; + + // Set the start and end time to 7 days before and after the breakpoint + // TODO: This should be removed when the endpoint begins returning the start and end + // explicitly + if (event?.occurrence) { + eventView.statsPeriod = undefined; + const detectionTime = new Date(event?.occurrence?.evidenceData?.breakpoint * 1000); + const start = new Date(detectionTime).setDate(detectionTime.getDate() - 7); + const end = new Date(detectionTime).setDate(detectionTime.getDate() + 7); + + eventView.start = new Date(start).toISOString(); + eventView.end = new Date(Math.min(end, Date.now())).toISOString(); + } else { + eventView.statsPeriod = '14d'; + } + + // The evidence data keys are returned to us in camelCase, but we need to + // convert them to snake_case to match the NormalizedTrendsTransaction type + const normalizedOccurrenceEvent = Object.keys( + event?.occurrence?.evidenceData ?? [] + ).reduce((acc, key) => { + acc[camelToUnderscore(key)] = event?.occurrence?.evidenceData?.[key]; + return acc; + }, {}) as NormalizedTrendsTransaction; + + return ( + + + {({trendsData, isLoading}) => { + return ( + + ); + }} + + + ); +} + +export default EventBreakpointChart; diff --git a/static/app/components/events/eventStatisticalDetector/regressionMessage.tsx b/static/app/components/events/eventStatisticalDetector/regressionMessage.tsx index 274e3cf6926a22..4fcb44e2f9f11a 100644 --- a/static/app/components/events/eventStatisticalDetector/regressionMessage.tsx +++ b/static/app/components/events/eventStatisticalDetector/regressionMessage.tsx @@ -25,8 +25,7 @@ function EventStatisticalDetectorMessage({event}: EventStatisticalDetectorMessag projectID: event.projectID, display: DisplayModes.TREND, }); - const detectionTime = new Date(0); - detectionTime.setUTCSeconds(event?.occurrence?.evidenceData?.breakpoint); + const detectionTime = new Date(event?.occurrence?.evidenceData?.breakpoint * 1000); // TODO: This messaging should respect selected locale in user settings return ( @@ -40,7 +39,7 @@ function EventStatisticalDetectorMessage({event}: EventStatisticalDetectorMessag {transactionName} ), amount: formatPercentage( - event?.occurrence?.evidenceData?.trendPercentage / 100 + event?.occurrence?.evidenceData?.trendPercentage - 1 ), date: detectionTime.toLocaleDateString(undefined, { month: 'short', diff --git a/static/app/components/modals/inviteMembersModal/index.tsx b/static/app/components/modals/inviteMembersModal/index.tsx index bd5666ba0fd068..72b0bc0b64b16a 100644 --- a/static/app/components/modals/inviteMembersModal/index.tsx +++ b/static/app/components/modals/inviteMembersModal/index.tsx @@ -41,13 +41,15 @@ interface State extends AsyncComponentState { const DEFAULT_ROLE = 'member'; -const InviteModalHook = HookOrDefault({ +export const InviteModalHook = HookOrDefault({ hookName: 'member-invite-modal:customization', defaultComponent: ({onSendInvites, children}) => children({sendInvites: onSendInvites, canSend: true}), }); -type InviteModalRenderFunc = React.ComponentProps['children']; +export type InviteModalRenderFunc = React.ComponentProps< + typeof InviteModalHook +>['children']; class InviteMembersModal extends DeprecatedAsyncComponent< InviteMembersModalProps, @@ -508,7 +510,7 @@ const FooterContent = styled('div')` flex: 1; `; -const StatusMessage = styled('div')<{status?: 'success' | 'error'}>` +export const StatusMessage = styled('div')<{status?: 'success' | 'error'}>` display: flex; gap: ${space(1)}; align-items: center; diff --git a/static/app/components/modals/inviteMissingMembersModal/index.spec.tsx b/static/app/components/modals/inviteMissingMembersModal/index.spec.tsx new file mode 100644 index 00000000000000..03892616a9d5a4 --- /dev/null +++ b/static/app/components/modals/inviteMissingMembersModal/index.spec.tsx @@ -0,0 +1,209 @@ +import selectEvent from 'react-select-event'; +import styled from '@emotion/styled'; + +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; + +import {makeCloseButton} from 'sentry/components/globalModal/components'; +import InviteMissingMembersModal, { + InviteMissingMembersModalProps, +} from 'sentry/components/modals/inviteMissingMembersModal'; +import TeamStore from 'sentry/stores/teamStore'; +import {OrgRole} from 'sentry/types'; + +const roles = [ + { + id: 'admin', + name: 'Admin', + desc: 'This is the admin role', + allowed: true, + }, + { + id: 'member', + name: 'Member', + desc: 'This is the member role', + allowed: true, + }, +] as OrgRole[]; + +describe('InviteMissingMembersModal', function () { + const team = TestStubs.Team(); + const org = TestStubs.Organization({access: ['member:write'], teams: [team]}); + TeamStore.loadInitialData([team]); + const missingMembers = {integration: 'github', users: TestStubs.MissingMembers()}; + + const styledWrapper = styled(c => c.children); + const modalProps: InviteMissingMembersModalProps = { + Body: styledWrapper(), + Header: p => {p.children}, + Footer: styledWrapper(), + closeModal: () => {}, + CloseButton: makeCloseButton(() => {}), + organization: TestStubs.Organization(), + missingMembers: {integration: 'github', users: []}, + allowedRoles: [], + }; + + beforeEach(function () { + MockApiClient.clearMockResponses(); + MockApiClient.addMockResponse({ + url: `/organizations/${org.slug}/members/me/`, + method: 'GET', + body: {roles}, + }); + }); + + it('renders with empty table when no missing members', function () { + render(); + + expect( + screen.getByRole('heading', {name: 'Invite Your Dev Team'}) + ).toBeInTheDocument(); + + // 1 checkbox column + 4 content columns + expect(screen.queryAllByTestId('table-header')).toHaveLength(5); + }); + + it('does not render without org:write', function () { + const organization = TestStubs.Organization({access: []}); + render(); + + expect( + screen.queryByRole('heading', {name: 'Invite Your Dev Team'}) + ).not.toBeInTheDocument(); + }); + + it('disables invite button if no members selected', function () { + render(); + + expect( + screen.getByRole('heading', {name: 'Invite Your Dev Team'}) + ).toBeInTheDocument(); + + expect(screen.getByLabelText('Send Invites')).toBeDisabled(); + expect(screen.getByText('Invite missing members')).toBeInTheDocument(); + }); + + it('enables and disables invite button when toggling one checkbox', async function () { + render(); + + expect( + screen.getByRole('heading', {name: 'Invite Your Dev Team'}) + ).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Select hello@sentry.io')); + + expect(screen.getByLabelText('Send Invites')).toBeEnabled(); + expect(screen.getByText('Invite 1 missing member')).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Select hello@sentry.io')); + + expect(screen.getByLabelText('Send Invites')).toBeDisabled(); + expect(screen.getByText('Invite missing members')).toBeInTheDocument(); + }); + + it('can select and deselect all rows', async function () { + render(); + + expect( + screen.getByRole('heading', {name: 'Invite Your Dev Team'}) + ).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Select All')); + + expect(screen.getByLabelText('Send Invites')).toBeEnabled(); + expect(screen.getByText('Invite all 5 missing members')).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Deselect All')); + + expect(screen.getByLabelText('Send Invites')).toBeDisabled(); + expect(screen.getByText('Invite missing members')).toBeInTheDocument(); + }); + + it('can invite all members', async function () { + render( + + ); + + const createMemberMock = MockApiClient.addMockResponse({ + url: `/organizations/${org.slug}/members/`, + method: 'POST', + body: {}, + }); + + expect( + screen.getByRole('heading', {name: 'Invite Your Dev Team'}) + ).toBeInTheDocument(); + + await userEvent.click(screen.getByLabelText('Select All')); + await userEvent.click(screen.getByLabelText('Send Invites')); + + // Verify data sent to the backend + expect(createMemberMock).toHaveBeenCalledTimes(5); + + missingMembers.users.forEach((member, i) => { + expect(createMemberMock).toHaveBeenNthCalledWith( + i + 1, + `/organizations/${org.slug}/members/`, + expect.objectContaining({ + data: {email: member.email, role: 'member', teams: []}, + }) + ); + }); + }); + + it('can invite multiple members', async function () { + render( + + ); + + const createMemberMock = MockApiClient.addMockResponse({ + url: `/organizations/${org.slug}/members/`, + method: 'POST', + body: {}, + }); + + expect( + screen.getByRole('heading', {name: 'Invite Your Dev Team'}) + ).toBeInTheDocument(); + + const roleInputs = screen.getAllByRole('textbox', {name: 'Role'}); + const teamInputs = screen.getAllByRole('textbox', {name: 'Add to Team'}); + + await userEvent.click(screen.getByLabelText('Select hello@sentry.io')); + await selectEvent.select(roleInputs[0], 'Admin'); + + await userEvent.click(screen.getByLabelText('Select abcd@sentry.io')); + await selectEvent.select(teamInputs[1], '#team-slug'); + + await userEvent.click(screen.getByLabelText('Send Invites')); + + // Verify data sent to the backend + expect(createMemberMock).toHaveBeenCalledTimes(2); + + expect(createMemberMock).toHaveBeenNthCalledWith( + 1, + `/organizations/${org.slug}/members/`, + expect.objectContaining({ + data: {email: 'hello@sentry.io', role: 'admin', teams: []}, + }) + ); + + expect(createMemberMock).toHaveBeenNthCalledWith( + 2, + `/organizations/${org.slug}/members/`, + expect.objectContaining({ + data: {email: 'abcd@sentry.io', role: 'member', teams: [team.slug]}, + }) + ); + }); +}); diff --git a/static/app/components/modals/inviteMissingMembersModal/index.tsx b/static/app/components/modals/inviteMissingMembersModal/index.tsx new file mode 100644 index 00000000000000..d844a3a9b8581b --- /dev/null +++ b/static/app/components/modals/inviteMissingMembersModal/index.tsx @@ -0,0 +1,340 @@ +import {Fragment, useState} from 'react'; +import {css} from '@emotion/react'; +import styled from '@emotion/styled'; + +import {ModalRenderProps} from 'sentry/actionCreators/modal'; +import {Button} from 'sentry/components/button'; +import ButtonBar from 'sentry/components/buttonBar'; +import Checkbox from 'sentry/components/checkbox'; +import LoadingIndicator from 'sentry/components/loadingIndicator'; +import { + InviteModalHook, + InviteModalRenderFunc, + StatusMessage, +} from 'sentry/components/modals/inviteMembersModal'; +import {InviteStatus} from 'sentry/components/modals/inviteMembersModal/types'; +import {MissingMemberInvite} from 'sentry/components/modals/inviteMissingMembersModal/types'; +import PanelItem from 'sentry/components/panels/panelItem'; +import PanelTable from 'sentry/components/panels/panelTable'; +import RoleSelectControl from 'sentry/components/roleSelectControl'; +import TeamSelector from 'sentry/components/teamSelector'; +import {Tooltip} from 'sentry/components/tooltip'; +import {IconCheckmark, IconCommit, IconGithub, IconInfo} from 'sentry/icons'; +import {t, tct, tn} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import {MissingMember, Organization, OrgRole} from 'sentry/types'; +import {trackAnalytics} from 'sentry/utils/analytics'; +import useApi from 'sentry/utils/useApi'; +import withOrganization from 'sentry/utils/withOrganization'; +import { + StyledExternalLink, + Subtitle, +} from 'sentry/views/settings/organizationMembers/inviteBanner'; + +export interface InviteMissingMembersModalProps extends ModalRenderProps { + allowedRoles: OrgRole[]; + missingMembers: {integration: string; users: MissingMember[]}; + organization: Organization; +} + +export function InviteMissingMembersModal({ + missingMembers, + organization, + allowedRoles, + closeModal, +}: InviteMissingMembersModalProps) { + const initialMemberInvites = (missingMembers.users || []).map(member => ({ + email: member.email, + commitCount: member.commitCount, + role: organization.defaultRole, + teamSlugs: new Set(), + externalId: member.externalId, + selected: false, + })); + const [memberInvites, setMemberInvites] = + useState(initialMemberInvites); + const [inviteStatus, setInviteStatus] = useState({}); + const [sendingInvites, setSendingInvites] = useState(false); + const [complete, setComplete] = useState(false); + + const api = useApi(); + + if (!memberInvites || !organization.access.includes('org:write')) { + return null; + } + + const setRole = (role: string, index: number) => { + setMemberInvites(currentMemberInvites => + currentMemberInvites.map((member, i) => { + if (i === index) { + member.role = role; + } + return member; + }) + ); + }; + + const setTeams = (teamSlugs: string[], index: number) => { + setMemberInvites(currentMemberInvites => + currentMemberInvites.map((member, i) => { + if (i === index) { + member.teamSlugs = new Set(teamSlugs); + } + return member; + }) + ); + }; + + const selectAll = (checked: boolean) => { + const selectedMembers = memberInvites.map(m => ({...m, selected: checked})); + setMemberInvites(selectedMembers); + }; + + const toggleCheckbox = (checked: boolean, index: number) => { + const selectedMembers = [...memberInvites]; + selectedMembers[index].selected = checked; + setMemberInvites(selectedMembers); + }; + + const renderStatusMessage = () => { + if (sendingInvites) { + return ( + + + {t('Sending organization invitations\u2026')} + + ); + } + + if (complete) { + const statuses = Object.values(inviteStatus); + const sentCount = statuses.filter(i => i.sent).length; + const errorCount = statuses.filter(i => i.error).length; + + const invites = {tn('%s invite', '%s invites', sentCount)}; + const tctComponents = { + invites, + failed: errorCount, + }; + + return ( + + + {errorCount > 0 + ? tct('Sent [invites], [failed] failed to send.', tctComponents) + : tct('Sent [invites]', tctComponents)} + + ); + } + + return null; + }; + + const sendMemberInvite = async (invite: MissingMemberInvite) => { + const data = { + email: invite.email, + teams: [...invite.teamSlugs], + role: invite.role, + }; + + try { + await api.requestPromise(`/organizations/${organization?.slug}/members/`, { + method: 'POST', + data, + }); + } catch (err) { + const errorResponse = err.responseJSON; + + // Use the email error message if available. This inconsistently is + // returned as either a list of errors for the field, or a single error. + const emailError = + !errorResponse || !errorResponse.email + ? false + : Array.isArray(errorResponse.email) + ? errorResponse.email[0] + : errorResponse.email; + + const error = emailError || t('Could not invite user'); + + setInviteStatus(prevInviteStatus => { + return {...prevInviteStatus, [invite.email]: {sent: false, error}}; + }); + } + + setInviteStatus(prevInviteStatus => { + return {...prevInviteStatus, [invite.email]: {sent: true}}; + }); + }; + + const sendMemberInvites = async () => { + setSendingInvites(true); + await Promise.all(memberInvites.filter(i => i.selected).map(sendMemberInvite)); + setSendingInvites(false); + setComplete(true); + + if (organization) { + trackAnalytics( + 'missing_members_invite_modal.requests_sent', + { + organization, + }, + {startSession: true} + ); + } + }; + + const selectedCount = memberInvites.filter(i => i.selected).length; + const selectedAll = memberInvites.length === selectedCount; + + const inviteButtonLabel = () => { + return tct('Invite [memberCount] missing member[isPlural]', { + memberCount: + memberInvites.length === selectedCount + ? `all ${selectedCount}` + : selectedCount === 0 + ? '' + : selectedCount, + isPlural: selectedCount !== 1 ? 's' : '', + }); + }; + + const hookRenderer: InviteModalRenderFunc = ({sendInvites, canSend, headerInfo}) => ( + +

{t('Invite Your Dev Team')}

+ {headerInfo} + selectAll(!selectedAll)} + checked={selectedAll} + />, + t('User Information'), + + {t('Recent Commits')} + + + + , + t('Role'), + t('Team'), + ]} + > + {memberInvites?.map((member, i) => { + const checked = memberInvites[i].selected; + const username = member.externalId.split(':').pop(); + return ( + +
+ toggleCheckbox(!checked, i)} + /> +
+ + + + + @{username} + + + {member.email} + + + + {member.commitCount} + + setRole(value?.value, i)} + /> + setTeams(opts ? opts.map(v => v.value) : [], i)} + multiple + clearable + /> +
+ ); + })} +
+
+
{renderStatusMessage()}
+ + + + +
+
+ ); + + return ( + + {hookRenderer} + + ); +} + +export default withOrganization(InviteMissingMembersModal); + +const StyledPanelTable = styled(PanelTable)` + grid-template-columns: max-content 1fr max-content 1fr 1fr; + overflow: visible; +`; + +const StyledHeader = styled('div')` + display: flex; + gap: ${space(0.5)}; +`; + +const StyledPanelItem = styled(PanelItem)` + flex-direction: column; +`; + +const Footer = styled('div')` + display: flex; + justify-content: space-between; +`; + +const ContentRow = styled('div')` + display: flex; + align-items: center; + font-size: ${p => p.theme.fontSizeMedium}; + & > *:first-child { + margin-right: ${space(0.75)}; + } +`; + +export const modalCss = css` + width: 80%; + max-width: 870px; +`; diff --git a/static/app/components/modals/inviteMissingMembersModal/types.tsx b/static/app/components/modals/inviteMissingMembersModal/types.tsx new file mode 100644 index 00000000000000..2e81d9ee65bc99 --- /dev/null +++ b/static/app/components/modals/inviteMissingMembersModal/types.tsx @@ -0,0 +1,8 @@ +export type MissingMemberInvite = { + commitCount: number; + email: string; + externalId: string; + role: string; + selected: boolean; + teamSlugs: Set; +}; diff --git a/static/app/components/onboarding/productSelection.tsx b/static/app/components/onboarding/productSelection.tsx index 1cebe3716888ed..a2b8b4b20978f7 100644 --- a/static/app/components/onboarding/productSelection.tsx +++ b/static/app/components/onboarding/productSelection.tsx @@ -64,7 +64,31 @@ export const platformProductAvailability = { ProductSolution.SESSION_REPLAY, ], python: [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-aiohttp': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-awslambda': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-bottle': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-celery': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-chalice': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-django': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-falcon': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-fastapi': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-flask': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-gcpfunctions': [ + ProductSolution.PERFORMANCE_MONITORING, + ProductSolution.PROFILING, + ], + 'python-pyramid': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-quart': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-rq': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-sanic': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-serverless': [ + ProductSolution.PERFORMANCE_MONITORING, + ProductSolution.PROFILING, + ], + 'python-tornado': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-starlette': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-tryton': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], + 'python-wsgi': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], } as Record; export type DisabledProduct = { diff --git a/static/app/components/pageOverlay.tsx b/static/app/components/pageOverlay.tsx index b1f13cce724f8f..3ae11f3c18b270 100644 --- a/static/app/components/pageOverlay.tsx +++ b/static/app/components/pageOverlay.tsx @@ -1,4 +1,4 @@ -import {Component, createRef} from 'react'; +import {useEffect, useRef} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import {motion} from 'framer-motion'; @@ -105,8 +105,6 @@ type Props = { customWrapper?: React.ComponentType; }; -type DefaultProps = Pick; - /** * When a background with a anchor is used and no positioningStrategy is * provided, by default we'll align the top left of the container to the anchor @@ -129,109 +127,96 @@ const defaultPositioning = ({mainRect, anchorRect}: PositioningStrategyOpts) => * wrapper to a safe space in the background to aid in alignment of the wrapper * to a safe space in the background. */ -class PageOverlay extends Component { - static defaultProps: DefaultProps = { - positioningStrategy: defaultPositioning, - }; - - componentDidMount() { - if (this.contentRef.current === null || this.anchorRef.current === null) { - return; +function PageOverlay({ + positioningStrategy = defaultPositioning, + text, + animateDelay, + background: BackgroundComponent, + customWrapper, + children, +}: Props) { + const contentRef = useRef(null); + const wrapperRef = useRef(null); + const anchorRef = useRef(null); + + useEffect(() => { + if (contentRef.current === null || anchorRef.current === null) { + return () => {}; } - this.anchorWrapper(); - - // Observe changes to the upsell container to reanchor if available - if (window.ResizeObserver) { - this.bgResizeObserver = new ResizeObserver(this.anchorWrapper); - this.bgResizeObserver.observe(this.contentRef.current); + /** + * Align the wrapper component to the anchor by computing x/y values using + * the passed function. By default if no function is specified it will align + * to the top left of the anchor. + */ + function anchorWrapper() { + if ( + contentRef.current === null || + wrapperRef.current === null || + anchorRef.current === null + ) { + return; + } + + // Absolute position the container, this avoids the browser having to reflow + // the component + wrapperRef.current.style.position = 'absolute'; + wrapperRef.current.style.left = `0px`; + wrapperRef.current.style.top = `0px`; + + const mainRect = contentRef.current.getBoundingClientRect(); + const anchorRect = anchorRef.current.getBoundingClientRect(); + const wrapperRect = wrapperRef.current.getBoundingClientRect(); + + // Compute the position of the wrapper + const {x, y} = positioningStrategy({mainRect, anchorRect, wrapperRect}); + + const transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`; + wrapperRef.current.style.transform = transform; } - } - - componentWillUnmount() { - this.bgResizeObserver?.disconnect(); - } - /** - * Used to re-anchor the text wrapper to the anchor point in the background when - * the size of the page changes. - */ - bgResizeObserver: ResizeObserver | null = null; + anchorWrapper(); - contentRef = createRef(); - wrapperRef = createRef(); - anchorRef = createRef(); + /** + * Used to re-anchor the text wrapper to the anchor point in the background when + * the size of the page changes. + */ + let bgResizeObserver: ResizeObserver | null = null; - /** - * Align the wrapper component to the anchor by computing x/y values using - * the passed function. By default if no function is specified it will align - * to the top left of the anchor. - */ - anchorWrapper = () => { - if ( - this.contentRef.current === null || - this.wrapperRef.current === null || - this.anchorRef.current === null - ) { - return; + // Observe changes to the upsell container to reanchor if available + if (window.ResizeObserver) { + bgResizeObserver = new ResizeObserver(anchorWrapper); + bgResizeObserver.observe(contentRef.current); } - // Absolute position the container, this avoids the browser having to reflow - // the component - this.wrapperRef.current.style.position = 'absolute'; - this.wrapperRef.current.style.left = `0px`; - this.wrapperRef.current.style.top = `0px`; - - const mainRect = this.contentRef.current.getBoundingClientRect(); - const anchorRect = this.anchorRef.current.getBoundingClientRect(); - const wrapperRect = this.wrapperRef.current.getBoundingClientRect(); - - // Compute the position of the wrapper - const {x, y} = this.props.positioningStrategy({mainRect, anchorRect, wrapperRect}); - - const transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`; - this.wrapperRef.current.style.transform = transform; - }; - - render() { - const { - text, - children, - animateDelay, - background: BackgroundComponent, - customWrapper, - ...props - } = this.props; - const Wrapper = customWrapper ?? DefaultWrapper; - - const transition = testableTransition({ - delay: 1, - duration: 1.2, - ease: 'easeInOut', - delayChildren: animateDelay ?? (BackgroundComponent ? 0.5 : 1.5), - staggerChildren: 0.15, - }); - - return ( - - {children} - - {BackgroundComponent && ( - - - - )} - - {text({Body, Header})} - - - - ); - } + return () => bgResizeObserver?.disconnect(); + }, [positioningStrategy]); + + const Wrapper = customWrapper ?? DefaultWrapper; + + const transition = testableTransition({ + delay: 1, + duration: 1.2, + ease: 'easeInOut', + delayChildren: animateDelay ?? (BackgroundComponent ? 0.5 : 1.5), + staggerChildren: 0.15, + }); + + return ( + + {children} + + {BackgroundComponent && ( + + + + )} + + {text({Body, Header})} + + + + ); } const absoluteFull = css` diff --git a/static/app/components/profiling/flamegraph/flamegraph.tsx b/static/app/components/profiling/flamegraph/flamegraph.tsx index af2d0f8ffdb668..19aaa22e696af7 100644 --- a/static/app/components/profiling/flamegraph/flamegraph.tsx +++ b/static/app/components/profiling/flamegraph/flamegraph.tsx @@ -157,6 +157,7 @@ function findLongestMatchingFrame( const LOADING_OR_FALLBACK_FLAMEGRAPH = FlamegraphModel.Empty(); const LOADING_OR_FALLBACK_SPAN_TREE = SpanTree.Empty; const LOADING_OR_FALLBACK_UIFRAMES = UIFrames.Empty; +const LOADING_OR_FALLBACK_BATTERY_CHART = FlamegraphChartModel.Empty; const LOADING_OR_FALLBACK_CPU_CHART = FlamegraphChartModel.Empty; const LOADING_OR_FALLBACK_MEMORY_CHART = FlamegraphChartModel.Empty; @@ -192,6 +193,9 @@ function Flamegraph(): ReactElement { const [uiFramesCanvasRef, setUIFramesCanvasRef] = useState( null ); + + const [batteryChartCanvasRef, setBatteryChartCanvasRef] = + useState(null); const [cpuChartCanvasRef, setCpuChartCanvasRef] = useState( null ); @@ -209,6 +213,14 @@ function Flamegraph(): ReactElement { ); }, [organization.features, profileGroup.metadata.platform]); + const hasBatteryChart = useMemo(() => { + const platform = profileGroup.metadata.platform; + return ( + platform === 'cocoa' && + organization.features.includes('profiling-battery-usage-chart') + ); + }, [profileGroup.metadata.platform, organization.features]); + const hasCPUChart = useMemo(() => { const platform = profileGroup.metadata.platform; return ( @@ -309,6 +321,26 @@ function Flamegraph(): ReactElement { hasUIFrames, ]); + const batteryChart = useMemo(() => { + if (!hasCPUChart) { + return LOADING_OR_FALLBACK_BATTERY_CHART; + } + + const measures: ProfileSeriesMeasurement[] = []; + + for (const key in profileGroup.measurements) { + if (key === 'cpu_energy_usage') { + measures.push({...profileGroup.measurements[key]!, name: 'CPU energy usage'}); + } + } + + return new FlamegraphChartModel( + Rect.From(flamegraph.configSpace), + measures.length > 0 ? measures : [], + flamegraphTheme.COLORS.BATTERY_CHART_COLORS + ); + }, [profileGroup.measurements, flamegraph.configSpace, flamegraphTheme, hasCPUChart]); + const CPUChart = useMemo(() => { if (!hasCPUChart) { return LOADING_OR_FALLBACK_CPU_CHART; @@ -400,6 +432,13 @@ function Flamegraph(): ReactElement { return new FlamegraphCanvas(uiFramesCanvasRef, vec2.fromValues(0, 0)); }, [uiFramesCanvasRef]); + const batteryChartCanvas = useMemo(() => { + if (!batteryChartCanvasRef) { + return null; + } + return new FlamegraphCanvas(batteryChartCanvasRef, vec2.fromValues(0, 0)); + }, [batteryChartCanvasRef]); + const cpuChartCanvas = useMemo(() => { if (!cpuChartCanvasRef) { return null; @@ -553,6 +592,48 @@ function Flamegraph(): ReactElement { [flamegraphView, flamegraphCanvas, flamegraph, uiFrames] ); + const batteryChartView = useMemoWithPrevious | null>( + _previousView => { + if (!flamegraphView || !flamegraphCanvas || !batteryChart || !batteryChartCanvas) { + return null; + } + + const newView = new CanvasView({ + canvas: flamegraphCanvas, + model: batteryChart, + mode: 'anchorBottom', + options: { + // Invert chart so origin is at bottom left + // corner as opposed to top left + inverted: true, + minWidth: uiFrames.minFrameDuration, + barHeight: 0, + depthOffset: 0, + maxHeight: batteryChart.configSpace.height, + minHeight: batteryChart.configSpace.height, + }, + }); + + // Compute the total size of the padding and stretch the view. This ensures that + // the total range is rendered and perfectly aligned from top to bottom. + newView.setConfigView( + flamegraphView.configView.withHeight(newView.configView.height), + { + width: {min: 0}, + } + ); + + return newView; + }, + [ + flamegraphView, + flamegraphCanvas, + batteryChart, + uiFrames.minFrameDuration, + batteryChartCanvas, + ] + ); + const cpuChartView = useMemoWithPrevious | null>( _previousView => { if (!flamegraphView || !flamegraphCanvas || !CPUChart || !cpuChartCanvas) { @@ -674,7 +755,8 @@ function Flamegraph(): ReactElement { spansView?.minWidth ?? Number.MAX_SAFE_INTEGER, uiFramesView?.minWidth ?? Number.MAX_SAFE_INTEGER, cpuChartView?.minWidth ?? Number.MAX_SAFE_INTEGER, - memoryChartView?.minWidth ?? Number.MAX_SAFE_INTEGER + memoryChartView?.minWidth ?? Number.MAX_SAFE_INTEGER, + batteryChartView?.minWidth ?? Number.MAX_SAFE_INTEGER ); flamegraphView?.setMinWidth?.(minWidthBetweenViews); @@ -682,7 +764,14 @@ function Flamegraph(): ReactElement { uiFramesView?.setMinWidth?.(minWidthBetweenViews); cpuChartView?.setMinWidth?.(minWidthBetweenViews); memoryChartView?.setMinWidth?.(minWidthBetweenViews); - }, [flamegraphView, spansView, uiFramesView, cpuChartView, memoryChartView]); + }, [ + flamegraphView, + spansView, + uiFramesView, + cpuChartView, + memoryChartView, + batteryChartView, + ]); // Uses a useLayoutEffect to ensure that these top level/global listeners are added before // any of the children components effects actually run. This way we do not lose events @@ -755,6 +844,9 @@ function Flamegraph(): ReactElement { if (uiFramesView) { uiFramesView.transformConfigView(mat); } + if (batteryChartView) { + batteryChartView.transformConfigView(mat); + } if (cpuChartView) { cpuChartView.transformConfigView(mat); } @@ -771,6 +863,9 @@ function Flamegraph(): ReactElement { if (uiFramesView) { uiFramesView.transformConfigView(mat); } + if (batteryChartView) { + batteryChartView.transformConfigView(mat); + } if (cpuChartView) { cpuChartView.transformConfigView(mat); } @@ -790,6 +885,9 @@ function Flamegraph(): ReactElement { if (uiFramesView && uiFramesCanvas) { uiFramesView.resetConfigView(uiFramesCanvas); } + if (batteryChartView && batteryChartCanvas) { + batteryChartView.resetConfigView(batteryChartCanvas); + } if (cpuChartView && cpuChartCanvas) { cpuChartView.resetConfigView(cpuChartCanvas); } @@ -815,6 +913,11 @@ function Flamegraph(): ReactElement { newConfigView.withHeight(uiFramesView.configView.height) ); } + if (batteryChartView) { + batteryChartView.setConfigView( + newConfigView.withHeight(batteryChartView.configView.height) + ); + } if (cpuChartView) { cpuChartView.setConfigView( newConfigView.withHeight(cpuChartView.configView.height) @@ -850,6 +953,11 @@ function Flamegraph(): ReactElement { newConfigView.withHeight(uiFramesView.configView.height) ); } + if (batteryChartView) { + batteryChartView.setConfigView( + newConfigView.withHeight(batteryChartView.configView.height) + ); + } if (cpuChartView) { cpuChartView.setConfigView( newConfigView.withHeight(cpuChartView.configView.height) @@ -889,6 +997,8 @@ function Flamegraph(): ReactElement { cpuChartView, memoryChartCanvas, memoryChartView, + batteryChartView, + batteryChartCanvas, ]); const minimapCanvases = useMemo(() => { @@ -924,6 +1034,17 @@ function Flamegraph(): ReactElement { uiFramesView ); + const batteryChartCanvases = useMemo(() => { + return [batteryChartCanvasRef]; + }, [batteryChartCanvasRef]); + + const batteryChartCanvasBounds = useResizeCanvasObserver( + batteryChartCanvases, + canvasPoolManager, + batteryChartCanvas, + batteryChartView + ); + const cpuChartCanvases = useMemo(() => { return [cpuChartCanvasRef]; }, [cpuChartCanvasRef]); @@ -1149,6 +1270,26 @@ function Flamegraph(): ReactElement { /> ) : null } + batteryChart={ + hasBatteryChart ? ( + + ) : null + } memoryChart={ hasMemoryChart ? ( { + dispatch({ + type: 'toggle timeline', + payload: {timeline: 'battery_chart', value: true}, + }); + }, [dispatch]); + + const onCloseBatteryChart = useCallback(() => { + dispatch({ + type: 'toggle timeline', + payload: {timeline: 'battery_chart', value: false}, + }); + }, [dispatch]); + const spansTreeDepth = props.spansTreeDepth ?? 0; const spansTimelineHeight = Math.min( @@ -197,6 +212,24 @@ export function FlamegraphLayout(props: FlamegraphLayoutProps) { ) : null} + {props.batteryChart ? ( + + + {props.batteryChart} + + + ) : null} {props.memoryChart ? ( layout === 'table bottom' - ? 'auto auto auto auto auto 1fr' + ? 'auto auto auto auto auto auto 1fr' : layout === 'table right' - ? 'min-content min-content min-content min-content min-content 1fr' - : 'min-content min-content min-content min-content min-content 1fr'}; + ? 'min-content min-content min-content min-content min-content min-content 1fr' + : 'min-content min-content min-content min-content min-content min-content 1fr'}; grid-template-columns: ${({layout}) => layout === 'table bottom' ? '100%' @@ -315,8 +348,9 @@ const FlamegraphGrid = styled('div')<{ layout === 'table bottom' ? ` 'minimap' - 'ui-frames' 'spans' + 'ui-frames' + 'battery-chart' 'memory-chart' 'cpu-chart' 'flamegraph' @@ -324,18 +358,20 @@ const FlamegraphGrid = styled('div')<{ ` : layout === 'table right' ? ` - 'minimap frame-stack' - 'ui-frames frame-stack' - 'spans frame-stack' - 'memory-chart frame-stack' - 'cpu-chart frame-stack' - 'flamegraph frame-stack' + 'minimap frame-stack' + 'spans frame-stack' + 'ui-frames frame-stack' + 'battery-chart frame-stack' + 'memory-chart frame-stack' + 'cpu-chart frame-stack' + 'flamegraph frame-stack' ` : layout === 'table left' ? ` 'frame-stack minimap' - 'frame-stack ui-frames' 'frame-stack spans' + 'frame-stack ui-frames' + 'frame-stack battery-chart' 'frame-stack memory-chart' 'frame-stack cpu-chart' 'frame-stack flamegraph' @@ -385,6 +421,14 @@ const MemoryChartContainer = styled('div')<{ grid-area: memory-chart; `; +const BatteryChartContainer = styled('div')<{ + containerHeight: FlamegraphTheme['SIZES']['BATTERY_CHART_HEIGHT']; +}>` + position: relative; + height: ${p => p.containerHeight}px; + grid-area: battery-chart; +`; + const UIFramesContainer = styled('div')<{ containerHeight: FlamegraphTheme['SIZES']['UI_FRAMES_HEIGHT']; }>` diff --git a/static/app/data/platformCategories.tsx b/static/app/data/platformCategories.tsx index 56821296beadd2..7f7884d0f199e4 100644 --- a/static/app/data/platformCategories.tsx +++ b/static/app/data/platformCategories.tsx @@ -112,6 +112,12 @@ export const backend = [ 'python-starlette', 'python-sanic', 'python-celery', + 'python-aiohttp', + 'python-chalice', + 'python-falcon', + 'python-quart', + 'python-tryton', + 'python-wsgi', 'python-bottle', 'python-pylons', 'python-pyramid', @@ -129,6 +135,7 @@ export const serverless = [ 'python-awslambda', 'python-azurefunctions', 'python-gcpfunctions', + 'python-serverless', 'node-awslambda', 'node-azurefunctions', 'node-gcpfunctions', diff --git a/static/app/data/platforms.tsx b/static/app/data/platforms.tsx index 7c1b358e87b82e..c4e654acbcc149 100644 --- a/static/app/data/platforms.tsx +++ b/static/app/data/platforms.tsx @@ -1,108 +1,689 @@ -import integrationDocsPlatforms from 'integration-docs-platforms'; import sortBy from 'lodash/sortBy'; import {t} from 'sentry/locale'; -import {PlatformIntegration} from 'sentry/types'; - -import {tracing} from './platformCategories'; - -const goPlatforms = [ - { - integrations: [ - ...(integrationDocsPlatforms.platforms.find(platform => platform.id === 'go') - ?.integrations ?? []), - { - link: 'https://docs.sentry.io/platforms/go/guides/echo/', - type: 'framework', - id: 'go-echo', - name: t('Echo'), - }, - { - link: 'https://docs.sentry.io/platforms/go/guides/fasthttp/', - type: 'framework', - id: 'go-fasthttp', - name: t('FastHTTP'), - }, - { - link: 'https://docs.sentry.io/platforms/go/guides/gin/', - type: 'framework', - id: 'go-gin', - name: t('Gin'), - }, - { - link: 'https://docs.sentry.io/platforms/go/guides/http/', - type: 'framework', - id: 'go-http', - name: t('Net/Http'), - }, - { - link: 'https://docs.sentry.io/platforms/go/guides/iris', - type: 'framework', - id: 'go-iris', - name: t('Iris'), - }, - { - link: 'https://docs.sentry.io/platforms/go/guides/martini/', - type: 'framework', - id: 'go-martini', - name: t('Martini'), - }, - { - link: 'https://docs.sentry.io/platforms/go/guides/negroni/', - type: 'framework', - id: 'go-negroni', - name: t('Negroni'), - }, - ], +import type {PlatformIntegration} from 'sentry/types'; + +type Platform = { + id: string; + language: string; + link: string; + name: string; + type: string; +}; + +const goPlatforms: Platform[] = [ + { id: 'go', - name: t('Go'), + link: 'https://docs.sentry.io/platforms/go/', + name: 'Go', + type: 'language', + language: 'go', + }, + { + link: 'https://docs.sentry.io/platforms/go/guides/echo/', + type: 'framework', + id: 'go-echo', + name: t('Echo'), + language: 'go', + }, + { + link: 'https://docs.sentry.io/platforms/go/guides/fasthttp/', + type: 'framework', + id: 'go-fasthttp', + name: t('FastHTTP'), + language: 'go', + }, + { + link: 'https://docs.sentry.io/platforms/go/guides/gin/', + type: 'framework', + id: 'go-gin', + name: t('Gin'), + language: 'go', + }, + { + link: 'https://docs.sentry.io/platforms/go/guides/http/', + type: 'framework', + id: 'go-http', + name: t('Net/Http'), + language: 'go', + }, + { + link: 'https://docs.sentry.io/platforms/go/guides/iris', + type: 'framework', + id: 'go-iris', + name: t('Iris'), + language: 'go', + }, + { + link: 'https://docs.sentry.io/platforms/go/guides/martini/', + type: 'framework', + id: 'go-martini', + name: t('Martini'), + language: 'go', + }, + { + link: 'https://docs.sentry.io/platforms/go/guides/negroni/', + type: 'framework', + id: 'go-negroni', + name: t('Negroni'), + language: 'go', + }, +]; + +const javaScriptPlatforms: Platform[] = [ + { + id: 'javascript-angular', + name: 'Angular', + type: 'framework', + language: 'javascript', + link: 'https://docs.sentry.io/platforms/javascript/guides/angular/', + }, + { + id: 'javascript', + name: 'Browser JavaScript', + type: 'language', + language: 'javascript', + link: 'https://docs.sentry.io/platforms/javascript/', + }, + { + id: 'javascript-ember', + name: 'Ember', + type: 'framework', + language: 'javascript', + link: 'https://docs.sentry.io/platforms/javascript/guides/ember/', + }, + { + id: 'javascript-gatsby', + name: 'Gatsby', + type: 'framework', + language: 'javascript', + link: 'https://docs.sentry.io/platforms/javascript/guides/gatsby/', + }, + { + id: 'javascript-nextjs', + name: 'Next.js', + type: 'framework', + language: 'javascript', + link: 'https://docs.sentry.io/platforms/javascript/guides/nextjs/', + }, + { + id: 'javascript-react', + name: 'React', + type: 'framework', + language: 'javascript', + link: 'https://docs.sentry.io/platforms/javascript/guides/react/', + }, + { + id: 'javascript-remix', + name: 'Remix', + type: 'framework', + language: 'javascript', + link: 'https://docs.sentry.io/platforms/javascript/guides/remix/', + }, + { + id: 'javascript-svelte', + name: 'Svelte', + type: 'framework', + language: 'javascript', + link: 'https://docs.sentry.io/platforms/javascript/guides/svelte/', + }, + { + id: 'javascript-sveltekit', + name: 'SvelteKit', + type: 'framework', + language: 'javascript', + link: 'https://docs.sentry.io/platforms/javascript/guides/sveltekit/', + }, + { + id: 'javascript-vue', + name: 'Vue', + type: 'framework', + language: 'javascript', + link: 'https://docs.sentry.io/platforms/javascript/guides/vue/', + }, +]; + +const javaPlatforms: Platform[] = [ + { + id: 'java', + name: 'Java', + type: 'language', + language: 'java', + link: 'https://docs.sentry.io/platforms/java/', + }, + { + id: 'java-log4j2', + name: 'Log4j 2.x', + type: 'framework', + language: 'java', + link: 'https://docs.sentry.io/platforms/java/guides/log4j2/', + }, + { + id: 'java-logback', + name: 'Logback', + type: 'framework', + language: 'java', + link: 'https://docs.sentry.io/platforms/java/guides/logback/', + }, + { + id: 'java-spring', + name: 'Spring', + type: 'framework', + language: 'java', + link: 'https://https://docs.sentry.io/platforms/java/guides/spring/', + }, + { + id: 'java-spring-boot', + name: 'Spring Boot', + type: 'framework', + language: 'java', + link: 'https://docs.sentry.io/platforms/java/guides/spring-boot/', }, ]; -const platformIntegrations: PlatformIntegration[] = [ - ...integrationDocsPlatforms.platforms.filter(platform => platform.id !== 'go'), +const pythonPlatforms: Platform[] = [ + { + id: 'python-aiohttp', + name: 'AIOHTTP', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/aiohttp/', + }, + { + id: 'python-asgi', + name: 'ASGI', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/asgi/', + }, + { + id: 'python-awslambda', + name: 'AWS Lambda (Python)', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/aws-lambda/', + }, + { + id: 'python-bottle', + name: 'Bottle', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/bottle/', + }, + { + id: 'python-celery', + name: 'Celery', + type: 'library', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/celery/', + }, + { + id: 'python-chalice', + name: 'Chalice', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/chalice/', + }, + { + id: 'python-django', + name: 'Django', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/django/', + }, + { + id: 'python-falcon', + name: 'Falcon', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/falcon/', + }, + { + id: 'python-fastapi', + name: 'FastAPI', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/fastapi/', + }, + { + id: 'python-flask', + name: 'Flask', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/flask/', + }, + { + id: 'python-gcpfunctions', + name: 'Google Cloud Functions (Python)', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/gcp-functions/', + }, + { + id: 'python-pymongo', + name: 'PyMongo', + type: 'library', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/pymongo/', + }, + { + id: 'python-pylons', + name: 'Pylons', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/legacy-sdk/integrations/pylons/', + }, + { + id: 'python-pyramid', + name: 'Pyramid', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/pyramid/', + }, + { + id: 'python', + name: 'Python', + type: 'language', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/', + }, + { + id: 'python-quart', + name: 'Quart', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/quart/', + }, + { + id: 'python-rq', + name: 'RQ (Redis Queue)', + type: 'library', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/rq/', + }, + { + id: 'python-sanic', + name: 'Sanic', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/sanic/', + }, + { + id: 'python-serverless', + name: 'Serverless', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/serverless/', + }, + { + id: 'python-starlette', + name: 'Starlette', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/starlette/', + }, + { + id: 'python-tornado', + name: 'Tornado', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/tornado/', + }, + { + id: 'python-tryton', + name: 'Tryton', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/tryton/', + }, + { + id: 'python-wsgi', + name: 'WSGI', + type: 'framework', + language: 'python', + link: 'https://docs.sentry.io/platforms/python/guides/wsgi/', + }, +]; + +const phpPlatforms: Platform[] = [ + { + id: 'php-laravel', + name: 'Laravel', + type: 'framework', + language: 'php', + link: 'https://docs.sentry.io/platforms/php/guides/laravel/', + }, + { + id: 'php', + name: 'PHP', + type: 'language', + language: 'php', + link: 'https://docs.sentry.io/platforms/php/', + }, + { + id: 'php-symfony', + name: 'Symfony', + type: 'framework', + language: 'php', + link: 'https://docs.sentry.io/platforms/php/guides/symfony/', + }, +]; + +const nodePlatforms: Platform[] = [ + { + id: 'node-awslambda', + name: 'AWS Lambda (Node)', + type: 'framework', + language: 'node', + link: 'https://docs.sentry.io/platforms/node/guides/aws-lambda/', + }, + { + id: 'node-azurefunctions', + name: 'Azure Functions (Node)', + type: 'framework', + language: 'node', + link: 'https://docs.sentry.io/platforms/node/guides/azure-functions/', + }, + { + id: 'node-connect', + name: 'Connect', + type: 'framework', + language: 'node', + link: 'https://docs.sentry.io/platforms/node/guides/connect/', + }, + { + id: 'node-express', + name: 'Express', + type: 'framework', + language: 'node', + link: 'https://docs.sentry.io/platforms/node/guides/express/', + }, + { + id: 'node-gcpfunctions', + name: 'Google Cloud Functions (Node)', + type: 'framework', + language: 'node', + link: 'https://docs.sentry.io/platforms/node/guides/gcp-functions/', + }, + { + id: 'node-koa', + name: 'Koa', + type: 'framework', + language: 'node', + link: 'https://docs.sentry.io/platforms/node/guides/koa/', + }, + { + id: 'node', + name: 'Node.js', + type: 'language', + language: 'node', + link: 'https://docs.sentry.io/platforms/node/', + }, + { + id: 'node-serverlesscloud', + name: 'Serverless (Node)', + type: 'framework', + language: 'node', + link: 'https://docs.sentry.io/platforms/node/guides/serverless-cloud/', + }, +]; + +const dotNetPlatforms: Platform[] = [ + { + id: 'dotnet', + name: '.NET', + type: 'language', + language: 'dotnet', + link: 'https://docs.sentry.io/platforms/dotnet/', + }, + { + id: 'dotnet-aspnet', + name: 'ASP.NET', + type: 'framework', + language: 'dotnet', + link: 'https://docs.sentry.io/platforms/dotnet/guides/aspnet/', + }, + { + id: 'dotnet-aspnetcore', + name: 'ASP.NET Core', + type: 'framework', + language: 'dotnet', + link: 'https://docs.sentry.io/platforms/dotnet/guides/aspnetcore/', + }, + { + id: 'dotnet-awslambda', + name: 'AWS Lambda (.NET)', + type: 'framework', + language: 'dotnet', + link: 'https://docs.sentry.io/platforms/dotnet/guides/aws-lambda/', + }, + { + id: 'dotnet-gcpfunctions', + name: 'Google Cloud Functions (.NET)', + type: 'framework', + language: 'dotnet', + link: 'https://docs.sentry.io/platforms/dotnet/guides/google-cloud-functions/', + }, + { + id: 'dotnet-maui', + name: 'Multi-platform App UI (MAUI)', + type: 'framework', + language: 'dotnet', + link: 'https://docs.sentry.io/platforms/dotnet/guides/maui/', + }, + { + id: 'dotnet-uwp', + name: 'UWP', + type: 'framework', + language: 'dotnet', + link: 'https://docs.sentry.io/platforms/dotnet/guides/uwp/', + }, + { + id: 'dotnet-wpf', + name: 'WPF', + type: 'framework', + language: 'dotnet', + link: 'https://docs.sentry.io/platforms/dotnet/guides/wpf/', + }, + { + id: 'dotnet-winforms', + name: 'Windows Forms', + type: 'framework', + language: 'dotnet', + link: 'https://docs.sentry.io/platforms/dotnet/guides/winforms/', + }, + { + id: 'dotnet-xamarin', + name: 'Xamarin', + type: 'framework', + language: 'dotnet', + link: 'https://docs.sentry.io/platforms/dotnet/guides/xamarin/', + }, +]; + +const applePlatforms: Platform[] = [ + { + id: 'apple', + name: 'Apple', + type: 'language', + language: 'apple', + link: 'https://docs.sentry.io/platforms/apple/', + }, + { + id: 'apple-ios', + name: 'iOS', + type: 'language', + language: 'apple', + link: 'https://docs.sentry.io/platforms/apple/', + }, + { + id: 'apple-macos', + name: 'macOS', + type: 'language', + language: 'apple', + link: 'https://docs.sentry.io/platforms/apple/', + }, +]; + +const rubyPlatforms: Platform[] = [ + { + id: 'ruby-rack', + name: 'Rack Middleware', + type: 'framework', + language: 'ruby', + link: 'https://docs.sentry.io/platforms/ruby/guides/rack/', + }, + { + id: 'ruby-rails', + name: 'Rails', + type: 'framework', + language: 'ruby', + link: 'https://docs.sentry.io/platforms/ruby/guides/rails/', + }, + { + id: 'ruby', + name: 'Ruby', + type: 'language', + language: 'ruby', + link: 'https://docs.sentry.io/platforms/ruby/', + }, +]; + +const nativePlatforms: Platform[] = [ + { + id: 'native', + name: 'Native', + type: 'language', + language: 'native', + link: 'https://docs.sentry.io/platforms/native/', + }, + { + id: 'native-qt', + name: 'Qt', + type: 'framework', + language: 'native', + link: 'https://docs.sentry.io/platforms/native/guides/qt/', + }, +]; + +const otherPlatforms: Platform[] = [ + { + id: 'android', + name: 'Android', + type: 'framework', + language: 'android', + link: 'https://docs.sentry.io/platforms/android/', + }, + { + id: 'capacitor', + name: 'Capacitor', + type: 'framework', + language: 'capacitor', + link: 'https://docs.sentry.io/platforms/javascript/guides/capacitor/', + }, + { + id: 'cordova', + name: 'Cordova', + type: 'language', + language: 'cordova', + link: 'https://docs.sentry.io/platforms/javascript/guides/cordova/', + }, + { + id: 'dart', + name: 'Dart', + type: 'framework', + language: 'dart', + link: 'https://docs.sentry.io/platforms/dart/', + }, + { + id: 'electron', + name: 'Electron', + type: 'language', + language: 'electron', + link: 'https://docs.sentry.io/platforms/javascript/guides/electron/', + }, + { + id: 'elixir', + name: 'Elixir', + type: 'language', + language: 'elixir', + link: 'https://docs.sentry.io/platforms/elixir/', + }, + { + id: 'flutter', + name: 'Flutter', + type: 'framework', + language: 'flutter', + link: 'https://docs.sentry.io/platforms/flutter/', + }, + { + id: 'ionic', + name: 'Ionic', + type: 'framework', + language: 'ionic', + link: 'https://docs.sentry.io/platforms/javascript/guides/capacitor/', + }, + { + id: 'kotlin', + name: 'Kotlin', + type: 'language', + language: 'kotlin', + link: 'https://docs.sentry.io/platforms/kotlin/', + }, + { + id: 'minidump', + name: 'Minidump', + type: 'framework', + language: 'minidump', + link: 'https://docs.sentry.io/platforms/native/minidump/', + }, + { + id: 'rust', + name: 'Rust', + type: 'language', + language: 'rust', + link: 'https://docs.sentry.io/platforms/rust/', + }, + { + id: 'unity', + name: 'Unity', + type: 'framework', + language: 'unity', + link: 'https://docs.sentry.io/platforms/unity/', + }, + { + id: 'unreal', + name: 'Unreal Engine', + type: 'framework', + language: 'unreal', + link: 'https://docs.sentry.io/platforms/unreal/', + }, + { + id: 'react-native', + name: 'React Native', + type: 'language', + language: 'react-native', + link: 'https://docs.sentry.io/platforms/react-native/', + }, +]; + +// If you update items of this list, please remember to update the "GETTING_STARTED_DOCS_PLATFORMS" list +// in the 'src/sentry/models/project.py' file. This way, they'll work together correctly. +// Otherwise, creating a project will cause an error in the backend, saying "Invalid platform". +const allPlatforms = [ + ...javaScriptPlatforms, + ...nodePlatforms, + ...dotNetPlatforms, + ...applePlatforms, + ...javaPlatforms, + ...pythonPlatforms, + ...phpPlatforms, ...goPlatforms, -] - .map(platform => { - const integrations = platform.integrations.reduce((acc, value) => { - // filter out any javascript-[angular|angularjs|ember|gatsby|nextjs|react|remix|svelte|sveltekit|vue]-* platforms; as they're not meant to be used as a platform in the PlatformPicker component - if (value.id.match('^javascript-([A-Za-z]+)-([a-zA-Z0-9]+.*)$')) { - return acc; - } - - // filter out any tracing platforms; as they're not meant to be used as a platform for - // the project creation flow - if ((tracing as readonly string[]).includes(value.id)) { - return acc; - } - - // filter out any performance onboarding documentation - if (value.id.includes('performance-onboarding')) { - return acc; - } - - // filter out any replay onboarding documentation - if (value.id.includes('replay-onboarding')) { - return acc; - } - - // filter out any profiling onboarding documentation - if (value.id.includes('profiling-onboarding')) { - return acc; - } - - if (!acc[value.id]) { - acc[value.id] = {...value, language: platform.id}; - return acc; - } - - return acc; - }, {}); - - return Object.values(integrations) as PlatformIntegration[]; - }) - .flat(); - -const platforms = sortBy(platformIntegrations, 'id'); + ...rubyPlatforms, + ...nativePlatforms, + ...otherPlatforms, +]; + +const platforms = sortBy(allPlatforms, 'id') as PlatformIntegration[]; export default platforms; diff --git a/static/app/gettingStartedDocs/python/aiohttp.spec.tsx b/static/app/gettingStartedDocs/python/aiohttp.spec.tsx index c1b9ddf6a4d0ec..a92dea7e96b6e5 100644 --- a/static/app/gettingStartedDocs/python/aiohttp.spec.tsx +++ b/static/app/gettingStartedDocs/python/aiohttp.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithAIOHTTP', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/aiohttp.tsx b/static/app/gettingStartedDocs/python/aiohttp.tsx index acea5ca5c2cf7f..b45cc5b1c44f16 100644 --- a/static/app/gettingStartedDocs/python/aiohttp.tsx +++ b/static/app/gettingStartedDocs/python/aiohttp.tsx @@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + const introduction = (

{tct( @@ -17,8 +29,10 @@ const introduction = ( ); export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description: ( @@ -60,15 +74,7 @@ import sentry_sdk from sentry_sdk.integrations.aiohttp import AioHttpIntegration sentry_sdk.init( - dsn="${dsn}", - integrations=[ - AioHttpIntegration(), - ], - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) from aiohttp import web @@ -87,8 +93,37 @@ web.run_app(app) ]; // Configuration End -export function GettingStartedWithAIOHTTP({dsn, ...props}: ModuleProps) { - return ; +export function GettingStartedWithAIOHTTP({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [ + ` dsn="${dsn}",`, + ` integrations=[AioHttpIntegration()],`, + ]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); } export default GettingStartedWithAIOHTTP; diff --git a/static/app/gettingStartedDocs/python/awslambda.spec.tsx b/static/app/gettingStartedDocs/python/awslambda.spec.tsx index 3406c669bf5898..19acf6ddd1afee 100644 --- a/static/app/gettingStartedDocs/python/awslambda.spec.tsx +++ b/static/app/gettingStartedDocs/python/awslambda.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithAwsLambda', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content', dsn: 'test-dsn'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/awslambda.tsx b/static/app/gettingStartedDocs/python/awslambda.tsx index f2563efc656390..14bc04cfe49634 100644 --- a/static/app/gettingStartedDocs/python/awslambda.tsx +++ b/static/app/gettingStartedDocs/python/awslambda.tsx @@ -6,10 +6,22 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + const introduction = (

{tct( @@ -25,7 +37,11 @@ const introduction = ( export const steps = ({ dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + dsn: string; + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description: ( @@ -51,15 +67,7 @@ import sentry_sdk from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration sentry_sdk.init( - dsn="${dsn}", - integrations=[ - AwsLambdaIntegration(), - ], - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) def my_function(event, context): @@ -108,10 +116,6 @@ sentry_sdk.init( integrations=[ AwsLambdaIntegration(timeout_warning=True), ], - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, ) `, }, @@ -123,10 +127,35 @@ sentry_sdk.init( ]; // Configuration End -export function GettingStartedWithAwsLambda({dsn, ...props}: ModuleProps) { +export function GettingStartedWithAwsLambda({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [ + ` dsn="${dsn}",`, + ` integrations=[AwsLambdaIntegration()],`, + ]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + return ( - + {tct( 'If you are using another web framework inside of AWS Lambda, the framework might catch those exceptions before we get to see them. Make sure to enable the framework specific integration as well, if one exists. See [link:Integrations] for more information.', diff --git a/static/app/gettingStartedDocs/python/bottle.spec.tsx b/static/app/gettingStartedDocs/python/bottle.spec.tsx index f9260a6a640182..052731acba579b 100644 --- a/static/app/gettingStartedDocs/python/bottle.spec.tsx +++ b/static/app/gettingStartedDocs/python/bottle.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithBottle', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/bottle.tsx b/static/app/gettingStartedDocs/python/bottle.tsx index bab134199e265e..04ab294cb9f1da 100644 --- a/static/app/gettingStartedDocs/python/bottle.tsx +++ b/static/app/gettingStartedDocs/python/bottle.tsx @@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + const introduction = (

{tct( @@ -17,8 +29,10 @@ const introduction = ( ); export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description: ( @@ -54,14 +68,7 @@ from bottle import Bottle, run from sentry_sdk.integrations.bottle import BottleIntegration sentry_sdk.init( - dsn="${dsn}", - integrations=[ - BottleIntegration(), - ], - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) app = Bottle() @@ -72,8 +79,37 @@ app = Bottle() ]; // Configuration End -export function GettingStartedWithBottle({dsn, ...props}: ModuleProps) { - return ; +export function GettingStartedWithBottle({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [ + ` dsn="${dsn}",`, + ` integrations=[BottleIntegration()],`, + ]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); } export default GettingStartedWithBottle; diff --git a/static/app/gettingStartedDocs/python/celery.spec.tsx b/static/app/gettingStartedDocs/python/celery.spec.tsx index 6f63b36600bfda..3b7622b6d77091 100644 --- a/static/app/gettingStartedDocs/python/celery.spec.tsx +++ b/static/app/gettingStartedDocs/python/celery.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithCelery', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/celery.tsx b/static/app/gettingStartedDocs/python/celery.tsx index 53ebe57dea3e0a..87edc336e6dbc5 100644 --- a/static/app/gettingStartedDocs/python/celery.tsx +++ b/static/app/gettingStartedDocs/python/celery.tsx @@ -4,9 +4,21 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + const introduction = (

{tct('The celery integration adds support for the [link:Celery Task Queue System].', { @@ -16,8 +28,10 @@ const introduction = ( ); export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.CONFIGURE, description: ( @@ -39,15 +53,7 @@ import sentry_sdk from sentry_sdk.integrations.celery import CeleryIntegration sentry_sdk.init( - dsn='${dsn}', - integrations=[ - CeleryIntegration(), - ], - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) `, }, @@ -66,8 +72,37 @@ sentry_sdk.init( ]; // Configuration End -export function GettingStartedWithCelery({dsn, ...props}: ModuleProps) { - return ; +export function GettingStartedWithCelery({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [ + ` dsn="${dsn}",`, + ` integrations=[CeleryIntegration()],`, + ]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); } export default GettingStartedWithCelery; diff --git a/static/app/gettingStartedDocs/python/chalice.spec.tsx b/static/app/gettingStartedDocs/python/chalice.spec.tsx index 954771244c5cd5..d6228184d05d8e 100644 --- a/static/app/gettingStartedDocs/python/chalice.spec.tsx +++ b/static/app/gettingStartedDocs/python/chalice.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithChalice', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/chalice.tsx b/static/app/gettingStartedDocs/python/chalice.tsx index 24c1c97a6ec997..d28d32db0d36ca 100644 --- a/static/app/gettingStartedDocs/python/chalice.tsx +++ b/static/app/gettingStartedDocs/python/chalice.tsx @@ -1,13 +1,26 @@ import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {tct} from 'sentry/locale'; // Configuration Start +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description: ( @@ -37,15 +50,7 @@ from sentry_sdk.integrations.chalice import ChaliceIntegration sentry_sdk.init( - dsn="${dsn}", - integrations=[ - ChaliceIntegration(), - ], - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) app = Chalice(app_name="appname") @@ -56,8 +61,35 @@ app = Chalice(app_name="appname") ]; // Configuration End -export function GettingStartedWithChalice({dsn, ...props}: ModuleProps) { - return ; -} +export function GettingStartedWithChalice({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [ + ` dsn="${dsn}",`, + ` integrations=[ChaliceIntegration()],`, + ]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); +} export default GettingStartedWithChalice; diff --git a/static/app/gettingStartedDocs/python/django.tsx b/static/app/gettingStartedDocs/python/django.tsx index 5062654d390ba3..9fab29d4c37d60 100644 --- a/static/app/gettingStartedDocs/python/django.tsx +++ b/static/app/gettingStartedDocs/python/django.tsx @@ -7,19 +7,19 @@ import {t, tct} from 'sentry/locale'; // Configuration Start -const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% - # of sampled transactions. - # We recommend adjusting this value in production. - profiles_sample_rate=1.0,`; +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; -const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production. - traces_sample_rate=1.0,`; +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; -const piiConfiguration = ` # If you wish to associate users to errors (assuming you are using - # django.contrib.auth) you may enable sending PII data. - send_default_pii=True,`; +const piiConfiguration = ` # If you wish to associate users to errors (assuming you are using + # django.contrib.auth) you may enable sending PII data. + send_default_pii=True,`; export const steps = ({ sentryInitContent, @@ -80,12 +80,12 @@ ${sentryInitContent} from django.urls import path def trigger_error(request): - division_by_zero = 1 / 0 + division_by_zero = 1 / 0 - urlpatterns = [ - path('sentry-debug/', trigger_error), - # ... - ] + urlpatterns = [ + path('sentry-debug/', trigger_error), + # ... + ] `, }, ], @@ -104,8 +104,8 @@ export function GettingStartedWithDjango({ const otherConfigs: string[] = []; let sentryInitContent: string[] = [ - ` dsn="${dsn}",`, - ` integrations=[DjangoIntegration()],`, + ` dsn="${dsn}",`, + ` integrations=[DjangoIntegration()],`, piiConfiguration, ]; diff --git a/static/app/gettingStartedDocs/python/falcon.spec.tsx b/static/app/gettingStartedDocs/python/falcon.spec.tsx index 5f27afdc562610..1c888ed1c2a673 100644 --- a/static/app/gettingStartedDocs/python/falcon.spec.tsx +++ b/static/app/gettingStartedDocs/python/falcon.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithFalcon', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/falcon.tsx b/static/app/gettingStartedDocs/python/falcon.tsx index e3fa713c34eebd..6c0617c34a94d4 100644 --- a/static/app/gettingStartedDocs/python/falcon.tsx +++ b/static/app/gettingStartedDocs/python/falcon.tsx @@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + const introduction = (

{tct( @@ -17,8 +29,10 @@ const introduction = ( ); export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description: ( @@ -53,15 +67,7 @@ import sentry_sdk from sentry_sdk.integrations.falcon import FalconIntegration sentry_sdk.init( - dsn="${dsn}", - integrations=[ - FalconIntegration(), - ], - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) api = falcon.API() @@ -72,8 +78,37 @@ api = falcon.API() ]; // Configuration End -export function GettingStartedWithFalcon({dsn, ...props}: ModuleProps) { - return ; +export function GettingStartedWithFalcon({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [ + ` dsn="${dsn}",`, + ` integrations=[FalconIntegration()],`, + ]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); } export default GettingStartedWithFalcon; diff --git a/static/app/gettingStartedDocs/python/fastapi.spec.tsx b/static/app/gettingStartedDocs/python/fastapi.spec.tsx index e836ef82b995d5..8eb49e13340da5 100644 --- a/static/app/gettingStartedDocs/python/fastapi.spec.tsx +++ b/static/app/gettingStartedDocs/python/fastapi.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithFastApi', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/fastapi.tsx b/static/app/gettingStartedDocs/python/fastapi.tsx index c8a8c3c7f0af0f..b6fdefe1e2ced0 100644 --- a/static/app/gettingStartedDocs/python/fastapi.tsx +++ b/static/app/gettingStartedDocs/python/fastapi.tsx @@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + const introduction = (

{tct('The FastAPI integration adds support for the [link:FastAPI Framework].', { @@ -14,8 +26,10 @@ const introduction = ( ); export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description: ( @@ -51,12 +65,7 @@ import sentry_sdk sentry_sdk.init( - dsn="${dsn}", - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) app = FastAPI() @@ -100,8 +109,34 @@ division_by_zero = 1 / 0 ]; // Configuration End -export function GettingStartedWithFastApi({dsn, ...props}: ModuleProps) { - return ; +export function GettingStartedWithFastApi({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [` dsn="${dsn}",`]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); } export default GettingStartedWithFastApi; diff --git a/static/app/gettingStartedDocs/python/flask.spec.tsx b/static/app/gettingStartedDocs/python/flask.spec.tsx index d3495777731c22..39ab492bc57642 100644 --- a/static/app/gettingStartedDocs/python/flask.spec.tsx +++ b/static/app/gettingStartedDocs/python/flask.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithDjango', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/flask.tsx b/static/app/gettingStartedDocs/python/flask.tsx index 83ad6ba926c975..d29e46d5e94d43 100644 --- a/static/app/gettingStartedDocs/python/flask.tsx +++ b/static/app/gettingStartedDocs/python/flask.tsx @@ -2,12 +2,26 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description: ( @@ -54,15 +68,7 @@ from flask import Flask from sentry_sdk.integrations.flask import FlaskIntegration sentry_sdk.init( - dsn="${dsn}", - integrations=[ - FlaskIntegration(), - ], - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production. - traces_sample_rate=1.0 +${sentryInitContent} ) app = Flask(__name__) @@ -100,8 +106,36 @@ def trigger_error(): ]; // Configuration End -export function GettingStartedWithFlask({dsn, ...props}: ModuleProps) { - return ; +export function GettingStartedWithFlask({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [ + ` dsn="${dsn}",`, + ` integrations=[FlaskIntegration()],`, + ]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); } export default GettingStartedWithFlask; diff --git a/static/app/gettingStartedDocs/python/gcpfunctions.spec.tsx b/static/app/gettingStartedDocs/python/gcpfunctions.spec.tsx index febad135a18a42..f24fddcfdaa53e 100644 --- a/static/app/gettingStartedDocs/python/gcpfunctions.spec.tsx +++ b/static/app/gettingStartedDocs/python/gcpfunctions.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithGCPFunctions', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content', dsn: 'test-dsn'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/gcpfunctions.tsx b/static/app/gettingStartedDocs/python/gcpfunctions.tsx index 31a165a0698c12..2c2b3189ffe614 100644 --- a/static/app/gettingStartedDocs/python/gcpfunctions.tsx +++ b/static/app/gettingStartedDocs/python/gcpfunctions.tsx @@ -6,13 +6,29 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + export const steps = ({ dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + dsn: string; + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description: ( @@ -38,19 +54,11 @@ import sentry_sdk from sentry_sdk.integrations.gcp import GcpIntegration sentry_sdk.init( - dsn="${dsn}", - integrations=[ - GcpIntegration(), - ], - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) def http_function_entrypoint(request): - ... + ... `, }, ], @@ -91,15 +99,10 @@ def http_function_entrypoint(request): language: 'python', code: ` sentry_sdk.init( - dsn="${dsn}", - integrations=[ - GcpIntegration(timeout_warning=True), - ], - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, + dsn="${dsn}", + integrations=[ + GcpIntegration(timeout_warning=True), + ], ) `, }, @@ -111,10 +114,34 @@ sentry_sdk.init( ]; // Configuration End -export function GettingStartedWithGCPFunctions({dsn, ...props}: ModuleProps) { +export function GettingStartedWithGCPFunctions({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [ + ` dsn="${dsn}",`, + ` integrations=[GcpIntegration()],`, + ]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + return ( - + {tct( 'If you are using a web framework in your Cloud Function, the framework might catch those exceptions before we get to see them. Make sure to enable the framework specific integration as well, if one exists. See [link:Integrations] for more information.', diff --git a/static/app/gettingStartedDocs/python/pyramid.spec.tsx b/static/app/gettingStartedDocs/python/pyramid.spec.tsx index ff05c418454580..d2019c494a2cb0 100644 --- a/static/app/gettingStartedDocs/python/pyramid.spec.tsx +++ b/static/app/gettingStartedDocs/python/pyramid.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithPyramid', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/pyramid.tsx b/static/app/gettingStartedDocs/python/pyramid.tsx index 7fb8994f8d8491..224ab2b221ef05 100644 --- a/static/app/gettingStartedDocs/python/pyramid.tsx +++ b/static/app/gettingStartedDocs/python/pyramid.tsx @@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + const introduction = (

{tct( @@ -17,8 +29,10 @@ const introduction = ( ); export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description:

{tct('Install [code:sentry-sdk] from PyPI:', {code: })}

, @@ -45,26 +59,18 @@ import sentry_sdk from sentry_sdk.integrations.pyramid import PyramidIntegration sentry_sdk.init( - dsn="${dsn}", - integrations=[ - PyramidIntegration(), - ], - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production. - traces_sample_rate=1.0, +${sentryInitContent} ) def sentry_debug(request): -division_by_zero = 1 / 0 + division_by_zero = 1 / 0 with Configurator() as config: -config.add_route('sentry-debug', '/') -config.add_view(sentry_debug, route_name='sentry-debug') -app = config.make_wsgi_app() -server = make_server('0.0.0.0', 6543, app) -server.serve_forever() + config.add_route('sentry-debug', '/') + config.add_view(sentry_debug, route_name='sentry-debug') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 6543, app) + server.serve_forever() `, }, ], @@ -72,8 +78,37 @@ server.serve_forever() ]; // Configuration End -export function GettingStartedWithPyramid({dsn, ...props}: ModuleProps) { - return ; +export function GettingStartedWithPyramid({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [ + ` dsn="${dsn}",`, + ` integrations=[PyramidIntegration()],`, + ]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); } export default GettingStartedWithPyramid; diff --git a/static/app/gettingStartedDocs/python/python.tsx b/static/app/gettingStartedDocs/python/python.tsx index dbfd0fbf90b035..4b9c23b9d9d7c9 100644 --- a/static/app/gettingStartedDocs/python/python.tsx +++ b/static/app/gettingStartedDocs/python/python.tsx @@ -6,15 +6,15 @@ import {t, tct} from 'sentry/locale'; // Configuration Start -const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% - # of sampled transactions. - # We recommend adjusting this value in production. - profiles_sample_rate=1.0,`; +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; -const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production. - traces_sample_rate=1.0,`; +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; export const steps = ({ sentryInitContent, @@ -87,7 +87,7 @@ export function GettingStartedWithPython({ }: ModuleProps) { const otherConfigs: string[] = []; - let sentryInitContent: string[] = [` dsn="${dsn}",`]; + let sentryInitContent: string[] = [` dsn="${dsn}",`]; if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { otherConfigs.push(performanceConfiguration); diff --git a/static/app/gettingStartedDocs/python/quart.spec.tsx b/static/app/gettingStartedDocs/python/quart.spec.tsx index a795acab8c67f3..773506f59d18c5 100644 --- a/static/app/gettingStartedDocs/python/quart.spec.tsx +++ b/static/app/gettingStartedDocs/python/quart.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithDjango', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/quart.tsx b/static/app/gettingStartedDocs/python/quart.tsx index 9295db403c08da..ff63823952b5d0 100644 --- a/static/app/gettingStartedDocs/python/quart.tsx +++ b/static/app/gettingStartedDocs/python/quart.tsx @@ -4,9 +4,21 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + const introduction = (

@@ -22,8 +34,10 @@ const introduction = ( ); export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description:

{tct('Install [code:sentry-sdk] from PyPI:', {code: })}

, @@ -48,14 +62,7 @@ from sentry_sdk.integrations.quart import QuartIntegration from quart import Quart sentry_sdk.init( - dsn="${dsn}", - integrations=[ - QuartIntegration(), - ], - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) app = Quart(__name__) @@ -66,8 +73,37 @@ app = Quart(__name__) ]; // Configuration End -export function GettingStartedWithQuart({dsn, ...props}: ModuleProps) { - return ; +export function GettingStartedWithQuart({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [ + ` dsn="${dsn}",`, + ` integrations=[QuartIntegration()],`, + ]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); } export default GettingStartedWithQuart; diff --git a/static/app/gettingStartedDocs/python/rq.spec.tsx b/static/app/gettingStartedDocs/python/rq.spec.tsx index 29dacd3d8acc45..4f713c4ef0517d 100644 --- a/static/app/gettingStartedDocs/python/rq.spec.tsx +++ b/static/app/gettingStartedDocs/python/rq.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithRq', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/rq.tsx b/static/app/gettingStartedDocs/python/rq.tsx index 53beac47fbf502..3917e1f684fb27 100644 --- a/static/app/gettingStartedDocs/python/rq.tsx +++ b/static/app/gettingStartedDocs/python/rq.tsx @@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + const introduction = (

{tct('The RQ integration adds support for the [link:RQ Job Queue System].', { @@ -14,8 +26,10 @@ const introduction = ( ); export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.CONFIGURE, description: ( @@ -33,15 +47,7 @@ import sentry_sdk from sentry_sdk.integrations.rq import RqIntegration sentry_sdk.init( - dsn="${dsn}", - integrations=[ - RqIntegration(), - ], - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) `, }, @@ -59,8 +65,37 @@ rq worker \ ]; // Configuration End -export function GettingStartedWithRq({dsn, ...props}: ModuleProps) { - return ; +export function GettingStartedWithRq({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [ + ` dsn="${dsn}",`, + ` integrations=[RqIntegration()],`, + ]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); } export default GettingStartedWithRq; diff --git a/static/app/gettingStartedDocs/python/sanic.spec.tsx b/static/app/gettingStartedDocs/python/sanic.spec.tsx index 26f65fd9ae9925..a3dfb9efa2af7d 100644 --- a/static/app/gettingStartedDocs/python/sanic.spec.tsx +++ b/static/app/gettingStartedDocs/python/sanic.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithSanic', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/sanic.tsx b/static/app/gettingStartedDocs/python/sanic.tsx index 632b287b39442a..0caab573f61115 100644 --- a/static/app/gettingStartedDocs/python/sanic.tsx +++ b/static/app/gettingStartedDocs/python/sanic.tsx @@ -6,9 +6,21 @@ import ListItem from 'sentry/components/list/listItem'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + const introduction = (

@@ -40,8 +52,10 @@ const introduction = ( ); export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description:

{tct('Install [code:sentry-sdk] from PyPI:', {code: })}

, @@ -80,15 +94,7 @@ from sentry_sdk.integrations.sanic import SanicIntegration from sanic import Sanic sentry_sdk.init( - dsn="${dsn}", - integrations=[ - SanicIntegration(), - ], - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) app = Sanic(__name__) @@ -99,8 +105,37 @@ app = Sanic(__name__) ]; // Configuration End -export function GettingStartedWithSanic({dsn, ...props}: ModuleProps) { - return ; +export function GettingStartedWithSanic({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [ + ` dsn="${dsn}",`, + ` integrations=[SanicIntegration()],`, + ]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); } export default GettingStartedWithSanic; diff --git a/static/app/gettingStartedDocs/python/serverless.spec.tsx b/static/app/gettingStartedDocs/python/serverless.spec.tsx index ce863883be3dfe..aacca3d8c2109e 100644 --- a/static/app/gettingStartedDocs/python/serverless.spec.tsx +++ b/static/app/gettingStartedDocs/python/serverless.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithServerless', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/serverless.tsx b/static/app/gettingStartedDocs/python/serverless.tsx index e8ee834185a065..93a9088b547c01 100644 --- a/static/app/gettingStartedDocs/python/serverless.tsx +++ b/static/app/gettingStartedDocs/python/serverless.tsx @@ -4,17 +4,26 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start -// It is recommended to use an integration for your particular serverless environment if available, as those are easier to use and capture more useful information. +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; -// If you use a serverless provider not directly supported by the SDK, you can use this generic integration. +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description: ( @@ -50,12 +59,7 @@ import sentry_sdk from sentry_sdk.integrations.serverless import serverless_function sentry_sdk.init( - dsn="${dsn}", - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) @serverless_function @@ -67,8 +71,33 @@ def my_function(...): ... ]; // Configuration End -export function GettingStartedWithServerless({dsn, ...props}: ModuleProps) { - return ; +export function GettingStartedWithServerless({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [` dsn="${dsn}",`]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); } export default GettingStartedWithServerless; diff --git a/static/app/gettingStartedDocs/python/starlette.spec.tsx b/static/app/gettingStartedDocs/python/starlette.spec.tsx index cb07fe3381e80f..8586e8d2a56e8a 100644 --- a/static/app/gettingStartedDocs/python/starlette.spec.tsx +++ b/static/app/gettingStartedDocs/python/starlette.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithDjango', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/starlette.tsx b/static/app/gettingStartedDocs/python/starlette.tsx index fbcfc06f7cee78..495e8d530a595d 100644 --- a/static/app/gettingStartedDocs/python/starlette.tsx +++ b/static/app/gettingStartedDocs/python/starlette.tsx @@ -2,18 +2,33 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + const introduction = tct( 'The Starlette integration adds support for the Starlette Framework.', { link: , } ); + export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description: ( @@ -54,12 +69,7 @@ import sentry_sdk sentry_sdk.init( - dsn="${dsn}", - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) app = Starlette(routes=[...]) @@ -91,10 +101,10 @@ from starlette.routing import Route async def trigger_error(request): - division_by_zero = 1 / 0 + division_by_zero = 1 / 0 app = Starlette(routes=[ - Route("/sentry-debug", trigger_error), + Route("/sentry-debug", trigger_error), ]) `, additionalInfo: t( @@ -106,8 +116,34 @@ app = Starlette(routes=[ ]; // Configuration End -export function GettingStartedWithStarlette({dsn, ...props}: ModuleProps) { - return ; +export function GettingStartedWithStarlette({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [` dsn="${dsn}",`]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); } export default GettingStartedWithStarlette; diff --git a/static/app/gettingStartedDocs/python/tornado.spec.tsx b/static/app/gettingStartedDocs/python/tornado.spec.tsx index 55c07396966c19..b0cf9ccd005179 100644 --- a/static/app/gettingStartedDocs/python/tornado.spec.tsx +++ b/static/app/gettingStartedDocs/python/tornado.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithTornado', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/tornado.tsx b/static/app/gettingStartedDocs/python/tornado.tsx index 3c8fa7779bce9d..2e056a3d36e4a7 100644 --- a/static/app/gettingStartedDocs/python/tornado.tsx +++ b/static/app/gettingStartedDocs/python/tornado.tsx @@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + const introduction = (

{tct( @@ -17,8 +29,10 @@ const introduction = ( ); export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description:

{tct('Install [code:sentry-sdk] from PyPI:', {code: })}

, @@ -54,15 +68,7 @@ import sentry_sdk from sentry_sdk.integrations.tornado import TornadoIntegration sentry_sdk.init( - dsn="${dsn}", - integrations=[ - TornadoIntegration(), - ], - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) # Your app code here, without changes @@ -76,8 +82,37 @@ class MyHandler(...): ]; // Configuration End -export function GettingStartedWithTornado({dsn, ...props}: ModuleProps) { - return ; +export function GettingStartedWithTornado({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [ + ` dsn="${dsn}",`, + ` integrations=[TornadoIntegration()],`, + ]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); } export default GettingStartedWithTornado; diff --git a/static/app/gettingStartedDocs/python/tryton.spec.tsx b/static/app/gettingStartedDocs/python/tryton.spec.tsx index a8274467ffcc9d..6c376929b18a95 100644 --- a/static/app/gettingStartedDocs/python/tryton.spec.tsx +++ b/static/app/gettingStartedDocs/python/tryton.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithTryton', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/tryton.tsx b/static/app/gettingStartedDocs/python/tryton.tsx index b090a14bdb4b86..ddcaef21fc7cc8 100644 --- a/static/app/gettingStartedDocs/python/tryton.tsx +++ b/static/app/gettingStartedDocs/python/tryton.tsx @@ -2,9 +2,21 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + const introduction = (

{tct('The Tryton integration adds support for the [link:Tryton Framework Server].', { @@ -14,8 +26,10 @@ const introduction = ( ); export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.CONFIGURE, description: ( @@ -37,15 +51,7 @@ import sentry_sdk import sentry_sdk.integrations.trytond sentry_sdk.init( - dsn="${dsn}", - integrations=[ - sentry_sdk.integrations.trytond.TrytondWSGIIntegration(), - ], - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) from trytond.application import app as application @@ -67,12 +73,12 @@ from trytond.exceptions import UserError @application.error_handler def _(app, request, e): - if isinstance(e, TrytonException): - return - else: - event_id = sentry_sdk.last_event_id() - data = UserError('Custom message', f'{event_id}{e}') - return app.make_response(request, data) + if isinstance(e, TrytonException): + return + else: + event_id = sentry_sdk.last_event_id() + data = UserError('Custom message', f'{event_id}{e}') + return app.make_response(request, data) `, }, ], @@ -80,8 +86,39 @@ def _(app, request, e): ]; // Configuration End -export function GettingStartedWithTryton({dsn, ...props}: ModuleProps) { - return ; +export function GettingStartedWithTryton({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [ + ` dsn="${dsn}",`, + ` integrations=[`, + ` sentry_sdk.integrations.trytond.TrytondWSGIIntegration(),`, + ` ],`, + ]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); } export default GettingStartedWithTryton; diff --git a/static/app/gettingStartedDocs/python/wsgi.spec.tsx b/static/app/gettingStartedDocs/python/wsgi.spec.tsx index c21523a9f12477..e2a8a3f2e09ada 100644 --- a/static/app/gettingStartedDocs/python/wsgi.spec.tsx +++ b/static/app/gettingStartedDocs/python/wsgi.spec.tsx @@ -9,7 +9,7 @@ describe('GettingStartedWithWSGI', function () { const {container} = render(); // Steps - for (const step of steps()) { + for (const step of steps({sentryInitContent: 'test-init-content'})) { expect( screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) ).toBeInTheDocument(); diff --git a/static/app/gettingStartedDocs/python/wsgi.tsx b/static/app/gettingStartedDocs/python/wsgi.tsx index 7fe3b4a7f000c4..825413290180ba 100644 --- a/static/app/gettingStartedDocs/python/wsgi.tsx +++ b/static/app/gettingStartedDocs/python/wsgi.tsx @@ -4,12 +4,26 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; // Configuration Start + +const profilingConfiguration = ` # Set profiles_sample_rate to 1.0 to profile 100% + # of sampled transactions. + # We recommend adjusting this value in production. + profiles_sample_rate=1.0,`; + +const performanceConfiguration = ` # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0,`; + export const steps = ({ - dsn, -}: Partial> = {}): LayoutProps['steps'] => [ + sentryInitContent, +}: { + sentryInitContent: string; +}): LayoutProps['steps'] => [ { type: StepType.INSTALL, description: ( @@ -39,12 +53,7 @@ from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from myapp import wsgi_app sentry_sdk.init( - dsn="${dsn}", - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production, - traces_sample_rate=1.0, +${sentryInitContent} ) wsgi_app = SentryWsgiMiddleware(wsgi_app) @@ -55,8 +64,32 @@ wsgi_app = SentryWsgiMiddleware(wsgi_app) ]; // Configuration End -export function GettingStartedWithWSGI({dsn, ...props}: ModuleProps) { - return ; -} +export function GettingStartedWithWSGI({ + dsn, + activeProductSelection = [], + ...props +}: ModuleProps) { + const otherConfigs: string[] = []; + + let sentryInitContent: string[] = [` dsn="${dsn}",`]; + + if (activeProductSelection.includes(ProductSolution.PERFORMANCE_MONITORING)) { + otherConfigs.push(performanceConfiguration); + } + if (activeProductSelection.includes(ProductSolution.PROFILING)) { + otherConfigs.push(profilingConfiguration); + } + + sentryInitContent = sentryInitContent.concat(otherConfigs); + + return ( + + ); +} export default GettingStartedWithWSGI; diff --git a/static/app/types/group.tsx b/static/app/types/group.tsx index 46285ee81f393d..6ee6a316d5e81f 100644 --- a/static/app/types/group.tsx +++ b/static/app/types/group.tsx @@ -72,7 +72,7 @@ export enum IssueType { PERFORMANCE_UNCOMPRESSED_ASSET = 'performance_uncompressed_assets', PERFORMANCE_LARGE_HTTP_PAYLOAD = 'performance_large_http_payload', PERFORMANCE_HTTP_OVERHEAD = 'performance_http_overhead', - PERFORMANCE_P95_TRANSACTION_DURATION_REGRESSION = 'performance_p95_transaction_duration_regression', + PERFORMANCE_DURATION_REGRESSION = 'performance_duration_regression', // Profile PROFILE_FILE_IO_MAIN_THREAD = 'profile_file_io_main_thread', diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index be34c2885da5e1..e3063ace248d29 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -50,7 +50,7 @@ import { } from 'sentry/views/performance/transactionSummary/filter'; import {PercentChangeCell} from 'sentry/views/starfish/components/tableCells/percentChangeCell'; import {TimeSpentCell} from 'sentry/views/starfish/components/tableCells/timeSpentCell'; -import {SpanMetricsFields} from 'sentry/views/starfish/types'; +import {SpanMetricsField} from 'sentry/views/starfish/types'; import {decodeScalar} from '../queryString'; @@ -769,7 +769,7 @@ const SPECIAL_FUNCTIONS: SpecialFunctions = { return ( ); }, diff --git a/static/app/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext.tsx b/static/app/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext.tsx index 129b61c744e059..274f47954d7de9 100644 --- a/static/app/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext.tsx +++ b/static/app/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext.tsx @@ -19,6 +19,7 @@ export const DEFAULT_FLAMEGRAPH_STATE: FlamegraphState = { }, preferences: { timelines: { + battery_chart: true, ui_frames: true, minimap: true, transaction_spans: true, diff --git a/static/app/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphPreferences.tsx b/static/app/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphPreferences.tsx index b94da8e65aa8a6..533df808e6a763 100644 --- a/static/app/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphPreferences.tsx +++ b/static/app/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphPreferences.tsx @@ -18,6 +18,7 @@ export interface FlamegraphPreferences { layout: 'table right' | 'table bottom' | 'table left'; sorting: FlamegraphSorting; timelines: { + battery_chart: boolean; cpu_chart: boolean; memory_chart: boolean; minimap: boolean; diff --git a/static/app/utils/profiling/flamegraph/flamegraphTheme.tsx b/static/app/utils/profiling/flamegraph/flamegraphTheme.tsx index 09a71bd91455c4..b8c920250d8acc 100644 --- a/static/app/utils/profiling/flamegraph/flamegraphTheme.tsx +++ b/static/app/utils/profiling/flamegraph/flamegraphTheme.tsx @@ -46,6 +46,7 @@ export interface FlamegraphTheme { // They should instead be defined as arrays of numbers so we can use them with glsl and avoid unnecessary parsing COLORS: { BAR_LABEL_FONT_COLOR: string; + BATTERY_CHART_COLORS: ColorChannels[]; CHART_CURSOR_INDICATOR: string; CHART_LABEL_COLOR: string; COLOR_BUCKET: (t: number) => ColorChannels; @@ -96,6 +97,7 @@ export interface FlamegraphTheme { BAR_FONT_SIZE: number; BAR_HEIGHT: number; BAR_PADDING: number; + BATTERY_CHART_HEIGHT: number; CHART_PX_PADDING: number; CPU_CHART_HEIGHT: number; FLAMEGRAPH_DEPTH_OFFSET: number; @@ -154,6 +156,7 @@ const SIZES: FlamegraphTheme['SIZES'] = { BAR_FONT_SIZE: 11, BAR_HEIGHT: 20, BAR_PADDING: 4, + BATTERY_CHART_HEIGHT: 80, FLAMEGRAPH_DEPTH_OFFSET: 12, HOVERED_FRAME_BORDER_WIDTH: 2, HIGHLIGHTED_FRAME_BORDER_WIDTH: 3, @@ -186,6 +189,7 @@ export const LightFlamegraphTheme: FlamegraphTheme = { SIZES, COLORS: { BAR_LABEL_FONT_COLOR: '#000', + BATTERY_CHART_COLORS: [[0.4, 0.56, 0.9, 0.65]], COLOR_BUCKET: makeColorBucketTheme(LCH_LIGHT), SPAN_COLOR_BUCKET: makeColorBucketTheme(SPAN_LCH_LIGHT, 140, 220), COLOR_MAPS: { @@ -239,6 +243,7 @@ export const DarkFlamegraphTheme: FlamegraphTheme = { SIZES, COLORS: { BAR_LABEL_FONT_COLOR: 'rgb(255 255 255 / 80%)', + BATTERY_CHART_COLORS: [[0.4, 0.56, 0.9, 0.5]], COLOR_BUCKET: makeColorBucketTheme(LCH_DARK), SPAN_COLOR_BUCKET: makeColorBucketTheme(SPANS_LCH_DARK, 140, 220), COLOR_MAPS: { @@ -252,8 +257,8 @@ export const DarkFlamegraphTheme: FlamegraphTheme = { }, CPU_CHART_COLORS: CHART_PALETTE[12].map(c => hexToColorChannels(c, 0.8)), MEMORY_CHART_COLORS: [ - hexToColorChannels(CHART_PALETTE[4][2], 0.8), - hexToColorChannels(CHART_PALETTE[4][3], 0.8), + hexToColorChannels(CHART_PALETTE[4][2], 0.5), + hexToColorChannels(CHART_PALETTE[4][3], 0.5), ], CHART_CURSOR_INDICATOR: 'rgba(255, 255, 255, 0.5)', CHART_LABEL_COLOR: 'rgba(255, 255, 255, 0.5)', @@ -262,8 +267,8 @@ export const DarkFlamegraphTheme: FlamegraphTheme = { DIFFERENTIAL_INCREASE: [0.98, 0.2058, 0.4381], FOCUSED_FRAME_BORDER_COLOR: darkTheme.focus, FRAME_GRAYSCALE_COLOR: [0.5, 0.5, 0.5, 0.4], - FRAME_APPLICATION_COLOR: [0.1, 0.1, 0.8, 0.4], - FRAME_SYSTEM_COLOR: [0.7, 0.1, 0.1, 0.5], + FRAME_APPLICATION_COLOR: [0.1, 0.1, 0.5, 0.4], + FRAME_SYSTEM_COLOR: [0.6, 0.15, 0.25, 0.3], SPAN_FALLBACK_COLOR: [1, 1, 1, 0.3], GRID_FRAME_BACKGROUND_COLOR: 'rgb(26, 20, 31,1)', GRID_LINE_COLOR: '#222227', diff --git a/static/app/utils/replays/replayReader.spec.tsx b/static/app/utils/replays/replayReader.spec.tsx index ac243a125e832e..bbcde5bf6e547e 100644 --- a/static/app/utils/replays/replayReader.spec.tsx +++ b/static/app/utils/replays/replayReader.spec.tsx @@ -91,6 +91,14 @@ describe('ReplayReader', () => { startTimestamp: new Date('2023-12-25T00:03:00'), endTimestamp: new Date('2023-12-25T00:03:30'), }); + const navCrumb = TestStubs.Replay.BreadcrumbFrameEvent({ + timestamp: new Date('2023-12-25T00:03:00'), + data: { + payload: TestStubs.Replay.NavFrame({ + timestamp: new Date('2023-12-25T00:03:00'), + }), + }, + }); const consoleEvent = TestStubs.Replay.ConsoleEvent({timestamp}); const customEvent = TestStubs.Replay.BreadcrumbFrameEvent({ timestamp: new Date('2023-12-25T00:02:30'), @@ -114,6 +122,7 @@ describe('ReplayReader', () => { firstDiv, firstMemory, navigationEvent, + navCrumb, optionsEvent, secondDiv, secondMemory, @@ -170,6 +179,7 @@ describe('ReplayReader', () => { expected: [ expect.objectContaining({category: 'replay.init'}), expect.objectContaining({category: 'ui.slowClickDetected'}), + expect.objectContaining({category: 'navigation'}), expect.objectContaining({op: 'navigation.navigate'}), expect.objectContaining({category: 'ui.click'}), expect.objectContaining({category: 'ui.click'}), diff --git a/static/app/utils/replays/replayReader.tsx b/static/app/utils/replays/replayReader.tsx index 3aa0a1faa9faf4..7f9768324ee176 100644 --- a/static/app/utils/replays/replayReader.tsx +++ b/static/app/utils/replays/replayReader.tsx @@ -29,6 +29,8 @@ import { BreadcrumbCategories, isDeadClick, isDeadRageClick, + isLCPFrame, + isPaintFrame, } from 'sentry/utils/replays/types'; import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types'; @@ -245,27 +247,34 @@ export default class ReplayReader { ); getChapterFrames = memoize(() => + [ + ...this.getPerfFrames(), + ...this._sortedBreadcrumbFrames.filter(frame => + ['replay.init', 'replay.mutations'].includes(frame.category) + ), + ...this._errors, + ].sort(sortFrames) + ); + + getPerfFrames = memoize(() => [ ...removeDuplicateClicks( this._sortedBreadcrumbFrames.filter( frame => - ['navigation', 'replay.init', 'replay.mutations', 'ui.click'].includes( - frame.category - ) || + ['navigation', 'ui.click'].includes(frame.category) || (frame.category === 'ui.slowClickDetected' && (isDeadClick(frame as SlowClickFrame) || isDeadRageClick(frame as SlowClickFrame))) ) ), - ...this._sortedSpanFrames.filter(frame => - ['navigation.navigate', 'navigation.reload', 'navigation.back_forward'].includes( - frame.op - ) - ), - ...this._errors, + ...this._sortedSpanFrames.filter(frame => frame.op.startsWith('navigation.')), ].sort(sortFrames) ); + getLPCFrames = memoize(() => this._sortedSpanFrames.filter(isLCPFrame)); + + getPaintFrames = memoize(() => this._sortedSpanFrames.filter(isPaintFrame)); + getSDKOptions = () => this._optionFrame; isNetworkDetailsSetup = memoize(() => { diff --git a/static/app/utils/replays/types.tsx b/static/app/utils/replays/types.tsx index eeaa2451647dba..9fd38b6d1e0084 100644 --- a/static/app/utils/replays/types.tsx +++ b/static/app/utils/replays/types.tsx @@ -89,6 +89,14 @@ export function isConsoleFrame(frame: BreadcrumbFrame): frame is ConsoleFrame { return false; } +export function isLCPFrame(frame: SpanFrame): frame is LargestContentfulPaintFrame { + return frame.op === 'largest-contentful-paint'; +} + +export function isPaintFrame(frame: SpanFrame): frame is PaintFrame { + return frame.op === 'paint'; +} + export function isDeadClick(frame: SlowClickFrame) { return frame.data.endReason === 'timeout'; } diff --git a/static/app/views/dashboards/exportDashboard.tsx b/static/app/views/dashboards/exportDashboard.tsx index 59b9dea6880f90..9ba1064f5b44cd 100644 --- a/static/app/views/dashboards/exportDashboard.tsx +++ b/static/app/views/dashboards/exportDashboard.tsx @@ -17,10 +17,11 @@ async function exportDashboard() { const structure = { base_url: null, dashboard_id: null, + org_slug: null, }; const params = getAPIParams(structure); - const apiUrl = `https://${params.base_url}/api/0/organizations/testorg-az/dashboards/${params.dashboard_id}/`; + const apiUrl = `https://${params.base_url}/api/0/organizations/${params.org_slug}/dashboards/${params.dashboard_id}/`; const response = await fetch(apiUrl); const jsonData = await response.json(); const normalized = normalizeData(jsonData); @@ -39,6 +40,7 @@ function getAPIParams(structure) { const regex = { base_url: /(\/\/)(.*?)(\/)/, dashboard_id: /(dashboard\/)(.*?)(\/)/, + org_slug: /(\/\/)(.+?)(?=\.)/, }; for (const attr in regex) { @@ -122,6 +124,7 @@ function getPropertyStructure(property) { queries: [], displayType: '', widgetType: '', + layout: [], }; break; case 'queries': diff --git a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx index 8557d6e6e8412c..73fc9df82de29c 100644 --- a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx +++ b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx @@ -14,6 +14,7 @@ import {EventEvidence} from 'sentry/components/events/eventEvidence'; import {EventExtraData} from 'sentry/components/events/eventExtraData'; import EventReplay from 'sentry/components/events/eventReplay'; import {EventSdk} from 'sentry/components/events/eventSdk'; +import EventBreakpointChart from 'sentry/components/events/eventStatisticalDetector/breakpointChart'; import RegressionMessage from 'sentry/components/events/eventStatisticalDetector/regressionMessage'; import {EventTagsAndScreenshot} from 'sentry/components/events/eventTagsAndScreenshot'; import {EventViewHierarchy} from 'sentry/components/events/eventViewHierarchy'; @@ -85,11 +86,17 @@ function GroupEventDetailsContent({ const eventEntryProps = {group, event, project}; - if (group.issueType === IssueType.PERFORMANCE_P95_TRANSACTION_DURATION_REGRESSION) { + if (group.issueType === IssueType.PERFORMANCE_DURATION_REGRESSION) { return ( - // TODO: Swap this feature flag with the statistical detector flag - - + + + + + ); } diff --git a/static/app/views/onboarding/setupDocs.spec.tsx b/static/app/views/onboarding/setupDocs.spec.tsx index d7c4d5cddb1659..066b2a4f87889c 100644 --- a/static/app/views/onboarding/setupDocs.spec.tsx +++ b/static/app/views/onboarding/setupDocs.spec.tsx @@ -411,7 +411,7 @@ describe('Onboarding Setup Docs', function () { ); expect( - await screen.findByRole('heading', {name: 'Configure JavaScript SDK'}) + await screen.findByRole('heading', {name: 'Configure Browser JavaScript SDK'}) ).toBeInTheDocument(); expect(updateLoaderMock).toHaveBeenCalledTimes(1); diff --git a/static/app/views/performance/database/databaseLandingPage.tsx b/static/app/views/performance/database/databaseLandingPage.tsx index 1971bd415afb39..6267d602b0432c 100644 --- a/static/app/views/performance/database/databaseLandingPage.tsx +++ b/static/app/views/performance/database/databaseLandingPage.tsx @@ -11,7 +11,7 @@ import {space} from 'sentry/styles/space'; import useOrganization from 'sentry/utils/useOrganization'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import {ModulePageProviders} from 'sentry/views/performance/database/modulePageProviders'; -import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types'; +import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; import {ActionSelector} from 'sentry/views/starfish/views/spans/selectors/actionSelector'; import {DomainSelector} from 'sentry/views/starfish/views/spans/selectors/domainSelector'; import SpansTable from 'sentry/views/starfish/views/spans/spansTable'; @@ -64,12 +64,12 @@ function DatabaseLandingPage() { diff --git a/static/app/views/performance/database/databaseSpanSummaryPage.tsx b/static/app/views/performance/database/databaseSpanSummaryPage.tsx index efe8f76d8e51bd..32c8fa3379e980 100644 --- a/static/app/views/performance/database/databaseSpanSummaryPage.tsx +++ b/static/app/views/performance/database/databaseSpanSummaryPage.tsx @@ -26,7 +26,7 @@ import { useSpanMetrics, } from 'sentry/views/starfish/queries/useSpanMetrics'; import {useSpanMetricsSeries} from 'sentry/views/starfish/queries/useSpanMetricsSeries'; -import {SpanMetricsFields, StarfishFunctions} from 'sentry/views/starfish/types'; +import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types'; import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters'; import { getDurationChartTitle, @@ -43,7 +43,7 @@ type Query = { endpointMethod: string; transaction: string; transactionMethod: string; - [QueryParameterNames.SORT]: string; + [QueryParameterNames.SPANS_SORT]: string; }; type Props = { @@ -69,43 +69,43 @@ function SpanSummaryPage({params}: Props) { groupId, queryFilter, [ - SpanMetricsFields.SPAN_OP, - SpanMetricsFields.SPAN_DESCRIPTION, - SpanMetricsFields.SPAN_ACTION, - SpanMetricsFields.SPAN_DOMAIN, + SpanMetricsField.SPAN_OP, + SpanMetricsField.SPAN_DESCRIPTION, + SpanMetricsField.SPAN_ACTION, + SpanMetricsField.SPAN_DOMAIN, 'count()', - `${StarfishFunctions.SPM}()`, - `sum(${SpanMetricsFields.SPAN_SELF_TIME})`, - `avg(${SpanMetricsFields.SPAN_SELF_TIME})`, - `${StarfishFunctions.TIME_SPENT_PERCENTAGE}()`, - `${StarfishFunctions.HTTP_ERROR_COUNT}()`, + `${SpanFunction.SPM}()`, + `sum(${SpanMetricsField.SPAN_SELF_TIME})`, + `avg(${SpanMetricsField.SPAN_SELF_TIME})`, + `${SpanFunction.TIME_SPENT_PERCENTAGE}()`, + `${SpanFunction.HTTP_ERROR_COUNT}()`, ], 'api.starfish.span-summary-page-metrics' ); const span = { ...spanMetrics, - [SpanMetricsFields.SPAN_GROUP]: groupId, + [SpanMetricsField.SPAN_GROUP]: groupId, } as { - [SpanMetricsFields.SPAN_OP]: string; - [SpanMetricsFields.SPAN_DESCRIPTION]: string; - [SpanMetricsFields.SPAN_ACTION]: string; - [SpanMetricsFields.SPAN_DOMAIN]: string; - [SpanMetricsFields.SPAN_GROUP]: string; + [SpanMetricsField.SPAN_OP]: string; + [SpanMetricsField.SPAN_DESCRIPTION]: string; + [SpanMetricsField.SPAN_ACTION]: string; + [SpanMetricsField.SPAN_DOMAIN]: string; + [SpanMetricsField.SPAN_GROUP]: string; }; const {isLoading: areSpanMetricsSeriesLoading, data: spanMetricsSeriesData} = useSpanMetricsSeries( groupId, queryFilter, - [`avg(${SpanMetricsFields.SPAN_SELF_TIME})`, 'spm()', 'http_error_count()'], + [`avg(${SpanMetricsField.SPAN_SELF_TIME})`, 'spm()', 'http_error_count()'], 'api.starfish.span-summary-page-metrics-chart' ); useSynchronizeCharts([!areSpanMetricsSeriesLoading]); const spanMetricsThroughputSeries = { - seriesName: span?.[SpanMetricsFields.SPAN_OP]?.startsWith('db') + seriesName: span?.[SpanMetricsField.SPAN_OP]?.startsWith('db') ? 'Queries' : 'Requests', data: spanMetricsSeriesData?.['spm()'].data, @@ -155,14 +155,14 @@ function SpanSummaryPage({params}: Props) { - {span?.[SpanMetricsFields.SPAN_DESCRIPTION] && ( + {span?.[SpanMetricsField.SPAN_DESCRIPTION] && ( @@ -171,7 +171,7 @@ function SpanSummaryPage({params}: Props) { - + diff --git a/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx b/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx index c96860c9aa9810..06271e5b41a59f 100644 --- a/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx +++ b/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx @@ -292,4 +292,4 @@ export function TrendsWidget(props: PerformanceWidgetProps) { ); } -const TrendsChart = withProjects(Chart); +export const TrendsChart = withProjects(Chart); diff --git a/static/app/views/projectInstall/platform.spec.tsx b/static/app/views/projectInstall/platform.spec.tsx index 8899268495a3ef..00734c29520b6a 100644 --- a/static/app/views/projectInstall/platform.spec.tsx +++ b/static/app/views/projectInstall/platform.spec.tsx @@ -126,7 +126,7 @@ describe('ProjectInstallPlatform', function () { expect( await screen.findByRole('heading', { - name: 'Configure JavaScript SDK', + name: 'Configure Browser JavaScript SDK', }) ).toBeInTheDocument(); }); diff --git a/static/app/views/replays/detail/console/consoleLogRow.tsx b/static/app/views/replays/detail/console/consoleLogRow.tsx index 7cdf040914d17f..6614c5e44345d0 100644 --- a/static/app/views/replays/detail/console/consoleLogRow.tsx +++ b/static/app/views/replays/detail/console/consoleLogRow.tsx @@ -11,7 +11,7 @@ import type useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers'; import type {BreadcrumbFrame, ConsoleFrame} from 'sentry/utils/replays/types'; import MessageFormatter from 'sentry/views/replays/detail/console/messageFormatter'; import TimestampButton from 'sentry/views/replays/detail/timestampButton'; -import type {OnDimensionChange} from 'sentry/views/replays/detail/useVirtualListDimentionChange'; +import type {OnDimensionChange} from 'sentry/views/replays/detail/useVirtualizedInspector'; interface Props extends ReturnType { currentHoverTime: number | undefined; diff --git a/static/app/views/replays/detail/layout/index.tsx b/static/app/views/replays/detail/layout/index.tsx index 0c145775eba484..e548c19233b851 100644 --- a/static/app/views/replays/detail/layout/index.tsx +++ b/static/app/views/replays/detail/layout/index.tsx @@ -77,7 +77,7 @@ function ReplayLayout({layout = LayoutKey.TOPBAR}: Props) { ); - const hasSize = width + height; + const hasSize = width + height > 0; if (layout === LayoutKey.NO_VIDEO) { return ( diff --git a/static/app/views/replays/detail/perfTable/grabber.tsx b/static/app/views/replays/detail/perfTable/grabber.tsx new file mode 100644 index 00000000000000..3fea352f67f7d6 --- /dev/null +++ b/static/app/views/replays/detail/perfTable/grabber.tsx @@ -0,0 +1,51 @@ +import type {MouseEventHandler} from 'react'; +import styled from '@emotion/styled'; + +interface Props { + 'data-is-held': boolean; + 'data-slide-direction': 'leftright' | 'updown'; + onDoubleClick: MouseEventHandler; + onMouseDown: MouseEventHandler; +} + +const Grabber = styled('div')` + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 6px; + transform: translate(-3px, 0); + z-index: ${p => p.theme.zIndex.initial}; + + user-select: inherit; + &[data-is-held='true'] { + user-select: none; + } + + &[data-slide-direction='leftright'] { + cursor: ew-resize; + } + &[data-slide-direction='updown'] { + cursor: ns-resize; + } + + &:after { + content: ''; + position: absolute; + top: 0; + left: 2.5px; + height: 100%; + width: 1px; + transform: translate(-0.5px, 0); + z-index: ${p => p.theme.zIndex.initial}; + background: ${p => p.theme.border}; + } + &:hover:after, + &[data-is-held='true']:after { + left: 1.5px; + width: 3px; + background: ${p => p.theme.black}; + } +`; + +export default Grabber; diff --git a/static/app/views/replays/detail/perfTable/index.tsx b/static/app/views/replays/detail/perfTable/index.tsx index 8c0d6ee832766a..14d2a9104ddda0 100644 --- a/static/app/views/replays/detail/perfTable/index.tsx +++ b/static/app/views/replays/detail/perfTable/index.tsx @@ -1,15 +1,14 @@ -// import {useReplayContext} from 'sentry/components/replays/replayContext'; -// import PerfTable from 'sentry/views/replays/detail/perfTable/perfTable'; -// import useReplayPerfData from 'sentry/views/replays/detail/perfTable/useReplayPerfData'; +import {useReplayContext} from 'sentry/components/replays/replayContext'; +import PerfTable from 'sentry/views/replays/detail/perfTable/perfTable'; +import useReplayPerfData from 'sentry/views/replays/detail/perfTable/useReplayPerfData'; type Props = {}; function Perf({}: Props) { - // const {replay} = useReplayContext(); - // const perfData = useReplayPerfData({replay}); + const {replay} = useReplayContext(); + const perfData = useReplayPerfData({replay}); - // return ; - return

; + return ; } export default Perf; diff --git a/static/app/views/replays/detail/perfTable/perfFilters.tsx b/static/app/views/replays/detail/perfTable/perfFilters.tsx new file mode 100644 index 00000000000000..642f7ab20b56f5 --- /dev/null +++ b/static/app/views/replays/detail/perfTable/perfFilters.tsx @@ -0,0 +1,34 @@ +import type {SelectOption} from 'sentry/components/compactSelect'; +import {CompactSelect} from 'sentry/components/compactSelect'; +import {t} from 'sentry/locale'; +import FiltersGrid from 'sentry/views/replays/detail/filtersGrid'; +import usePerfFilters from 'sentry/views/replays/detail/perfTable/usePerfFilters'; + +type Props = { + traceRows: undefined | unknown[]; +} & ReturnType; + +function PerfFilters({getCrumbTypes, selectValue, setFilters}: Props) { + const crumbTypes = getCrumbTypes(); + return ( + + []) => void} + options={[ + { + label: t('Type'), + options: crumbTypes, + }, + ]} + size="sm" + triggerLabel={selectValue?.length === 0 ? t('Any') : null} + triggerProps={{prefix: t('Filter')}} + value={selectValue} + /> + + ); +} + +export default PerfFilters; diff --git a/static/app/views/replays/detail/perfTable/perfRow.tsx b/static/app/views/replays/detail/perfTable/perfRow.tsx new file mode 100644 index 00000000000000..a27ae46436d84e --- /dev/null +++ b/static/app/views/replays/detail/perfTable/perfRow.tsx @@ -0,0 +1,162 @@ +import {CSSProperties, Fragment, useCallback} from 'react'; +import styled from '@emotion/styled'; +import classNames from 'classnames'; + +import BreadcrumbIcon from 'sentry/components/events/interfaces/breadcrumbs/breadcrumb/type/icon'; +import {Tooltip} from 'sentry/components/tooltip'; +import {IconClock, IconRefresh} from 'sentry/icons'; +import {tct} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import getFrameDetails from 'sentry/utils/replays/getFrameDetails'; +import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers'; +import IconWrapper from 'sentry/views/replays/detail/iconWrapper'; +import TraceGrid from 'sentry/views/replays/detail/perfTable/traceGrid'; +import type {ReplayTraceRow} from 'sentry/views/replays/detail/perfTable/useReplayPerfData'; +import TimestampButton from 'sentry/views/replays/detail/timestampButton'; +import {OnDimensionChange} from 'sentry/views/replays/detail/useVirtualListDimentionChange'; + +interface Props { + currentHoverTime: number | undefined; + currentTime: number; + index: number; + onDimensionChange: OnDimensionChange; + startTimestampMs: number; + style: CSSProperties; + traceRow: ReplayTraceRow; +} + +export default function PerfRow({ + currentHoverTime, + currentTime, + index, + onDimensionChange, + startTimestampMs, + style, + traceRow, +}: Props) { + const {lcpFrames, replayFrame: frame, paintFrames, flattenedTraces} = traceRow; + const {color, description, title, type} = getFrameDetails(frame); + const lcp = lcpFrames.length ? getFrameDetails(lcpFrames[0]) : null; + + const handleDimensionChange = useCallback( + () => onDimensionChange(index), + [onDimensionChange, index] + ); + + const {onMouseEnter, onMouseLeave, onClickTimestamp} = useCrumbHandlers(); + + const handleClickTimestamp = useCallback( + () => onClickTimestamp(frame), + [onClickTimestamp, frame] + ); + const handleMouseEnter = useCallback(() => onMouseEnter(frame), [onMouseEnter, frame]); + const handleMouseLeave = useCallback(() => onMouseLeave(frame), [onMouseLeave, frame]); + + const hasOccurred = frame ? currentTime >= frame.offsetMs : false; + const isBeforeHover = frame + ? currentHoverTime === undefined || currentHoverTime >= frame.offsetMs + : false; + + return ( + + + + + + + + {title} + + {description} + + + + {lcp ? ( + + + {tct('[lcp] LCP', {lcp: lcp.description})} + + ) : null} + + + {paintFrames.length ? ( + + + {tct('[count] paint events', {count: paintFrames.length})} + + ) : null} + + + + {flattenedTraces.map((flatTrace, i) => ( + + ))} + + + ); +} + +const PerfListItem = styled('div')` + padding: ${space(1)} ${space(1.5)}; + + /* Overridden in TabItemContainer, depending on *CurrentTime and *HoverTime classes */ + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; +`; + +const Vertical = styled('div')` + display: flex; + flex-direction: column; + overflow-x: hidden; +`; + +const Horizontal = styled('div')` + display: flex; + flex: auto 1 1; + flex-direction: row; + + font-size: ${p => p.theme.fontSizeSmall}; + overflow: auto; +`; + +const Title = styled('span')<{hasOccurred?: boolean}>` + color: ${p => (p.hasOccurred ? p.theme.gray400 : p.theme.gray300)}; + font-size: ${p => p.theme.fontSizeMedium}; + font-weight: bold; + line-height: ${p => p.theme.text.lineHeightBody}; + text-transform: capitalize; + ${p => p.theme.overflowEllipsis}; +`; + +const Description = styled(Tooltip)` + ${p => p.theme.overflowEllipsis}; + font-size: 0.7rem; + font-variant-numeric: tabular-nums; + line-height: ${p => p.theme.text.lineHeightBody}; + color: ${p => p.theme.subText}; +`; + +const IconLabel = styled('span')` + display: flex; + align-items: center; + align-self: baseline; + gap: 4px; +`; diff --git a/static/app/views/replays/detail/perfTable/perfTable.tsx b/static/app/views/replays/detail/perfTable/perfTable.tsx new file mode 100644 index 00000000000000..59cb7ce19817d7 --- /dev/null +++ b/static/app/views/replays/detail/perfTable/perfTable.tsx @@ -0,0 +1,111 @@ +import {useMemo, useRef} from 'react'; +import { + AutoSizer, + CellMeasurer, + List as ReactVirtualizedList, + ListRowProps, +} from 'react-virtualized'; + +import Placeholder from 'sentry/components/placeholder'; +import {useReplayContext} from 'sentry/components/replays/replayContext'; +import {t} from 'sentry/locale'; +import FilterLoadingIndicator from 'sentry/views/replays/detail/filterLoadingIndicator'; +import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight'; +import NoRowRenderer from 'sentry/views/replays/detail/noRowRenderer'; +import PerfFilters from 'sentry/views/replays/detail/perfTable/perfFilters'; +import PerfRow from 'sentry/views/replays/detail/perfTable/perfRow'; +import usePerfFilters from 'sentry/views/replays/detail/perfTable/usePerfFilters'; +import type useReplayPerfData from 'sentry/views/replays/detail/perfTable/useReplayPerfData'; +import TabItemContainer from 'sentry/views/replays/detail/tabItemContainer'; +import useVirtualizedList from 'sentry/views/replays/detail/useVirtualizedList'; +import useVirtualListDimentionChange from 'sentry/views/replays/detail/useVirtualListDimentionChange'; + +interface Props { + perfData: ReturnType; +} + +const cellMeasurer = { + fixedWidth: true, + minHeight: 24, +}; + +export default function PerfTable({perfData}: Props) { + const {currentTime, currentHoverTime, replay} = useReplayContext(); + const startTimestampMs = replay?.getReplay().started_at.getTime() ?? 0; + + const traceRows = perfData.data; + + const filterProps = usePerfFilters({traceRows: traceRows || []}); + const {items} = filterProps; // setSearchTerm + const clearSearchTerm = () => {}; // setSearchTerm(''); + + const listRef = useRef(null); + const deps = useMemo(() => [items], [items]); + const {cache, updateList} = useVirtualizedList({ + cellMeasurer, + ref: listRef, + deps, + }); + + const {handleDimensionChange} = useVirtualListDimentionChange({cache, listRef}); + + const renderRow = ({index, key, style, parent}: ListRowProps) => { + const traceRow = items[index]; + + return ( + + + + ); + }; + + return ( + + + + + + {traceRows ? ( + + {({width, height}) => ( + ( + + {t('No events recorded')} + + )} + overscanRowCount={5} + ref={listRef} + rowCount={items.length} + rowHeight={cache.rowHeight} + rowRenderer={renderRow} + width={width} + /> + )} + + ) : ( + + )} + + + ); +} diff --git a/static/app/views/replays/detail/perfTable/resizeableContainer.tsx b/static/app/views/replays/detail/perfTable/resizeableContainer.tsx new file mode 100644 index 00000000000000..80b8c06779754b --- /dev/null +++ b/static/app/views/replays/detail/perfTable/resizeableContainer.tsx @@ -0,0 +1,47 @@ +import type {ReactNode} from 'react'; +import {Fragment} from 'react'; +import styled from '@emotion/styled'; + +import toPixels from 'sentry/utils/number/toPixels'; +import {useResizableDrawer} from 'sentry/utils/useResizableDrawer'; +import Grabber from 'sentry/views/replays/detail/perfTable/grabber'; + +interface Props { + children: [ReactNode, ReactNode]; + containerWidth: number; + max: number; + min: number; + onResize: (newSize: number, maybeOldSize?: number | undefined) => void; +} + +function ResizeableContainer({children, containerWidth, min, max, onResize}: Props) { + const {isHeld, onDoubleClick, onMouseDown, size} = useResizableDrawer({ + direction: 'left', + initialSize: containerWidth / 2, + min, + onResize, + }); + + const leftPx = toPixels(Math.min(size, max)); + return ( + + + {children} + + + + + ); +} + +const ResizeableContainerGrid = styled('div')` + display: grid; +`; + +export default ResizeableContainer; diff --git a/static/app/views/replays/detail/perfTable/traceGrid.tsx b/static/app/views/replays/detail/perfTable/traceGrid.tsx new file mode 100644 index 00000000000000..6b50c9c066292c --- /dev/null +++ b/static/app/views/replays/detail/perfTable/traceGrid.tsx @@ -0,0 +1,193 @@ +import {Fragment, useRef} from 'react'; +import styled from '@emotion/styled'; + +import ProjectAvatar from 'sentry/components/avatar/projectAvatar'; +import {pickBarColor} from 'sentry/components/performance/waterfall/utils'; +import {space} from 'sentry/styles/space'; +import {Project} from 'sentry/types'; +import toPercent from 'sentry/utils/number/toPercent'; +import toPixels from 'sentry/utils/number/toPixels'; +import type {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types'; +import {useDimensions} from 'sentry/utils/useDimensions'; +import useProjects from 'sentry/utils/useProjects'; +import ResizeableContainer from 'sentry/views/replays/detail/perfTable/resizeableContainer'; +import type {FlattenedTrace} from 'sentry/views/replays/detail/perfTable/useReplayPerfData'; +import useVirtualScrolling from 'sentry/views/replays/detail/perfTable/useVirtualScrolling'; + +const EMDASH = '\u2013'; + +interface Props { + flattenedTrace: FlattenedTrace; + onDimensionChange: () => void; +} + +export default function TraceGrid({flattenedTrace, onDimensionChange}: Props) { + const measureRef = useRef(null); + const {width} = useDimensions({elementRef: measureRef}); + + const scrollableWindowRef = useRef(null); + const scrollableContentRef = useRef(null); + const {offsetX, reclamp: adjustScrollPosition} = useVirtualScrolling({ + windowRef: scrollableWindowRef, + contentRef: scrollableContentRef, + }); + + const hasSize = width > 0; + + return ( + + {hasSize ? ( + { + adjustScrollPosition(); + onDimensionChange(); + }} + > + + + + + + + + + + + + ) : null} + + ); +} + +function SpanNameList({flattenedTrace}: {flattenedTrace: FlattenedTrace}) { + const {projects} = useProjects(); + + return ( + + {flattenedTrace.map(flattened => { + const project = projects.find(p => p.id === String(flattened.trace.project_id)); + + const labelStyle = { + paddingLeft: `calc(${space(2)} * ${flattened.indent})`, + }; + + return ( + + + + {flattened.trace['transaction.op']} + {EMDASH} + {flattened.trace.transaction} + + + ); + })} + + ); +} + +function SpanDurations({flattenedTrace}: {flattenedTrace: FlattenedTrace}) { + const traces = flattenedTrace.map(flattened => flattened.trace); + const startTimestampMs = Math.min(...traces.map(trace => trace.start_timestamp)) * 1000; + const endTimestampMs = Math.max( + ...traces.map(trace => trace.start_timestamp * 1000 + trace['transaction.duration']) + ); + + return ( + + {flattenedTrace.map(flattened => ( + + + + + + {flattened.trace['transaction.duration']}ms + + + ))} + + ); +} + +function barCSSPosition( + startTimestampMs: number, + endTimestampMs: number, + trace: TraceFullDetailed +) { + const fullDuration = Math.abs(endTimestampMs - startTimestampMs) || 1; + const sinceStart = trace.start_timestamp * 1000 - startTimestampMs; + const duration = trace['transaction.duration']; + return { + left: toPercent(sinceStart / fullDuration), + width: toPercent(duration / fullDuration), + }; +} + +const TwoColumns = styled('div')` + display: grid; + grid-template-columns: 1fr max-content; +`; +const Relative = styled('div')` + position: relative; +`; +const OverflowHidden = styled('div')` + overflow: hidden; +`; + +const TxnList = styled('div')` + font-size: ${p => p.theme.fontSizeRelativeSmall}; + + & > :nth-child(2n + 1) { + background: ${p => p.theme.backgroundTertiary}; + } +`; + +const TxnCell = styled('div')` + position: relative; + display: flex; + align-items: center; + justify-self: auto; + + padding: ${space(0.25)} ${space(0.5)}; + overflow: hidden; +`; + +const TxnLabel = styled('div')` + display: flex; + gap: ${space(0.5)}; + + align-items: center; + white-space: nowrap; +`; + +const TxnDuration = styled('div')` + display: flex; + flex: 1 1 auto; + align-items: center; + justify-content: flex-end; +`; + +const TxnDurationBar = styled('div')` + position: absolute; + content: ''; + top: 50%; + transform: translate(0, -50%); + height: ${space(1.5)}; + margin-block: ${space(0.25)}; + user-select: none; + min-width: 1px; +`; diff --git a/static/app/views/replays/detail/perfTable/usePerfFilters.spec.tsx b/static/app/views/replays/detail/perfTable/usePerfFilters.spec.tsx new file mode 100644 index 00000000000000..631dd05c96e1a7 --- /dev/null +++ b/static/app/views/replays/detail/perfTable/usePerfFilters.spec.tsx @@ -0,0 +1,142 @@ +import {browserHistory} from 'react-router'; +import type {Location} from 'history'; + +import {reactHooks} from 'sentry-test/reactTestingLibrary'; + +import hydrateBreadcrumbs from 'sentry/utils/replays/hydrateBreadcrumbs'; +import hydrateSpans from 'sentry/utils/replays/hydrateSpans'; +import {LargestContentfulPaintFrame} from 'sentry/utils/replays/types'; +import {useLocation} from 'sentry/utils/useLocation'; +import type {ReplayTraceRow} from 'sentry/views/replays/detail/perfTable/useReplayPerfData'; + +import usePerfFilters, {FilterFields} from './usePerfFilters'; + +jest.mock('react-router'); +jest.mock('sentry/utils/useLocation'); + +const mockUseLocation = jest.mocked(useLocation); + +const replayRecord = TestStubs.ReplayRecord(); + +const CRUMB_1_NAV: ReplayTraceRow = { + durationMs: 100, + flattenedTraces: [], + lcpFrames: hydrateSpans(replayRecord, [ + TestStubs.Replay.LargestContentfulPaintFrame({ + startTimestamp: new Date(1663691559961), + endTimestamp: new Date(1663691559962), + data: { + nodeId: 1126, + size: 17782, + value: 0, + }, + }), + ]) as LargestContentfulPaintFrame[], + offsetMs: 100, + paintFrames: [], + replayFrame: hydrateSpans(replayRecord, [ + TestStubs.Replay.NavigationFrame({ + startTimestamp: new Date(1663691559961), + endTimestamp: new Date(1663691559962), + }), + ])[0], + timestampMs: 1663691559961, + traces: [], +}; + +const CRUMB_2_CLICK: ReplayTraceRow = { + durationMs: 100, + flattenedTraces: [], + lcpFrames: [], + offsetMs: 100, + paintFrames: [], + replayFrame: hydrateBreadcrumbs(replayRecord, [ + TestStubs.Replay.ClickFrame({ + timestamp: new Date(1663691559961), + }), + ])[0], + timestampMs: 1663691560061, + traces: [], +}; + +describe('usePerfFilters', () => { + const traceRows: ReplayTraceRow[] = [CRUMB_1_NAV, CRUMB_2_CLICK]; + + beforeEach(() => { + jest.mocked(browserHistory.push).mockReset(); + }); + + it('should update the url when setters are called', () => { + const TYPE_OPTION = { + value: 'ui.click', + label: 'User Click', + qs: 'f_p_type' as const, + }; + + mockUseLocation + .mockReturnValueOnce({ + pathname: '/', + query: {}, + } as Location) + .mockReturnValueOnce({ + pathname: '/', + query: {f_p_type: [TYPE_OPTION.value]}, + } as Location); + + const {result} = reactHooks.renderHook(usePerfFilters, { + initialProps: {traceRows}, + }); + + result.current.setFilters([TYPE_OPTION]); + expect(browserHistory.push).toHaveBeenLastCalledWith({ + pathname: '/', + query: { + f_p_type: [TYPE_OPTION.value], + }, + }); + }); + + it('should not filter anything when no values are set', () => { + mockUseLocation.mockReturnValue({ + pathname: '/', + query: {}, + } as Location); + + const {result} = reactHooks.renderHook(usePerfFilters, {initialProps: {traceRows}}); + expect(result.current.items.length).toEqual(1); + }); + + it('should filter by crumb type', () => { + mockUseLocation.mockReturnValue({ + pathname: '/', + query: { + f_p_type: ['ui.click'], + }, + } as Location); + + const {result} = reactHooks.renderHook(usePerfFilters, {initialProps: {traceRows}}); + expect(result.current.items.length).toEqual(1); + }); +}); + +describe('getCrumbTypes', () => { + it('should return a sorted list of crumb types', () => { + const traceRows = [CRUMB_1_NAV, CRUMB_2_CLICK]; // ACTION_1_DEBUG, ACTION_2_CLICK]; + + const {result} = reactHooks.renderHook(usePerfFilters, {initialProps: {traceRows}}); + expect(result.current.getCrumbTypes()).toStrictEqual([ + {label: 'Page Load', qs: 'f_p_type', value: 'navigation.navigate'}, + {label: 'User Click', qs: 'f_p_type', value: 'ui.click'}, + ]); + }); + + it('should deduplicate crumb types', () => { + const traceRows = [CRUMB_1_NAV, CRUMB_2_CLICK, CRUMB_2_CLICK]; + + const {result} = reactHooks.renderHook(usePerfFilters, {initialProps: {traceRows}}); + expect(result.current.getCrumbTypes()).toStrictEqual([ + {label: 'Page Load', qs: 'f_p_type', value: 'navigation.navigate'}, + {label: 'User Click', qs: 'f_p_type', value: 'ui.click'}, + ]); + }); +}); diff --git a/static/app/views/replays/detail/perfTable/usePerfFilters.tsx b/static/app/views/replays/detail/perfTable/usePerfFilters.tsx new file mode 100644 index 00000000000000..326b5482515666 --- /dev/null +++ b/static/app/views/replays/detail/perfTable/usePerfFilters.tsx @@ -0,0 +1,102 @@ +import {useCallback, useMemo} from 'react'; +import uniq from 'lodash/uniq'; + +import type {SelectOption} from 'sentry/components/compactSelect'; +import {decodeList} from 'sentry/utils/queryString'; +import useFiltersInLocationQuery from 'sentry/utils/replays/hooks/useFiltersInLocationQuery'; +import {getFrameOpOrCategory} from 'sentry/utils/replays/types'; +import type {ReplayTraceRow} from 'sentry/views/replays/detail/perfTable/useReplayPerfData'; +import {filterItems} from 'sentry/views/replays/detail/utils'; + +export interface PerfSelectOption extends SelectOption { + qs: 'f_p_type'; +} + +const DEFAULT_FILTERS = { + f_p_type: [], +} as Record; + +export type FilterFields = { + f_p_type: string[]; +}; + +type Options = { + traceRows: ReplayTraceRow[]; +}; + +type Return = { + getCrumbTypes: () => {label: string; value: string}[]; + items: ReplayTraceRow[]; + selectValue: string[]; + setFilters: (val: PerfSelectOption[]) => void; +}; + +const TYPE_TO_LABEL: Record = { + 'ui.click': 'User Click', + 'ui.slowClickDetected': 'Rage & Dead Click', + 'navigation.back_forward': 'Navigate Back/Forward', + 'navigation.navigate': 'Page Load', + 'navigation.push': 'Navigation', + 'navigation.reload': 'Reload', +}; + +function typeToLabel(val: string): string { + return TYPE_TO_LABEL[val] ?? 'Unknown'; +} + +const FILTERS = { + type: (item: ReplayTraceRow, type: string[]) => + type.length === 0 || type.includes(getFrameOpOrCategory(item.replayFrame)), +}; + +function usePerfFilters({traceRows}: Options): Return { + const {setFilter, query} = useFiltersInLocationQuery(); + + const type = useMemo(() => decodeList(query.f_p_type), [query.f_p_type]); + + const items = useMemo( + () => + filterItems({ + items: traceRows, + filterFns: FILTERS, + filterVals: {type}, + }), + [traceRows, type] + ); + + const getCrumbTypes = useCallback( + () => + uniq( + traceRows.map(traceRow => getFrameOpOrCategory(traceRow.replayFrame)).concat(type) + ) + .sort() + .map(value => ({ + value, + label: typeToLabel(value), + qs: 'f_p_type', + })), + [traceRows, type] + ); + + const setFilters = useCallback( + (value: PerfSelectOption[]) => { + const groupedValues = value.reduce((state, selection) => { + return { + ...state, + [selection.qs]: [...state[selection.qs], selection.value], + }; + }, DEFAULT_FILTERS); + setFilter(groupedValues); + }, + [setFilter] + ); + + return { + getCrumbTypes, + items, + setFilters, + selectValue: [...type], + }; +} + +export default usePerfFilters; diff --git a/static/app/views/replays/detail/perfTable/useReplayPerfData.tsx b/static/app/views/replays/detail/perfTable/useReplayPerfData.tsx new file mode 100644 index 00000000000000..c63b2a961525e0 --- /dev/null +++ b/static/app/views/replays/detail/perfTable/useReplayPerfData.tsx @@ -0,0 +1,108 @@ +import {useEffect, useState} from 'react'; + +import type {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types'; +import type ReplayReader from 'sentry/utils/replays/replayReader'; +import type { + LargestContentfulPaintFrame, + PaintFrame, + ReplayFrame, +} from 'sentry/utils/replays/types'; +import { + useFetchTransactions, + useTransactionData, +} from 'sentry/views/replays/detail/trace/replayTransactionContext'; + +interface IndentedTraceDetailed { + indent: number; + trace: TraceFullDetailed; +} + +export type FlattenedTrace = IndentedTraceDetailed[]; + +export interface ReplayTraceRow { + durationMs: number; + flattenedTraces: FlattenedTrace[]; + lcpFrames: LargestContentfulPaintFrame[]; + offsetMs: number; + paintFrames: PaintFrame[]; + replayFrame: ReplayFrame; + timestampMs: number; + traces: TraceFullDetailed[]; +} + +interface Props { + replay: ReplayReader | null; +} + +function mapTraces(indent: number, traces: TraceFullDetailed[]) { + return traces.flatMap(trace => { + return [ + { + indent, + trace, + }, + ...mapTraces(indent + 1, trace.children), + ]; + }); +} + +export default function useReplayPerfData({replay}: Props) { + const [data, setData] = useState([]); + + const { + state: {didInit: _didInit, errors: errors, isFetching: isFetching, traces = []}, + eventView: _eventView, + } = useTransactionData(); + + useFetchTransactions(); + + useEffect(() => { + if (!replay) { + return; + } + + const frames = replay.getPerfFrames(); + + const rows = frames.map((thisFrame, i): ReplayTraceRow => { + const nextFrame = frames[i + 1] as ReplayFrame | undefined; + + const isWithinThisAndNextFrame = (frame: ReplayFrame) => { + return ( + frame.timestampMs > thisFrame.timestampMs && + (nextFrame === undefined || frame.timestampMs < nextFrame.timestampMs) + ); + }; + + const lcpFrames = replay.getLPCFrames().filter(isWithinThisAndNextFrame); + const paintFrames = replay.getPaintFrames().filter(isWithinThisAndNextFrame); + + const tracesAfterThis = traces.filter( + trace => trace.timestamp * 1000 >= thisFrame.timestampMs + ); + + const relatedTraces = nextFrame + ? tracesAfterThis.filter(trace => trace.timestamp * 1000 < nextFrame.timestampMs) + : tracesAfterThis; + const flattenedTraces = relatedTraces.map(trace => mapTraces(0, [trace])); + + return { + durationMs: nextFrame ? nextFrame.timestampMs - thisFrame.timestampMs : 0, + lcpFrames, + offsetMs: thisFrame.offsetMs, + paintFrames, + replayFrame: thisFrame, + timestampMs: thisFrame.timestampMs, + traces: relatedTraces, + flattenedTraces, + }; + }); + + setData(rows); + }, [replay, traces]); + + return { + data, + errors, + isFetching, + }; +} diff --git a/static/app/views/replays/detail/perfTable/useVirtualScrolling.tsx b/static/app/views/replays/detail/perfTable/useVirtualScrolling.tsx new file mode 100644 index 00000000000000..c680da212f5937 --- /dev/null +++ b/static/app/views/replays/detail/perfTable/useVirtualScrolling.tsx @@ -0,0 +1,66 @@ +import {RefObject, useCallback, useEffect, useState} from 'react'; + +import clamp from 'sentry/utils/number/clamp'; + +interface Props { + contentRef: RefObject; + windowRef: RefObject; +} + +export default function useVirtualScrolling({ + contentRef, + windowRef, +}: Props) { + const window = windowRef.current; + const content = contentRef.current; + + const [scrollPosition, setScrollPosition] = useState({ + offsetX: 0, + offsetY: 0, + }); + + const reclamp = useCallback(() => { + if (!window) { + return; + } + setScrollPosition(prev => { + const minX = + (content?.clientWidth ?? Number.MAX_SAFE_INTEGER) * -1 + window.clientWidth; + const minY = + (content?.clientHeight ?? Number.MAX_SAFE_INTEGER) * -1 + window.clientHeight; + const offsetX = clamp(prev.offsetX, minX, 0); + const offsetY = clamp(prev.offsetY, minY, 0); + return {offsetX, offsetY}; + }); + }, [content, window]); + + useEffect(() => { + if (!window) { + return () => {}; + } + + const handleWheel = (e: WheelEvent) => { + const {deltaX, deltaY} = e; + + setScrollPosition(prev => { + const minX = + (content?.clientWidth ?? Number.MAX_SAFE_INTEGER) * -1 + window.clientWidth; + const minY = + (content?.clientHeight ?? Number.MAX_SAFE_INTEGER) * -1 + window.clientHeight; + const offsetX = clamp(prev.offsetX - deltaX, minX, 0); + const offsetY = clamp(prev.offsetY - deltaY, minY, 0); + return {offsetX, offsetY}; + }); + }; + + window.addEventListener('wheel', handleWheel); + return () => { + window.removeEventListener('wheel', handleWheel); + }; + }, [content, window]); + + return { + ...scrollPosition, + reclamp, + }; +} diff --git a/static/app/views/replays/detail/useVirtualListDimentionChange.tsx b/static/app/views/replays/detail/useVirtualListDimentionChange.tsx index 6baf496df4be17..6d778da7724fd1 100644 --- a/static/app/views/replays/detail/useVirtualListDimentionChange.tsx +++ b/static/app/views/replays/detail/useVirtualListDimentionChange.tsx @@ -1,24 +1,19 @@ -import {MouseEvent, RefObject, useCallback} from 'react'; +import {RefObject, useCallback} from 'react'; import {CellMeasurerCache, List} from 'react-virtualized'; -import {OnExpandCallback} from 'sentry/components/objectInspector'; - type Opts = { cache: CellMeasurerCache; listRef: RefObject; }; -export type OnDimensionChange = OnExpandCallback extends (...a: infer U) => infer R - ? (index: number, ...a: U) => R - : never; +export type OnDimensionChange = (index: number) => void; export default function useVirtualListDimentionChange({cache, listRef}: Opts) { const handleDimensionChange = useCallback( - (index: number, event: MouseEvent) => { + (index: number) => { cache.clear(index, 0); listRef.current?.recomputeGridSize({rowIndex: index}); listRef.current?.forceUpdateGrid(); - event.stopPropagation(); }, [cache, listRef] ); diff --git a/static/app/views/replays/detail/useVirtualizedInspector.tsx b/static/app/views/replays/detail/useVirtualizedInspector.tsx index 60ba6586ba825f..f2cafb9684ed5f 100644 --- a/static/app/views/replays/detail/useVirtualizedInspector.tsx +++ b/static/app/views/replays/detail/useVirtualizedInspector.tsx @@ -9,6 +9,13 @@ type Opts = { listRef: RefObject; }; +export type OnDimensionChange = ( + index: number, + path: string, + expandedState: Record, + event: MouseEvent +) => void; + export default function useVirtualizedInspector({cache, listRef, expandPathsRef}: Opts) { const {handleDimensionChange} = useVirtualListDimentionChange({cache, listRef}); @@ -29,7 +36,8 @@ export default function useVirtualizedInspector({cache, listRef, expandPathsRef} rowState.delete(path); } expandPathsRef.current?.set(index, rowState); - handleDimensionChange(index, event); + handleDimensionChange(index); + event.stopPropagation(); }, [expandPathsRef, handleDimensionChange] ), diff --git a/static/app/views/settings/organizationAuditLog/auditLogList.tsx b/static/app/views/settings/organizationAuditLog/auditLogList.tsx index 2fd15e5c49fe64..99d559a1bdff5d 100644 --- a/static/app/views/settings/organizationAuditLog/auditLogList.tsx +++ b/static/app/views/settings/organizationAuditLog/auditLogList.tsx @@ -212,6 +212,20 @@ function AuditNote({ ); } + if (entry.event === 'project.ownership-rule.edit') { + return ( + + {tct('Modified ownership rules in project [projectSettingsLink]', { + projectSettingsLink: ( + + {entry.data.slug} + + ), + })} + + ); + } + return {entry.note}; } diff --git a/static/app/views/settings/organizationMembers/inviteBanner.spec.tsx b/static/app/views/settings/organizationMembers/inviteBanner.spec.tsx index d014f5526852ef..9672c5c8269da1 100644 --- a/static/app/views/settings/organizationMembers/inviteBanner.spec.tsx +++ b/static/app/views/settings/organizationMembers/inviteBanner.spec.tsx @@ -41,8 +41,10 @@ describe('inviteBanner', function () { render( undefined} + onSendInvite={() => {}} organization={org} + allowedRoles={[]} + onModalClose={() => {}} /> ); @@ -63,8 +65,10 @@ describe('inviteBanner', function () { render( undefined} + onSendInvite={() => {}} organization={org} + allowedRoles={[]} + onModalClose={() => {}} /> ); @@ -83,8 +87,10 @@ describe('inviteBanner', function () { render( undefined} + onSendInvite={() => {}} organization={org} + allowedRoles={[]} + onModalClose={() => {}} /> ); @@ -104,8 +110,10 @@ describe('inviteBanner', function () { render( undefined} + onSendInvite={() => {}} organization={org} + allowedRoles={[]} + onModalClose={() => {}} /> ); @@ -136,8 +144,10 @@ describe('inviteBanner', function () { render( undefined} + onSendInvite={() => {}} organization={org} + allowedRoles={[]} + onModalClose={() => {}} /> ); @@ -168,8 +178,10 @@ describe('inviteBanner', function () { render( undefined} + onSendInvite={() => {}} organization={org} + allowedRoles={[]} + onModalClose={() => {}} /> ); diff --git a/static/app/views/settings/organizationMembers/inviteBanner.tsx b/static/app/views/settings/organizationMembers/inviteBanner.tsx index b2329a224cc8d3..e18584a3ce0644 100644 --- a/static/app/views/settings/organizationMembers/inviteBanner.tsx +++ b/static/app/views/settings/organizationMembers/inviteBanner.tsx @@ -1,6 +1,7 @@ import {useCallback, useEffect, useState} from 'react'; import styled from '@emotion/styled'; +import {openInviteMissingMembersModal} from 'sentry/actionCreators/modal'; import {promptsCheck, promptsUpdate} from 'sentry/actionCreators/prompts'; import {Button} from 'sentry/components/button'; import Card from 'sentry/components/card'; @@ -12,18 +13,26 @@ import QuestionTooltip from 'sentry/components/questionTooltip'; import {IconCommit, IconEllipsis, IconGithub, IconMail} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; -import {MissingMember, Organization} from 'sentry/types'; +import {MissingMember, Organization, OrgRole} from 'sentry/types'; import {promptIsDismissed} from 'sentry/utils/promptIsDismissed'; import useApi from 'sentry/utils/useApi'; import withOrganization from 'sentry/utils/withOrganization'; type Props = { + allowedRoles: OrgRole[]; missingMembers: {integration: string; users: MissingMember[]}; + onModalClose: () => void; onSendInvite: (email: string) => void; organization: Organization; }; -export function InviteBanner({missingMembers, onSendInvite, organization}: Props) { +export function InviteBanner({ + missingMembers, + onSendInvite, + organization, + allowedRoles, + onModalClose, +}: Props) { // NOTE: this is currently used for Github only const hideBanner = @@ -88,34 +97,48 @@ export function InviteBanner({missingMembers, onSendInvite, organization}: Props const users = missingMembers.users; - const cards = users.slice(0, 5).map(member => ( - - - - - {/* TODO: create mapping from integration to lambda external link function */} - - {tct('@[externalId]', {externalId: member.externalId})} - - - - - {tct('[commitCount] Recent Commits', {commitCount: member.commitCount})} - - {member.email} - - - - )); + + + + {/* TODO(cathy): create mapping from integration to lambda external link function */} + + @{username} + + + + + {tct('[commitCount] Recent Commits', {commitCount: member.commitCount})} + + {member.email} + + + + ); + }); - cards.push(); + cards.push( + + ); return ( @@ -138,7 +161,14 @@ export function InviteBanner({missingMembers, onSendInvite, organization}: Props @@ -161,30 +191,47 @@ export function InviteBanner({missingMembers, onSendInvite, organization}: Props export default withOrganization(InviteBanner); type SeeMoreCardProps = { - missingUsers: MissingMember[]; + allowedRoles: OrgRole[]; + missingMembers: {integration: string; users: MissingMember[]}; + onModalClose: () => void; + organization: Organization; }; -function SeeMoreCard({missingUsers}: SeeMoreCardProps) { +function SeeMoreCard({ + missingMembers, + allowedRoles, + onModalClose, + organization, +}: SeeMoreCardProps) { + const {users} = missingMembers; + return ( {tct('See all [missingMembersCount] missing members', { - missingMembersCount: missingUsers.length, + missingMembersCount: users.length, })} {tct('Accounting for [totalCommits] recent commits', { - totalCommits: missingUsers.reduce((acc, curr) => acc + curr.commitCount, 0), + totalCommits: users.reduce((acc, curr) => acc + curr.commitCount, 0), })} @@ -202,7 +249,6 @@ const StyledCard = styled(Card)` const CardTitleContainer = styled('div')` display: flex; justify-content: space-between; - margin-bottom: ${space(1)}; `; const CardTitleContent = styled('div')` @@ -217,7 +263,7 @@ const CardTitle = styled('h6')` color: ${p => p.theme.gray400}; `; -const Subtitle = styled('div')` +export const Subtitle = styled('div')` display: flex; align-items: center; font-size: ${p => p.theme.fontSizeSmall}; @@ -264,7 +310,7 @@ const MemberCardContentRow = styled('div')` } `; -const StyledExternalLink = styled(ExternalLink)` +export const StyledExternalLink = styled(ExternalLink)` font-size: ${p => p.theme.fontSizeMedium}; `; diff --git a/static/app/views/settings/organizationMembers/organizationMembersList.tsx b/static/app/views/settings/organizationMembers/organizationMembersList.tsx index ca25a17357aec2..511c2bc737874a 100644 --- a/static/app/views/settings/organizationMembers/organizationMembersList.tsx +++ b/static/app/views/settings/organizationMembers/organizationMembersList.tsx @@ -340,6 +340,8 @@ class OrganizationMembersList extends DeprecatedAsyncView { {({css}) => diff --git a/static/app/views/settings/organizationTeams/teamMembersRow.tsx b/static/app/views/settings/organizationTeams/teamMembersRow.tsx index 33dc225d2cc420..308711fb9f2d22 100644 --- a/static/app/views/settings/organizationTeams/teamMembersRow.tsx +++ b/static/app/views/settings/organizationTeams/teamMembersRow.tsx @@ -130,7 +130,7 @@ const RoleSelectWrapper = styled('div')` export const GRID_TEMPLATE = ` display: grid; - grid-template-columns: minmax(100px, 1fr) 200px 95px; + grid-template-columns: minmax(100px, 1fr) 200px 150px; gap: ${space(1)}; `; diff --git a/static/app/views/starfish/components/chart.tsx b/static/app/views/starfish/components/chart.tsx index b30c5831896059..3623045ded8306 100644 --- a/static/app/views/starfish/components/chart.tsx +++ b/static/app/views/starfish/components/chart.tsx @@ -50,15 +50,15 @@ import { } from 'sentry/utils/discover/fields'; import usePageFilters from 'sentry/utils/usePageFilters'; import useRouter from 'sentry/utils/useRouter'; -import {SpanMetricsFields} from 'sentry/views/starfish/types'; +import {SpanMetricsField} from 'sentry/views/starfish/types'; const STARFISH_CHART_GROUP = 'starfish_chart_group'; export const STARFISH_FIELDS: Record = { - [SpanMetricsFields.SPAN_DURATION]: { + [SpanMetricsField.SPAN_DURATION]: { outputType: 'duration', }, - [SpanMetricsFields.SPAN_SELF_TIME]: { + [SpanMetricsField.SPAN_SELF_TIME]: { outputType: 'duration', }, // local is only used with `time_spent_percentage` function diff --git a/static/app/views/starfish/components/chartPanel.tsx b/static/app/views/starfish/components/chartPanel.tsx index ab6ac6fd208d08..76001354f63e4e 100644 --- a/static/app/views/starfish/components/chartPanel.tsx +++ b/static/app/views/starfish/components/chartPanel.tsx @@ -3,14 +3,16 @@ import styled from '@emotion/styled'; import Panel from 'sentry/components/panels/panel'; import PanelBody from 'sentry/components/panels/panelBody'; import {space} from 'sentry/styles/space'; +import {Subtitle} from 'sentry/views/performance/landing/widgets/widgets/singleFieldAreaWidget'; type Props = { children: React.ReactNode; button?: JSX.Element; + subtitle?: React.ReactNode; title?: React.ReactNode; }; -export default function ChartPanel({title, children, button}: Props) { +export default function ChartPanel({title, children, button, subtitle}: Props) { return ( @@ -20,19 +22,27 @@ export default function ChartPanel({title, children, button}: Props) { {button} )} + {subtitle && ( + + {subtitle} + + )} {children} ); } -const ChartLabel = styled('p')` +const SubtitleContainer = styled('div')` + padding-top: ${space(0.5)}; +`; + +const ChartLabel = styled('div')` ${p => p.theme.text.cardTitle} `; const Header = styled('div')` padding: 0 ${space(1)} 0 0; - min-height: 36px; width: 100%; display: flex; align-items: center; diff --git a/static/app/views/starfish/components/spanDescription.tsx b/static/app/views/starfish/components/spanDescription.tsx index 7bf7c1646cfe9a..31389272f1d0f5 100644 --- a/static/app/views/starfish/components/spanDescription.tsx +++ b/static/app/views/starfish/components/spanDescription.tsx @@ -1,22 +1,22 @@ import styled from '@emotion/styled'; import {CodeSnippet} from 'sentry/components/codeSnippet'; -import {SpanMetricsFields, SpanMetricsFieldTypes} from 'sentry/views/starfish/types'; +import {SpanMetricsField, SpanMetricsFieldTypes} from 'sentry/views/starfish/types'; import {SQLishFormatter} from 'sentry/views/starfish/utils/sqlish/SQLishFormatter'; type Props = { span: Pick< SpanMetricsFieldTypes, - SpanMetricsFields.SPAN_OP | SpanMetricsFields.SPAN_DESCRIPTION + SpanMetricsField.SPAN_OP | SpanMetricsField.SPAN_DESCRIPTION >; }; export function SpanDescription({span}: Props) { - if (span[SpanMetricsFields.SPAN_OP].startsWith('db')) { + if (span[SpanMetricsField.SPAN_OP].startsWith('db')) { return ; } - return {span[SpanMetricsFields.SPAN_DESCRIPTION]}; + return {span[SpanMetricsField.SPAN_DESCRIPTION]}; } function DatabaseSpanDescription({span}: Props) { @@ -24,7 +24,7 @@ function DatabaseSpanDescription({span}: Props) { return ( - {formatter.toString(span[SpanMetricsFields.SPAN_DESCRIPTION])} + {formatter.toString(span[SpanMetricsField.SPAN_DESCRIPTION])} ); } diff --git a/static/app/views/starfish/components/tableCells/renderHeadCell.tsx b/static/app/views/starfish/components/tableCells/renderHeadCell.tsx index d9797013c8a6d5..6e9e6ffeae2859 100644 --- a/static/app/views/starfish/components/tableCells/renderHeadCell.tsx +++ b/static/app/views/starfish/components/tableCells/renderHeadCell.tsx @@ -8,7 +8,7 @@ import { parseFunction, Sort, } from 'sentry/utils/discover/fields'; -import {SpanMetricsFields, StarfishFunctions} from 'sentry/views/starfish/types'; +import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types'; import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters'; type Options = { @@ -17,8 +17,8 @@ type Options = { sort?: Sort; }; -const {SPAN_SELF_TIME} = SpanMetricsFields; -const {TIME_SPENT_PERCENTAGE, SPS, SPM, HTTP_ERROR_COUNT} = StarfishFunctions; +const {SPAN_SELF_TIME} = SpanMetricsField; +const {TIME_SPENT_PERCENTAGE, SPS, SPM, HTTP_ERROR_COUNT} = SpanFunction; export const SORTABLE_FIELDS = new Set([ `avg(${SPAN_SELF_TIME})`, @@ -54,7 +54,7 @@ export const renderHeadCell = ({column, location, sort}: Options) => { ...location, query: { ...location?.query, - [QueryParameterNames.SORT]: newSort, + [QueryParameterNames.SPANS_SORT]: newSort, }, }; }} diff --git a/static/app/views/starfish/components/tableCells/spanDescriptionCell.tsx b/static/app/views/starfish/components/tableCells/spanDescriptionCell.tsx index 892a6a5c8ba845..3db287baa33030 100644 --- a/static/app/views/starfish/components/tableCells/spanDescriptionCell.tsx +++ b/static/app/views/starfish/components/tableCells/spanDescriptionCell.tsx @@ -13,7 +13,7 @@ import {useHoverOverlay, UseHoverOverlayProps} from 'sentry/utils/useHoverOverla import {useLocation} from 'sentry/utils/useLocation'; import {OverflowEllipsisTextContainer} from 'sentry/views/starfish/components/textAlign'; import {useFullSpanFromTrace} from 'sentry/views/starfish/queries/useFullSpanFromTrace'; -import {ModuleName, StarfishFunctions} from 'sentry/views/starfish/types'; +import {ModuleName, SpanFunction} from 'sentry/views/starfish/types'; import {extractRoute} from 'sentry/views/starfish/utils/extractRoute'; import {useRoutingContext} from 'sentry/views/starfish/utils/routingContext'; import {SQLishFormatter} from 'sentry/views/starfish/utils/sqlish/SQLishFormatter'; @@ -54,13 +54,13 @@ export function SpanDescriptionCell({ endpointMethod, }; - const sort: string | undefined = queryString[QueryParameterNames.SORT]; + const sort: string | undefined = queryString[QueryParameterNames.SPANS_SORT]; // the spans page uses time_spent_percentage(local), so to persist the sort upon navigation we need to replace - if (sort?.includes(`${StarfishFunctions.TIME_SPENT_PERCENTAGE}()`)) { - queryString[QueryParameterNames.SORT] = sort.replace( - `${StarfishFunctions.TIME_SPENT_PERCENTAGE}()`, - `${StarfishFunctions.TIME_SPENT_PERCENTAGE}(local)` + if (sort?.includes(`${SpanFunction.TIME_SPENT_PERCENTAGE}()`)) { + queryString[QueryParameterNames.SPANS_SORT] = sort.replace( + `${SpanFunction.TIME_SPENT_PERCENTAGE}()`, + `${SpanFunction.TIME_SPENT_PERCENTAGE}(local)` ); } diff --git a/static/app/views/starfish/queries/useFullSpanFromTrace.tsx b/static/app/views/starfish/queries/useFullSpanFromTrace.tsx index 6e67aef4112bca..8c3fc9d4ec36f3 100644 --- a/static/app/views/starfish/queries/useFullSpanFromTrace.tsx +++ b/static/app/views/starfish/queries/useFullSpanFromTrace.tsx @@ -1,6 +1,6 @@ import {useEventJSON} from 'sentry/views/starfish/queries/useEventJSON'; import {useIndexedSpans} from 'sentry/views/starfish/queries/useIndexedSpans'; -import {SpanIndexedFields} from 'sentry/views/starfish/types'; +import {SpanIndexedField} from 'sentry/views/starfish/types'; // NOTE: Fetching the top one is a bit naive, but works for now. A better // approach might be to fetch several at a time, and let the hook consumer @@ -9,7 +9,7 @@ export function useFullSpanFromTrace(group?: string, enabled: boolean = true) { const filters: {[key: string]: string} = {}; if (group) { - filters[SpanIndexedFields.SPAN_GROUP] = group; + filters[SpanIndexedField.SPAN_GROUP] = group; } const indexedSpansResponse = useIndexedSpans(filters, 1, enabled); @@ -17,12 +17,12 @@ export function useFullSpanFromTrace(group?: string, enabled: boolean = true) { const firstIndexedSpan = indexedSpansResponse.data?.[0]; const eventJSONResponse = useEventJSON( - firstIndexedSpan ? firstIndexedSpan[SpanIndexedFields.TRANSACTION_ID] : undefined, - firstIndexedSpan ? firstIndexedSpan[SpanIndexedFields.PROJECT] : undefined + firstIndexedSpan ? firstIndexedSpan[SpanIndexedField.TRANSACTION_ID] : undefined, + firstIndexedSpan ? firstIndexedSpan[SpanIndexedField.PROJECT] : undefined ); const fullSpan = eventJSONResponse?.data?.spans?.find( - span => span.span_id === firstIndexedSpan?.[SpanIndexedFields.ID] + span => span.span_id === firstIndexedSpan?.[SpanIndexedField.ID] ); // N.B. There isn't a great pattern for us to merge the responses together, diff --git a/static/app/views/starfish/queries/useIndexedSpans.tsx b/static/app/views/starfish/queries/useIndexedSpans.tsx index 8fd30e5877f55b..aa83ad59945097 100644 --- a/static/app/views/starfish/queries/useIndexedSpans.tsx +++ b/static/app/views/starfish/queries/useIndexedSpans.tsx @@ -4,7 +4,7 @@ import EventView from 'sentry/utils/discover/eventView'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {useLocation} from 'sentry/utils/useLocation'; -import {SpanIndexedFields, SpanIndexedFieldTypes} from 'sentry/views/starfish/types'; +import {SpanIndexedField, SpanIndexedFieldTypes} from 'sentry/views/starfish/types'; import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery'; const DEFAULT_LIMIT = 10; @@ -43,7 +43,7 @@ function getEventView(filters: Filters, location: Location) { { name: '', query: search.formatString(), - fields: Object.values(SpanIndexedFields), + fields: Object.values(SpanIndexedField), dataset: DiscoverDatasets.SPANS_INDEXED, version: 2, }, diff --git a/static/app/views/starfish/queries/useSpanList.tsx b/static/app/views/starfish/queries/useSpanList.tsx index 29823b9b9a7143..0801cf391c9315 100644 --- a/static/app/views/starfish/queries/useSpanList.tsx +++ b/static/app/views/starfish/queries/useSpanList.tsx @@ -6,12 +6,12 @@ import EventView from 'sentry/utils/discover/eventView'; import type {Sort} from 'sentry/utils/discover/fields'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {useLocation} from 'sentry/utils/useLocation'; -import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types'; +import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; import {buildEventViewQuery} from 'sentry/views/starfish/utils/buildEventViewQuery'; import {useWrappedDiscoverQuery} from 'sentry/views/starfish/utils/useSpansQuery'; const {SPAN_SELF_TIME, SPAN_DESCRIPTION, SPAN_GROUP, SPAN_OP, SPAN_DOMAIN, PROJECT_ID} = - SpanMetricsFields; + SpanMetricsField; export type SpanMetrics = { 'avg(span.self_time)': number; diff --git a/static/app/views/starfish/queries/useSpanMetrics.tsx b/static/app/views/starfish/queries/useSpanMetrics.tsx index 42032da590e41d..c00cbe3fcac155 100644 --- a/static/app/views/starfish/queries/useSpanMetrics.tsx +++ b/static/app/views/starfish/queries/useSpanMetrics.tsx @@ -3,10 +3,10 @@ import {Location} from 'history'; import EventView from 'sentry/utils/discover/eventView'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {useLocation} from 'sentry/utils/useLocation'; -import {SpanMetricsFields} from 'sentry/views/starfish/types'; +import {SpanMetricsField} from 'sentry/views/starfish/types'; import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery'; -const {SPAN_GROUP} = SpanMetricsFields; +const {SPAN_GROUP} = SpanMetricsField; export type SpanMetrics = { [metric: string]: number | string; diff --git a/static/app/views/starfish/queries/useSpanMetricsSeries.tsx b/static/app/views/starfish/queries/useSpanMetricsSeries.tsx index abe234a9abf85d..2b5f4875d3af30 100644 --- a/static/app/views/starfish/queries/useSpanMetricsSeries.tsx +++ b/static/app/views/starfish/queries/useSpanMetricsSeries.tsx @@ -7,11 +7,11 @@ import EventView from 'sentry/utils/discover/eventView'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import usePageFilters from 'sentry/utils/usePageFilters'; import {SpanSummaryQueryFilters} from 'sentry/views/starfish/queries/useSpanMetrics'; -import {SpanMetricsFields} from 'sentry/views/starfish/types'; +import {SpanMetricsField} from 'sentry/views/starfish/types'; import {STARFISH_CHART_INTERVAL_FIDELITY} from 'sentry/views/starfish/utils/constants'; import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery'; -const {SPAN_GROUP} = SpanMetricsFields; +const {SPAN_GROUP} = SpanMetricsField; export type SpanMetrics = { interval: number; diff --git a/static/app/views/starfish/queries/useSpanSamples.tsx b/static/app/views/starfish/queries/useSpanSamples.tsx index 19287ce89e3fc8..87918a29743ab4 100644 --- a/static/app/views/starfish/queries/useSpanSamples.tsx +++ b/static/app/views/starfish/queries/useSpanSamples.tsx @@ -9,11 +9,11 @@ import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import {computeAxisMax} from 'sentry/views/starfish/components/chart'; import {useSpanMetricsSeries} from 'sentry/views/starfish/queries/useSpanMetricsSeries'; -import {SpanIndexedFields, SpanIndexedFieldTypes} from 'sentry/views/starfish/types'; +import {SpanIndexedField, SpanIndexedFieldTypes} from 'sentry/views/starfish/types'; import {getDateConditions} from 'sentry/views/starfish/utils/getDateConditions'; import {DATE_FORMAT} from 'sentry/views/starfish/utils/useSpansQuery'; -const {SPAN_SELF_TIME, SPAN_GROUP} = SpanIndexedFields; +const {SPAN_SELF_TIME, SPAN_GROUP} = SpanIndexedField; type Options = { groupId: string; @@ -23,11 +23,11 @@ type Options = { export type SpanSample = Pick< SpanIndexedFieldTypes, - | SpanIndexedFields.SPAN_SELF_TIME - | SpanIndexedFields.TRANSACTION_ID - | SpanIndexedFields.PROJECT - | SpanIndexedFields.TIMESTAMP - | SpanIndexedFields.ID + | SpanIndexedField.SPAN_SELF_TIME + | SpanIndexedField.TRANSACTION_ID + | SpanIndexedField.PROJECT + | SpanIndexedField.TIMESTAMP + | SpanIndexedField.ID >; export const useSpanSamples = (options: Options) => { diff --git a/static/app/views/starfish/queries/useSpanTransactionMetrics.tsx b/static/app/views/starfish/queries/useSpanTransactionMetrics.tsx index 727b78baa0fb8e..b3aec798794ea8 100644 --- a/static/app/views/starfish/queries/useSpanTransactionMetrics.tsx +++ b/static/app/views/starfish/queries/useSpanTransactionMetrics.tsx @@ -5,10 +5,10 @@ import {Sort} from 'sentry/utils/discover/fields'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {useLocation} from 'sentry/utils/useLocation'; -import {SpanMetricsFields} from 'sentry/views/starfish/types'; +import {SpanMetricsField} from 'sentry/views/starfish/types'; import {useWrappedDiscoverQuery} from 'sentry/views/starfish/utils/useSpansQuery'; -const {SPAN_SELF_TIME, SPAN_GROUP} = SpanMetricsFields; +const {SPAN_SELF_TIME, SPAN_GROUP} = SpanMetricsField; export type SpanTransactionMetrics = { 'avg(span.self_time)': number; @@ -24,7 +24,8 @@ export type SpanTransactionMetrics = { export const useSpanTransactionMetrics = ( group: string, options: {sorts?: Sort[]; transactions?: string[]}, - _referrer = 'api.starfish.span-transaction-metrics' + referrer = 'api.starfish.span-transaction-metrics', + cursor?: string ) => { const location = useLocation(); @@ -37,7 +38,8 @@ export const useSpanTransactionMetrics = ( initialData: [], enabled: Boolean(group), limit: 25, - referrer: _referrer, + referrer, + cursor, }); }; diff --git a/static/app/views/starfish/types.tsx b/static/app/views/starfish/types.tsx index 94bc3703622236..bcc57da0f5a888 100644 --- a/static/app/views/starfish/types.tsx +++ b/static/app/views/starfish/types.tsx @@ -16,7 +16,7 @@ export enum ModuleName { OTHER = 'other', } -export enum SpanMetricsFields { +export enum SpanMetricsField { SPAN_OP = 'span.op', SPAN_DESCRIPTION = 'span.description', SPAN_MODULE = 'span.module', @@ -29,17 +29,17 @@ export enum SpanMetricsFields { } export type SpanMetricsFieldTypes = { - [SpanMetricsFields.SPAN_OP]: string; - [SpanMetricsFields.SPAN_DESCRIPTION]: string; - [SpanMetricsFields.SPAN_MODULE]: string; - [SpanMetricsFields.SPAN_ACTION]: string; - [SpanMetricsFields.SPAN_DOMAIN]: string; - [SpanMetricsFields.SPAN_GROUP]: string; - [SpanMetricsFields.SPAN_SELF_TIME]: number; - [SpanMetricsFields.SPAN_DURATION]: number; + [SpanMetricsField.SPAN_OP]: string; + [SpanMetricsField.SPAN_DESCRIPTION]: string; + [SpanMetricsField.SPAN_MODULE]: string; + [SpanMetricsField.SPAN_ACTION]: string; + [SpanMetricsField.SPAN_DOMAIN]: string; + [SpanMetricsField.SPAN_GROUP]: string; + [SpanMetricsField.SPAN_SELF_TIME]: number; + [SpanMetricsField.SPAN_DURATION]: number; }; -export enum SpanIndexedFields { +export enum SpanIndexedField { SPAN_SELF_TIME = 'span.self_time', SPAN_GROUP = 'span.group', // Span group computed from the normalized description. Matches the group in the metrics data set SPAN_GROUP_RAW = 'span.group_raw', // Span group computed from non-normalized description. Matches the group in the event payload @@ -57,25 +57,25 @@ export enum SpanIndexedFields { } export type SpanIndexedFieldTypes = { - [SpanIndexedFields.SPAN_SELF_TIME]: number; - [SpanIndexedFields.SPAN_GROUP]: string; - [SpanIndexedFields.SPAN_GROUP_RAW]: string; - [SpanIndexedFields.SPAN_MODULE]: string; - [SpanIndexedFields.SPAN_DESCRIPTION]: string; - [SpanIndexedFields.SPAN_OP]: string; - [SpanIndexedFields.ID]: string; - [SpanIndexedFields.SPAN_ACTION]: string; - [SpanIndexedFields.TRANSACTION_ID]: string; - [SpanIndexedFields.TRANSACTION_METHOD]: string; - [SpanIndexedFields.TRANSACTION_OP]: string; - [SpanIndexedFields.SPAN_DOMAIN]: string; - [SpanIndexedFields.TIMESTAMP]: string; - [SpanIndexedFields.PROJECT]: string; + [SpanIndexedField.SPAN_SELF_TIME]: number; + [SpanIndexedField.SPAN_GROUP]: string; + [SpanIndexedField.SPAN_GROUP_RAW]: string; + [SpanIndexedField.SPAN_MODULE]: string; + [SpanIndexedField.SPAN_DESCRIPTION]: string; + [SpanIndexedField.SPAN_OP]: string; + [SpanIndexedField.ID]: string; + [SpanIndexedField.SPAN_ACTION]: string; + [SpanIndexedField.TRANSACTION_ID]: string; + [SpanIndexedField.TRANSACTION_METHOD]: string; + [SpanIndexedField.TRANSACTION_OP]: string; + [SpanIndexedField.SPAN_DOMAIN]: string; + [SpanIndexedField.TIMESTAMP]: string; + [SpanIndexedField.PROJECT]: string; }; -export type Op = SpanIndexedFieldTypes[SpanIndexedFields.SPAN_OP]; +export type Op = SpanIndexedFieldTypes[SpanIndexedField.SPAN_OP]; -export enum StarfishFunctions { +export enum SpanFunction { SPS = 'sps', SPM = 'spm', SPS_PERCENENT_CHANGE = 'sps_percent_change', @@ -84,39 +84,39 @@ export enum StarfishFunctions { } export const StarfishDatasetFields = { - [DiscoverDatasets.SPANS_METRICS]: SpanIndexedFields, - [DiscoverDatasets.SPANS_INDEXED]: SpanIndexedFields, + [DiscoverDatasets.SPANS_METRICS]: SpanIndexedField, + [DiscoverDatasets.SPANS_INDEXED]: SpanIndexedField, }; export const STARFISH_AGGREGATION_FIELDS: Record< - StarfishFunctions, + SpanFunction, FieldDefinition & {defaultOutputType: AggregationOutputType} > = { - [StarfishFunctions.SPS]: { + [SpanFunction.SPS]: { desc: t('Spans per second'), kind: FieldKind.FUNCTION, defaultOutputType: 'number', valueType: FieldValueType.NUMBER, }, - [StarfishFunctions.SPM]: { + [SpanFunction.SPM]: { desc: t('Spans per minute'), kind: FieldKind.FUNCTION, defaultOutputType: 'number', valueType: FieldValueType.NUMBER, }, - [StarfishFunctions.TIME_SPENT_PERCENTAGE]: { + [SpanFunction.TIME_SPENT_PERCENTAGE]: { desc: t('Span time spent percentage'), defaultOutputType: 'percentage', kind: FieldKind.FUNCTION, valueType: FieldValueType.NUMBER, }, - [StarfishFunctions.HTTP_ERROR_COUNT]: { + [SpanFunction.HTTP_ERROR_COUNT]: { desc: t('Count of 5XX http errors'), defaultOutputType: 'integer', kind: FieldKind.FUNCTION, valueType: FieldValueType.NUMBER, }, - [StarfishFunctions.SPS_PERCENENT_CHANGE]: { + [SpanFunction.SPS_PERCENENT_CHANGE]: { desc: t('Spans per second percentage change'), defaultOutputType: 'percentage', kind: FieldKind.FUNCTION, diff --git a/static/app/views/starfish/utils/buildEventViewQuery.tsx b/static/app/views/starfish/utils/buildEventViewQuery.tsx index 917499c76fca14..ee4f7ab27e7a05 100644 --- a/static/app/views/starfish/utils/buildEventViewQuery.tsx +++ b/static/app/views/starfish/utils/buildEventViewQuery.tsx @@ -1,12 +1,12 @@ import {Location} from 'history'; import {defined} from 'sentry/utils'; -import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types'; +import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; import {EMPTY_OPTION_VALUE} from 'sentry/views/starfish/views/spans/selectors/emptyOption'; import {NULL_SPAN_CATEGORY} from 'sentry/views/starfish/views/webServiceView/spanGroupBreakdownContainer'; const {SPAN_DESCRIPTION, SPAN_OP, SPAN_DOMAIN, SPAN_ACTION, SPAN_MODULE} = - SpanMetricsFields; + SpanMetricsField; const SPAN_FILTER_KEYS = [ SPAN_OP, diff --git a/static/app/views/starfish/utils/sqlish/SQLishParser.spec.tsx b/static/app/views/starfish/utils/sqlish/SQLishParser.spec.tsx index 7754f4499e287a..a53ee0c93008a7 100644 --- a/static/app/views/starfish/utils/sqlish/SQLishParser.spec.tsx +++ b/static/app/views/starfish/utils/sqlish/SQLishParser.spec.tsx @@ -26,6 +26,7 @@ describe('SQLishParser', function () { 'flags | %s)', // Bitwise OR 'flags ^ %s)', // Bitwise XOR 'flags ~ %s)', // Bitwise NOT + '+ %s as count', // Arithmetic ])('Parses %s', sql => { expect(() => { parser.parse(sql); diff --git a/static/app/views/starfish/utils/sqlish/sqlish.pegjs b/static/app/views/starfish/utils/sqlish/sqlish.pegjs index 3691ab00023491..a6f08d9c876690 100644 --- a/static/app/views/starfish/utils/sqlish/sqlish.pegjs +++ b/static/app/views/starfish/utils/sqlish/sqlish.pegjs @@ -36,4 +36,4 @@ Whitespace = Whitespace:[\n\t ]+ { return { type: 'Whitespace', content: Whitespace.join("") } } GenericToken - = GenericToken:[a-zA-Z0-9"'`_\-.=><:,*;!\[\]?$%|/@&~^]+ { return { type: 'GenericToken', content: GenericToken.join('') } } + = GenericToken:[a-zA-Z0-9"'`_\-.=><:,*;!\[\]?$%|/@&~^+]+ { return { type: 'GenericToken', content: GenericToken.join('') } } diff --git a/static/app/views/starfish/views/queryParameters.tsx b/static/app/views/starfish/views/queryParameters.tsx index f5371bef3c1362..fbce6a697b4056 100644 --- a/static/app/views/starfish/views/queryParameters.tsx +++ b/static/app/views/starfish/views/queryParameters.tsx @@ -1,4 +1,5 @@ export enum QueryParameterNames { - CURSOR = 'spansCursor', - SORT = 'spansSort', + SPANS_CURSOR = 'spansCursor', + SPANS_SORT = 'spansSort', + ENDPOINTS_CURSOR = 'endpointsCursor', } diff --git a/static/app/views/starfish/views/spanSummaryPage/index.tsx b/static/app/views/starfish/views/spanSummaryPage/index.tsx index a03d0ae24042a8..1c800d10111218 100644 --- a/static/app/views/starfish/views/spanSummaryPage/index.tsx +++ b/static/app/views/starfish/views/spanSummaryPage/index.tsx @@ -19,7 +19,7 @@ import { SpanSummaryQueryFilters, useSpanMetrics, } from 'sentry/views/starfish/queries/useSpanMetrics'; -import {SpanMetricsFields, StarfishFunctions} from 'sentry/views/starfish/types'; +import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types'; import {extractRoute} from 'sentry/views/starfish/utils/extractRoute'; import {ROUTE_NAMES} from 'sentry/views/starfish/utils/routeNames'; import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters'; @@ -36,7 +36,7 @@ type Query = { endpointMethod: string; transaction: string; transactionMethod: string; - [QueryParameterNames.SORT]: string; + [QueryParameterNames.SPANS_SORT]: string; }; type Props = { @@ -54,7 +54,7 @@ function SpanSummaryPage({params, location}: Props) { : {}; const sort = - fromSorts(location.query[QueryParameterNames.SORT]).filter(isAValidSort)[0] ?? + fromSorts(location.query[QueryParameterNames.SPANS_SORT]).filter(isAValidSort)[0] ?? DEFAULT_SORT; // We only allow one sort on this table in this view if (endpointMethod && queryFilter) { @@ -65,21 +65,21 @@ function SpanSummaryPage({params, location}: Props) { groupId, queryFilter, [ - SpanMetricsFields.SPAN_OP, - SpanMetricsFields.SPAN_GROUP, - SpanMetricsFields.PROJECT_ID, - `${StarfishFunctions.SPS}()`, + SpanMetricsField.SPAN_OP, + SpanMetricsField.SPAN_GROUP, + SpanMetricsField.PROJECT_ID, + `${SpanFunction.SPS}()`, ], 'api.starfish.span-summary-page-metrics' ); const span = { - [SpanMetricsFields.SPAN_OP]: spanMetrics[SpanMetricsFields.SPAN_OP], - [SpanMetricsFields.SPAN_GROUP]: groupId, + [SpanMetricsField.SPAN_OP]: spanMetrics[SpanMetricsField.SPAN_OP], + [SpanMetricsField.SPAN_GROUP]: groupId, }; const title = [ - getSpanOperationDescription(span[SpanMetricsFields.SPAN_OP]), + getSpanOperationDescription(span[SpanMetricsField.SPAN_OP]), t('Summary'), ].join(' '); @@ -137,8 +137,7 @@ function SpanSummaryPage({params, location}: Props) { )} diff --git a/static/app/views/starfish/views/spanSummaryPage/sampleList/durationChart/index.tsx b/static/app/views/starfish/views/spanSummaryPage/sampleList/durationChart/index.tsx index 75f9831152faac..48c81250db07ce 100644 --- a/static/app/views/starfish/views/spanSummaryPage/sampleList/durationChart/index.tsx +++ b/static/app/views/starfish/views/spanSummaryPage/sampleList/durationChart/index.tsx @@ -3,21 +3,22 @@ import {useTheme} from '@emotion/react'; import {t} from 'sentry/locale'; import {EChartClickHandler, EChartHighlightHandler, Series} from 'sentry/types/echarts'; import {usePageError} from 'sentry/utils/performance/contexts/pageError'; +import usePageFilters from 'sentry/utils/usePageFilters'; import {AVG_COLOR} from 'sentry/views/starfish/colours'; import Chart from 'sentry/views/starfish/components/chart'; +import ChartPanel from 'sentry/views/starfish/components/chartPanel'; import {isNearAverage} from 'sentry/views/starfish/components/samplesTable/common'; import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics'; import {useSpanMetricsSeries} from 'sentry/views/starfish/queries/useSpanMetricsSeries'; import {SpanSample, useSpanSamples} from 'sentry/views/starfish/queries/useSpanSamples'; -import {SpanMetricsFields} from 'sentry/views/starfish/types'; -import {DataTitles} from 'sentry/views/starfish/views/spans/types'; +import {SpanMetricsField} from 'sentry/views/starfish/types'; import { crossIconPath, downwardPlayIconPath, upwardPlayIconPath, } from 'sentry/views/starfish/views/spanSummaryPage/sampleList/durationChart/symbol'; -const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsFields; +const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsField; type Props = { groupId: string; @@ -41,6 +42,7 @@ function DurationChart({ }: Props) { const theme = useTheme(); const {setPageError} = usePageError(); + const pageFilter = usePageFilters(); const getSampleSymbol = ( duration: number, @@ -176,27 +178,32 @@ function DurationChart({ setPageError(t('An error has occured while loading chart data')); } + const subtitle = pageFilter.selection.datetime.period + ? t('Last %s', pageFilter.selection.datetime.period) + : t('Last period'); + return ( -
-
{DataTitles.avg}
- -
+ +
+ +
+
); } diff --git a/static/app/views/starfish/views/spanSummaryPage/sampleList/index.tsx b/static/app/views/starfish/views/spanSummaryPage/sampleList/index.tsx index 8608e4f0b3bddf..3d47df4979869d 100644 --- a/static/app/views/starfish/views/spanSummaryPage/sampleList/index.tsx +++ b/static/app/views/starfish/views/spanSummaryPage/sampleList/index.tsx @@ -23,17 +23,11 @@ import SampleTable from 'sentry/views/starfish/views/spanSummaryPage/sampleList/ type Props = { groupId: string; - projectId: number; transactionMethod: string; transactionName: string; }; -export function SampleList({ - groupId, - projectId, - transactionName, - transactionMethod, -}: Props) { +export function SampleList({groupId, transactionName, transactionMethod}: Props) { const router = useRouter(); const [highlightedSpanId, setHighlightedSpanId] = useState( undefined @@ -56,8 +50,8 @@ export function SampleList({ const {projects} = useProjects(); const project = useMemo( - () => projects.find(p => p.id === String(projectId)), - [projects, projectId] + () => projects.find(p => p.id === String(query.project)), + [projects, query.project] ); const onOpenDetailPanel = useCallback(() => { @@ -72,7 +66,7 @@ export function SampleList({ : transactionName; const link = `/performance/summary/?${qs.stringify({ - project: projectId, + project: query.project, transaction: transactionName, })}`; diff --git a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleInfo/index.tsx b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleInfo/index.tsx index 4a0b6e549d1a10..47a3761e063b6e 100644 --- a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleInfo/index.tsx +++ b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleInfo/index.tsx @@ -6,11 +6,11 @@ import {usePageError} from 'sentry/utils/performance/contexts/pageError'; import {DurationCell} from 'sentry/views/starfish/components/tableCells/durationCell'; import {ThroughputCell} from 'sentry/views/starfish/components/tableCells/throughputCell'; import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics'; -import {SpanMetricsFields} from 'sentry/views/starfish/types'; +import {SpanMetricsField} from 'sentry/views/starfish/types'; import {DataTitles, getThroughputTitle} from 'sentry/views/starfish/views/spans/types'; import {Block, BlockContainer} from 'sentry/views/starfish/views/spanSummaryPage/block'; -const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsFields; +const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsField; type Props = { groupId: string; diff --git a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx index fa640c0c3710bd..9c3ae44864adc3 100644 --- a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx +++ b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx @@ -5,7 +5,7 @@ import { } from 'sentry-test/reactTestingLibrary'; import {PageFilters} from 'sentry/types'; -import {SpanMetricsFields} from 'sentry/views/starfish/types'; +import {SpanMetricsField} from 'sentry/views/starfish/types'; import SampleTable from './sampleTable'; @@ -94,8 +94,8 @@ const initializeMockRequests = () => { body: { data: [ { - [SpanMetricsFields.SPAN_OP]: 'db', - [`avg(${SpanMetricsFields.SPAN_SELF_TIME})`]: 0.52, + [SpanMetricsField.SPAN_OP]: 'db', + [`avg(${SpanMetricsField.SPAN_SELF_TIME})`]: 0.52, }, ], }, diff --git a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.tsx b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.tsx index bcf3f64423de3f..0e5f80347b2679 100644 --- a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.tsx +++ b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.tsx @@ -13,9 +13,9 @@ import {SpanSamplesTable} from 'sentry/views/starfish/components/samplesTable/sp import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics'; import {SpanSample, useSpanSamples} from 'sentry/views/starfish/queries/useSpanSamples'; import {useTransactions} from 'sentry/views/starfish/queries/useTransactions'; -import {SpanMetricsFields} from 'sentry/views/starfish/types'; +import {SpanMetricsField} from 'sentry/views/starfish/types'; -const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsFields; +const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsField; const SpanSamplesTableContainer = styled('div')` padding-bottom: ${space(2)}; diff --git a/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.spec.tsx b/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.spec.tsx index 90fca50a0c2812..a215936e1144cb 100644 --- a/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.spec.tsx +++ b/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.spec.tsx @@ -1,15 +1,15 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; -import {SpanMetricsFields, StarfishFunctions} from 'sentry/views/starfish/types'; +import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types'; import {SpanMetricsRibbon} from 'sentry/views/starfish/views/spanSummaryPage/spanMetricsRibbon'; describe('SpanMetricsRibbon', function () { const sampleMetrics = { - [SpanMetricsFields.SPAN_OP]: 'db', - [`${StarfishFunctions.SPM}()`]: 17.8, - [`avg(${SpanMetricsFields.SPAN_SELF_TIME})`]: 127.1, - [`sum(${SpanMetricsFields.SPAN_SELF_TIME})`]: 1172319, - [`${StarfishFunctions.TIME_SPENT_PERCENTAGE}()`]: 0.002, + [SpanMetricsField.SPAN_OP]: 'db', + [`${SpanFunction.SPM}()`]: 17.8, + [`avg(${SpanMetricsField.SPAN_SELF_TIME})`]: 127.1, + [`sum(${SpanMetricsField.SPAN_SELF_TIME})`]: 1172319, + [`${SpanFunction.TIME_SPENT_PERCENTAGE}()`]: 0.002, }; it('renders basic metrics', function () { diff --git a/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.tsx b/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.tsx index 117184b01be5b2..3d3a9e03cd3084 100644 --- a/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.tsx +++ b/static/app/views/starfish/views/spanSummaryPage/spanMetricsRibbon.tsx @@ -4,48 +4,48 @@ import {CountCell} from 'sentry/views/starfish/components/tableCells/countCell'; import {DurationCell} from 'sentry/views/starfish/components/tableCells/durationCell'; import {ThroughputCell} from 'sentry/views/starfish/components/tableCells/throughputCell'; import {TimeSpentCell} from 'sentry/views/starfish/components/tableCells/timeSpentCell'; -import {SpanMetricsFields, StarfishFunctions} from 'sentry/views/starfish/types'; +import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types'; import {DataTitles, getThroughputTitle} from 'sentry/views/starfish/views/spans/types'; import {Block, BlockContainer} from 'sentry/views/starfish/views/spanSummaryPage/block'; interface Props { spanMetrics: { - [SpanMetricsFields.SPAN_OP]?: string; - [SpanMetricsFields.SPAN_DESCRIPTION]?: string; - [SpanMetricsFields.SPAN_ACTION]?: string; - [SpanMetricsFields.SPAN_DOMAIN]?: string; - [SpanMetricsFields.SPAN_GROUP]?: string; + [SpanMetricsField.SPAN_OP]?: string; + [SpanMetricsField.SPAN_DESCRIPTION]?: string; + [SpanMetricsField.SPAN_ACTION]?: string; + [SpanMetricsField.SPAN_DOMAIN]?: string; + [SpanMetricsField.SPAN_GROUP]?: string; }; } export function SpanMetricsRibbon({spanMetrics}: Props) { - const op = spanMetrics?.[SpanMetricsFields.SPAN_OP] ?? ''; + const op = spanMetrics?.[SpanMetricsField.SPAN_OP] ?? ''; return ( {op.startsWith('http') && ( - + )} diff --git a/static/app/views/starfish/views/spanSummaryPage/spanSummaryView.tsx b/static/app/views/starfish/views/spanSummaryPage/spanSummaryView.tsx index 5974b729d1a1a7..10f9ecbc558109 100644 --- a/static/app/views/starfish/views/spanSummaryPage/spanSummaryView.tsx +++ b/static/app/views/starfish/views/spanSummaryPage/spanSummaryView.tsx @@ -16,7 +16,7 @@ import { useSpanMetrics, } from 'sentry/views/starfish/queries/useSpanMetrics'; import {useSpanMetricsSeries} from 'sentry/views/starfish/queries/useSpanMetricsSeries'; -import {SpanMetricsFields, StarfishFunctions} from 'sentry/views/starfish/types'; +import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types'; import { DataTitles, getThroughputChartTitle, @@ -51,43 +51,43 @@ export function SpanSummaryView({groupId}: Props) { groupId, queryFilter, [ - SpanMetricsFields.SPAN_OP, - SpanMetricsFields.SPAN_DESCRIPTION, - SpanMetricsFields.SPAN_ACTION, - SpanMetricsFields.SPAN_DOMAIN, + SpanMetricsField.SPAN_OP, + SpanMetricsField.SPAN_DESCRIPTION, + SpanMetricsField.SPAN_ACTION, + SpanMetricsField.SPAN_DOMAIN, 'count()', - `${StarfishFunctions.SPM}()`, - `sum(${SpanMetricsFields.SPAN_SELF_TIME})`, - `avg(${SpanMetricsFields.SPAN_SELF_TIME})`, - `${StarfishFunctions.TIME_SPENT_PERCENTAGE}()`, - `${StarfishFunctions.HTTP_ERROR_COUNT}()`, + `${SpanFunction.SPM}()`, + `sum(${SpanMetricsField.SPAN_SELF_TIME})`, + `avg(${SpanMetricsField.SPAN_SELF_TIME})`, + `${SpanFunction.TIME_SPENT_PERCENTAGE}()`, + `${SpanFunction.HTTP_ERROR_COUNT}()`, ], 'api.starfish.span-summary-page-metrics' ); const span = { ...spanMetrics, - [SpanMetricsFields.SPAN_GROUP]: groupId, + [SpanMetricsField.SPAN_GROUP]: groupId, } as { - [SpanMetricsFields.SPAN_OP]: string; - [SpanMetricsFields.SPAN_DESCRIPTION]: string; - [SpanMetricsFields.SPAN_ACTION]: string; - [SpanMetricsFields.SPAN_DOMAIN]: string; - [SpanMetricsFields.SPAN_GROUP]: string; + [SpanMetricsField.SPAN_OP]: string; + [SpanMetricsField.SPAN_DESCRIPTION]: string; + [SpanMetricsField.SPAN_ACTION]: string; + [SpanMetricsField.SPAN_DOMAIN]: string; + [SpanMetricsField.SPAN_GROUP]: string; }; const {isLoading: areSpanMetricsSeriesLoading, data: spanMetricsSeriesData} = useSpanMetricsSeries( groupId, queryFilter, - [`avg(${SpanMetricsFields.SPAN_SELF_TIME})`, 'spm()', 'http_error_count()'], + [`avg(${SpanMetricsField.SPAN_SELF_TIME})`, 'spm()', 'http_error_count()'], 'api.starfish.span-summary-page-metrics-chart' ); useSynchronizeCharts([!areSpanMetricsSeriesLoading]); const spanMetricsThroughputSeries = { - seriesName: span?.[SpanMetricsFields.SPAN_OP]?.startsWith('db') + seriesName: span?.[SpanMetricsField.SPAN_OP]?.startsWith('db') ? 'Queries' : 'Requests', data: spanMetricsSeriesData?.['spm()'].data, @@ -103,14 +103,13 @@ export function SpanSummaryView({groupId}: Props) { - {span?.[SpanMetricsFields.SPAN_DESCRIPTION] && ( + {span?.[SpanMetricsField.SPAN_DESCRIPTION] && ( @@ -118,7 +117,7 @@ export function SpanSummaryView({groupId}: Props) { - + - {span?.[SpanMetricsFields.SPAN_OP]?.startsWith('http') && ( + {span?.[SpanMetricsField.SPAN_OP]?.startsWith('http') && ( ; endpoint?: string; endpointMethod?: string; @@ -63,15 +66,22 @@ export function SpanTransactionsTable({span, endpoint, endpointMethod, sort}: Pr const organization = useOrganization(); const router = useRouter(); + const cursor = decodeScalar(location.query?.[QueryParameterNames.ENDPOINTS_CURSOR]); + const { data: spanTransactionMetrics = [], meta, isLoading, pageLinks, - } = useSpanTransactionMetrics(span[SpanMetricsFields.SPAN_GROUP], { - transactions: endpoint ? [endpoint] : undefined, - sorts: [sort], - }); + } = useSpanTransactionMetrics( + span[SpanMetricsField.SPAN_GROUP], + { + transactions: endpoint ? [endpoint] : undefined, + sorts: [sort], + }, + undefined, + cursor + ); const spanTransactionsWithMetrics = spanTransactionMetrics.map(row => { return { @@ -89,7 +99,7 @@ export function SpanTransactionsTable({span, endpoint, endpointMethod, sort}: Pr const pathname = `${routingContext.baseURL}/${ extractRoute(location) ?? 'spans' - }/span/${encodeURIComponent(span[SpanMetricsFields.SPAN_GROUP])}`; + }/span/${encodeURIComponent(span[SpanMetricsField.SPAN_GROUP])}`; const query = { ...location.query, endpoint, @@ -127,6 +137,13 @@ export function SpanTransactionsTable({span, endpoint, endpointMethod, sort}: Pr return rendered; }; + const handleCursor: CursorHandler = (newCursor, pathname, query) => { + browserHistory.push({ + pathname, + query: {...query, [QueryParameterNames.ENDPOINTS_CURSOR]: newCursor}, + }); + }; + return ( )} - + ); @@ -165,7 +182,7 @@ export function SpanTransactionsTable({span, endpoint, endpointMethod, sort}: Pr const getColumnOrder = ( span: Pick< SpanIndexedFieldTypes, - SpanIndexedFields.SPAN_GROUP | SpanIndexedFields.SPAN_OP + SpanIndexedField.SPAN_GROUP | SpanIndexedField.SPAN_OP > ): TableColumnHeader[] => [ { @@ -175,11 +192,11 @@ const getColumnOrder = ( }, { key: 'spm()', - name: getThroughputTitle(span[SpanIndexedFields.SPAN_OP]), + name: getThroughputTitle(span[SpanIndexedField.SPAN_OP]), width: COL_WIDTH_UNDEFINED, }, { - key: `avg(${SpanMetricsFields.SPAN_SELF_TIME})`, + key: `avg(${SpanMetricsField.SPAN_SELF_TIME})`, name: DataTitles.avg, width: COL_WIDTH_UNDEFINED, }, diff --git a/static/app/views/starfish/views/spans/index.tsx b/static/app/views/starfish/views/spans/index.tsx index 181f5b8abcbaa4..5d7f752e753aea 100644 --- a/static/app/views/starfish/views/spans/index.tsx +++ b/static/app/views/starfish/views/spans/index.tsx @@ -12,13 +12,13 @@ import {useLocation} from 'sentry/utils/useLocation'; import StarfishDatePicker from 'sentry/views/starfish/components/datePicker'; import {StarfishPageFiltersContainer} from 'sentry/views/starfish/components/starfishPageFiltersContainer'; import {StarfishProjectSelector} from 'sentry/views/starfish/components/starfishProjectSelector'; -import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types'; +import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; import {ROUTE_NAMES} from 'sentry/views/starfish/utils/routeNames'; import {RoutingContextProvider} from 'sentry/views/starfish/utils/routingContext'; import SpansView from './spansView'; -const {SPAN_MODULE} = SpanMetricsFields; +const {SPAN_MODULE} = SpanMetricsField; type Query = { 'span.category'?: string; diff --git a/static/app/views/starfish/views/spans/selectors/actionSelector.tsx b/static/app/views/starfish/views/spans/selectors/actionSelector.tsx index c19d5c9cad2adc..fc014787db6232 100644 --- a/static/app/views/starfish/views/spans/selectors/actionSelector.tsx +++ b/static/app/views/starfish/views/spans/selectors/actionSelector.tsx @@ -8,7 +8,7 @@ import {t} from 'sentry/locale'; import EventView from 'sentry/utils/discover/eventView'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {useLocation} from 'sentry/utils/useLocation'; -import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types'; +import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; import {buildEventViewQuery} from 'sentry/views/starfish/utils/buildEventViewQuery'; import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery'; import { @@ -16,7 +16,7 @@ import { EmptyContainer, } from 'sentry/views/starfish/views/spans/selectors/emptyOption'; -const {SPAN_ACTION} = SpanMetricsFields; +const {SPAN_ACTION} = SpanMetricsField; type Props = { moduleName?: ModuleName; diff --git a/static/app/views/starfish/views/spans/selectors/domainSelector.tsx b/static/app/views/starfish/views/spans/selectors/domainSelector.tsx index 252fc583a8810d..073e03d1e4ebca 100644 --- a/static/app/views/starfish/views/spans/selectors/domainSelector.tsx +++ b/static/app/views/starfish/views/spans/selectors/domainSelector.tsx @@ -9,7 +9,7 @@ import {t} from 'sentry/locale'; import EventView from 'sentry/utils/discover/eventView'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {useLocation} from 'sentry/utils/useLocation'; -import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types'; +import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; import {buildEventViewQuery} from 'sentry/views/starfish/utils/buildEventViewQuery'; import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery'; import { @@ -17,7 +17,7 @@ import { EmptyContainer, } from 'sentry/views/starfish/views/spans/selectors/emptyOption'; -const {SPAN_DOMAIN} = SpanMetricsFields; +const {SPAN_DOMAIN} = SpanMetricsField; type Props = { moduleName?: ModuleName; diff --git a/static/app/views/starfish/views/spans/selectors/spanOperationSelector.tsx b/static/app/views/starfish/views/spans/selectors/spanOperationSelector.tsx index 8c902b3aa6c3c0..8ffc1716478b7c 100644 --- a/static/app/views/starfish/views/spans/selectors/spanOperationSelector.tsx +++ b/static/app/views/starfish/views/spans/selectors/spanOperationSelector.tsx @@ -7,7 +7,7 @@ import {t} from 'sentry/locale'; import EventView from 'sentry/utils/discover/eventView'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {useLocation} from 'sentry/utils/useLocation'; -import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types'; +import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; import {buildEventViewQuery} from 'sentry/views/starfish/utils/buildEventViewQuery'; import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery'; import { @@ -15,7 +15,7 @@ import { EMPTY_OPTION_VALUE, } from 'sentry/views/starfish/views/spans/selectors/emptyOption'; -const {SPAN_OP} = SpanMetricsFields; +const {SPAN_OP} = SpanMetricsField; type Props = { value: string; diff --git a/static/app/views/starfish/views/spans/spanTimeCharts.tsx b/static/app/views/starfish/views/spans/spanTimeCharts.tsx index cd7ec1600b4dfc..152e946df1540c 100644 --- a/static/app/views/starfish/views/spans/spanTimeCharts.tsx +++ b/static/app/views/starfish/views/spans/spanTimeCharts.tsx @@ -12,7 +12,7 @@ import usePageFilters from 'sentry/utils/usePageFilters'; import {AVG_COLOR, ERRORS_COLOR, THROUGHPUT_COLOR} from 'sentry/views/starfish/colours'; import Chart, {useSynchronizeCharts} from 'sentry/views/starfish/components/chart'; import ChartPanel from 'sentry/views/starfish/components/chartPanel'; -import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types'; +import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; import {STARFISH_CHART_INTERVAL_FIDELITY} from 'sentry/views/starfish/utils/constants'; import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery'; import {useErrorRateQuery as useErrorCountQuery} from 'sentry/views/starfish/views/spans/queries'; @@ -24,7 +24,7 @@ import { import {ModuleFilters} from 'sentry/views/starfish/views/spans/useModuleFilters'; import {NULL_SPAN_CATEGORY} from 'sentry/views/starfish/views/webServiceView/spanGroupBreakdownContainer'; -const {SPAN_SELF_TIME, SPAN_MODULE, SPAN_DESCRIPTION} = SpanMetricsFields; +const {SPAN_SELF_TIME, SPAN_MODULE, SPAN_DESCRIPTION} = SpanMetricsField; const CHART_HEIGHT = 140; diff --git a/static/app/views/starfish/views/spans/spansTable.tsx b/static/app/views/starfish/views/spans/spansTable.tsx index 91fc5be2c4c4fb..69fe01d083d332 100644 --- a/static/app/views/starfish/views/spans/spansTable.tsx +++ b/static/app/views/starfish/views/spans/spansTable.tsx @@ -18,7 +18,7 @@ import useOrganization from 'sentry/utils/useOrganization'; import {renderHeadCell} from 'sentry/views/starfish/components/tableCells/renderHeadCell'; import {SpanDescriptionCell} from 'sentry/views/starfish/components/tableCells/spanDescriptionCell'; import {useSpanList} from 'sentry/views/starfish/queries/useSpanList'; -import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types'; +import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters'; import {DataTitles, getThroughputTitle} from 'sentry/views/starfish/views/spans/types'; import type {ValidSort} from 'sentry/views/starfish/views/spans/useModuleSort'; @@ -48,7 +48,7 @@ type Props = { }; const {SPAN_SELF_TIME, SPAN_DESCRIPTION, SPAN_DOMAIN, SPAN_GROUP, SPAN_OP, PROJECT_ID} = - SpanMetricsFields; + SpanMetricsField; export default function SpansTable({ moduleName, @@ -62,7 +62,7 @@ export default function SpansTable({ const location = useLocation(); const organization = useOrganization(); - const spansCursor = decodeScalar(location.query?.[QueryParameterNames.CURSOR]); + const cursor = decodeScalar(location.query?.[QueryParameterNames.SPANS_CURSOR]); const {isLoading, data, meta, pageLinks} = useSpanList( moduleName ?? ModuleName.ALL, @@ -72,13 +72,13 @@ export default function SpansTable({ [sort], limit, 'api.starfish.use-span-list', - spansCursor + cursor ); - const handleCursor: CursorHandler = (cursor, pathname, query) => { + const handleCursor: CursorHandler = (newCursor, pathname, query) => { browserHistory.push({ pathname, - query: {...query, [QueryParameterNames.CURSOR]: cursor}, + query: {...query, [QueryParameterNames.SPANS_CURSOR]: newCursor}, }); }; diff --git a/static/app/views/starfish/views/spans/spansView.tsx b/static/app/views/starfish/views/spans/spansView.tsx index 7b32436d8dc07d..0c6e443ebfec41 100644 --- a/static/app/views/starfish/views/spans/spansView.tsx +++ b/static/app/views/starfish/views/spans/spansView.tsx @@ -2,7 +2,7 @@ import {Fragment} from 'react'; import styled from '@emotion/styled'; import {space} from 'sentry/styles/space'; -import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types'; +import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; import {ActionSelector} from 'sentry/views/starfish/views/spans/selectors/actionSelector'; import {DomainSelector} from 'sentry/views/starfish/views/spans/selectors/domainSelector'; import {SpanOperationSelector} from 'sentry/views/starfish/views/spans/selectors/spanOperationSelector'; @@ -12,7 +12,7 @@ import {useModuleSort} from 'sentry/views/starfish/views/spans/useModuleSort'; import SpansTable from './spansTable'; -const {SPAN_ACTION, SPAN_DOMAIN, SPAN_OP} = SpanMetricsFields; +const {SPAN_ACTION, SPAN_DOMAIN, SPAN_OP} = SpanMetricsField; const LIMIT: number = 25; diff --git a/static/app/views/starfish/views/spans/useModuleFilters.ts b/static/app/views/starfish/views/spans/useModuleFilters.ts index fb7ae045b0d756..d267476faa2883 100644 --- a/static/app/views/starfish/views/spans/useModuleFilters.ts +++ b/static/app/views/starfish/views/spans/useModuleFilters.ts @@ -1,22 +1,22 @@ import pick from 'lodash/pick'; import {useLocation} from 'sentry/utils/useLocation'; -import {SpanMetricsFields} from 'sentry/views/starfish/types'; +import {SpanMetricsField} from 'sentry/views/starfish/types'; export type ModuleFilters = { - [SpanMetricsFields.SPAN_ACTION]?: string; - [SpanMetricsFields.SPAN_DOMAIN]?: string; - [SpanMetricsFields.SPAN_GROUP]?: string; - [SpanMetricsFields.SPAN_OP]?: string; + [SpanMetricsField.SPAN_ACTION]?: string; + [SpanMetricsField.SPAN_DOMAIN]?: string; + [SpanMetricsField.SPAN_GROUP]?: string; + [SpanMetricsField.SPAN_OP]?: string; }; export const useModuleFilters = () => { const location = useLocation(); return pick(location.query, [ - SpanMetricsFields.SPAN_ACTION, - SpanMetricsFields.SPAN_DOMAIN, - SpanMetricsFields.SPAN_OP, - SpanMetricsFields.SPAN_GROUP, + SpanMetricsField.SPAN_ACTION, + SpanMetricsField.SPAN_DOMAIN, + SpanMetricsField.SPAN_OP, + SpanMetricsField.SPAN_GROUP, ]); }; diff --git a/static/app/views/starfish/views/spans/useModuleSort.ts b/static/app/views/starfish/views/spans/useModuleSort.ts index 2bf5a0595ab9c7..7ed8ae1431293f 100644 --- a/static/app/views/starfish/views/spans/useModuleSort.ts +++ b/static/app/views/starfish/views/spans/useModuleSort.ts @@ -1,19 +1,19 @@ import {fromSorts} from 'sentry/utils/discover/eventView'; import type {Sort} from 'sentry/utils/discover/fields'; import {useLocation} from 'sentry/utils/useLocation'; -import {SpanMetricsFields, StarfishFunctions} from 'sentry/views/starfish/types'; +import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types'; import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters'; type Query = { - [QueryParameterNames.SORT]: string; + [QueryParameterNames.SPANS_SORT]: string; }; const SORTABLE_FIELDS = [ - `avg(${SpanMetricsFields.SPAN_SELF_TIME})`, - `${StarfishFunctions.HTTP_ERROR_COUNT}()`, - `${StarfishFunctions.SPM}()`, - `${StarfishFunctions.TIME_SPENT_PERCENTAGE}()`, - `${StarfishFunctions.TIME_SPENT_PERCENTAGE}(local)`, + `avg(${SpanMetricsField.SPAN_SELF_TIME})`, + `${SpanFunction.HTTP_ERROR_COUNT}()`, + `${SpanFunction.SPM}()`, + `${SpanFunction.TIME_SPENT_PERCENTAGE}()`, + `${SpanFunction.TIME_SPENT_PERCENTAGE}(local)`, ] as const; export type ValidSort = Sort & { @@ -28,14 +28,14 @@ export function useModuleSort(fallback: Sort = DEFAULT_SORT) { const location = useLocation(); return ( - fromSorts(location.query[QueryParameterNames.SORT]).filter(isAValidSort)[0] ?? + fromSorts(location.query[QueryParameterNames.SPANS_SORT]).filter(isAValidSort)[0] ?? fallback ); } const DEFAULT_SORT: Sort = { kind: 'desc', - field: `${StarfishFunctions.TIME_SPENT_PERCENTAGE}()`, + field: `${SpanFunction.TIME_SPENT_PERCENTAGE}()`, }; function isAValidSort(sort: Sort): sort is ValidSort { diff --git a/static/app/views/starfish/views/webServiceView/spanGroupBreakdown.tsx b/static/app/views/starfish/views/webServiceView/spanGroupBreakdown.tsx index 6ad043c907b4cd..d87e0d1fc0d224 100644 --- a/static/app/views/starfish/views/webServiceView/spanGroupBreakdown.tsx +++ b/static/app/views/starfish/views/webServiceView/spanGroupBreakdown.tsx @@ -13,14 +13,14 @@ import {tooltipFormatterUsingAggregateOutputType} from 'sentry/utils/discover/ch import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry'; import useOrganization from 'sentry/utils/useOrganization'; import Chart from 'sentry/views/starfish/components/chart'; -import {SpanMetricsFields} from 'sentry/views/starfish/types'; +import {SpanMetricsField} from 'sentry/views/starfish/types'; import {useRoutingContext} from 'sentry/views/starfish/utils/routingContext'; import { DataDisplayType, DataRow, } from 'sentry/views/starfish/views/webServiceView/spanGroupBreakdownContainer'; -const {SPAN_MODULE} = SpanMetricsFields; +const {SPAN_MODULE} = SpanMetricsField; type Props = { colorPalette: string[]; diff --git a/static/app/views/starfish/views/webServiceView/spanGroupBreakdownContainer.tsx b/static/app/views/starfish/views/webServiceView/spanGroupBreakdownContainer.tsx index 9ce90e08be1661..ee25739ab4103b 100644 --- a/static/app/views/starfish/views/webServiceView/spanGroupBreakdownContainer.tsx +++ b/static/app/views/starfish/views/webServiceView/spanGroupBreakdownContainer.tsx @@ -16,12 +16,12 @@ import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; -import {SpanMetricsFields} from 'sentry/views/starfish/types'; +import {SpanMetricsField} from 'sentry/views/starfish/types'; import {STARFISH_CHART_INTERVAL_FIDELITY} from 'sentry/views/starfish/utils/constants'; import {useEventsStatsQuery} from 'sentry/views/starfish/utils/useEventsStatsQuery'; import {SpanGroupBreakdown} from 'sentry/views/starfish/views/webServiceView/spanGroupBreakdown'; -const {SPAN_SELF_TIME} = SpanMetricsFields; +const {SPAN_SELF_TIME} = SpanMetricsField; const OTHER_SPAN_GROUP_MODULE = 'Other'; export const NULL_SPAN_CATEGORY = t('custom'); diff --git a/tests/sentry/api/endpoints/test_organization_details.py b/tests/sentry/api/endpoints/test_organization_details.py index 81489c4e99a646..e75263a1bd3c59 100644 --- a/tests/sentry/api/endpoints/test_organization_details.py +++ b/tests/sentry/api/endpoints/test_organization_details.py @@ -38,6 +38,7 @@ from sentry.signals import project_created from sentry.silo import SiloMode, unguarded_write from sentry.testutils.cases import APITestCase, TwoFactorAPITestCase +from sentry.testutils.helpers import override_options from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode, region_silo_test from sentry.utils import json @@ -352,7 +353,7 @@ def test_invalid_slugs(self): self.get_error_response(self.organization.slug, slug="canada-", status_code=400) self.get_error_response(self.organization.slug, slug="-canada", status_code=400) self.get_error_response(self.organization.slug, slug="----", status_code=400) - with self.feature("app:enterprise-prevent-numeric-slugs"): + with override_options({"api.prevent-numeric-slugs": True}): self.get_error_response(self.organization.slug, slug="1234", status_code=400) def test_upload_avatar(self): diff --git a/tests/sentry/api/endpoints/test_organization_events_trends_v2.py b/tests/sentry/api/endpoints/test_organization_events_trends_v2.py index 29fc61ef68f260..70e5fc24b9c1d4 100644 --- a/tests/sentry/api/endpoints/test_organization_events_trends_v2.py +++ b/tests/sentry/api/endpoints/test_organization_events_trends_v2.py @@ -6,7 +6,7 @@ from django.urls import reverse from freezegun import freeze_time -from sentry.issues.grouptype import PerformanceP95TransactionDurationRegressionGroupType +from sentry.issues.grouptype import PerformanceDurationRegressionGroupType from sentry.snuba.metrics.naming_layer import TransactionMRI from sentry.testutils.cases import MetricsAPIBaseTestCase from sentry.testutils.helpers.datetime import iso_format @@ -535,7 +535,7 @@ def test_issue_creation_simple(self, mock_get_trends, mock_produce_occurrence_to }, {"name": "Transaction", "value": "foo", "important": True}, ], - "type": PerformanceP95TransactionDurationRegressionGroupType.type_id, + "type": PerformanceDurationRegressionGroupType.type_id, "level": "info", "culprit": "foo", }, diff --git a/tests/sentry/api/endpoints/test_organization_index.py b/tests/sentry/api/endpoints/test_organization_index.py index d846251d7cb94d..a7ff3995f08d5e 100644 --- a/tests/sentry/api/endpoints/test_organization_index.py +++ b/tests/sentry/api/endpoints/test_organization_index.py @@ -8,7 +8,7 @@ from sentry.models import Authenticator, Organization, OrganizationMember, OrganizationStatus from sentry.silo import SiloMode from sentry.testutils.cases import APITestCase, TwoFactorAPITestCase -from sentry.testutils.helpers.features import with_feature +from sentry.testutils.helpers.options import override_options from sentry.testutils.hybrid_cloud import HybridCloudTestMixin from sentry.testutils.silo import assume_test_silo_mode, region_silo_test @@ -139,7 +139,7 @@ def test_invalid_slugs(self): self.get_error_response(name="name", slug="canada-", status_code=400) self.get_error_response(name="name", slug="-canada", status_code=400) self.get_error_response(name="name", slug="----", status_code=400) - with self.feature("app:enterprise-prevent-numeric-slugs"): + with override_options({"api.prevent-numeric-slugs": True}): self.get_error_response(name="name", slug="1234", status_code=400) def test_without_slug(self): @@ -149,7 +149,7 @@ def test_without_slug(self): org = Organization.objects.get(id=organization_id) assert org.slug == "hello-world" - @with_feature("app:enterprise-prevent-numeric-slugs") + @override_options({"api.prevent-numeric-slugs": True}) def test_generated_slug_not_entirely_numeric(self): response = self.get_success_response(name="1234") diff --git a/tests/sentry/api/endpoints/test_organization_project_slugs.py b/tests/sentry/api/endpoints/test_organization_project_slugs.py index 8a6db848d3ad11..ce4eceda7ef0eb 100644 --- a/tests/sentry/api/endpoints/test_organization_project_slugs.py +++ b/tests/sentry/api/endpoints/test_organization_project_slugs.py @@ -1,6 +1,6 @@ from fixtures.apidocs_test_case import APIDocsTestCase from sentry.models.project import Project -from sentry.testutils.helpers.features import with_feature +from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import region_silo_test @@ -30,7 +30,7 @@ def test_updates_project_slugs(self): str(project_two.id): "new-two", } - @with_feature("app:enterprise-prevent-numeric-slugs") + @override_options({"api.prevent-numeric-slugs": True}) def test_invalid_numeric_slug(self): invalid_slugs = {**self.slugs, self.project_two.id: "1234"} response = self.get_error_response( diff --git a/tests/sentry/api/endpoints/test_organization_teams.py b/tests/sentry/api/endpoints/test_organization_teams.py index 728adf8d85c1d0..3b83041ef4991f 100644 --- a/tests/sentry/api/endpoints/test_organization_teams.py +++ b/tests/sentry/api/endpoints/test_organization_teams.py @@ -6,7 +6,7 @@ from sentry.models import OrganizationMember, OrganizationMemberTeam, Team from sentry.models.projectteam import ProjectTeam from sentry.testutils.cases import APITestCase -from sentry.testutils.helpers.features import with_feature +from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import region_silo_test from sentry.types.integrations import get_provider_string @@ -251,14 +251,14 @@ def test_name_too_long(self): self.organization.slug, name="x" * 65, slug="xxxxxxx", status_code=400 ) - @with_feature("app:enterprise-prevent-numeric-slugs") + @override_options({"api.prevent-numeric-slugs": True}) def test_invalid_numeric_slug(self): response = self.get_error_response( self.organization.slug, name="hello word", slug="1234", status_code=400 ) assert response.data["slug"][0] == DEFAULT_SLUG_ERROR_MESSAGE - @with_feature("app:enterprise-prevent-numeric-slugs") + @override_options({"api.prevent-numeric-slugs": True}) def test_generated_slug_not_entirely_numeric(self): response = self.get_success_response(self.organization.slug, name="1234", status_code=201) team = Team.objects.get(id=response.data["id"]) diff --git a/tests/sentry/api/endpoints/test_project_details.py b/tests/sentry/api/endpoints/test_project_details.py index 445ebb7322e2a1..71806d38af6b9d 100644 --- a/tests/sentry/api/endpoints/test_project_details.py +++ b/tests/sentry/api/endpoints/test_project_details.py @@ -35,6 +35,7 @@ from sentry.silo import SiloMode, unguarded_write from sentry.testutils.cases import APITestCase from sentry.testutils.helpers import Feature, with_feature +from sentry.testutils.helpers.options import override_options from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode, region_silo_test from sentry.types.integrations import ExternalProviders @@ -549,7 +550,7 @@ def test_invalid_slug(self): project = Project.objects.get(id=self.project.id) assert project.slug != new_project.slug - @with_feature("app:enterprise-prevent-numeric-slugs") + @override_options({"api.prevent-numeric-slugs": True}) def test_invalid_numeric_slug(self): response = self.get_error_response( self.org_slug, diff --git a/tests/sentry/api/endpoints/test_project_ownership.py b/tests/sentry/api/endpoints/test_project_ownership.py index cacd5ba6dd4658..d71bcbb79fd509 100644 --- a/tests/sentry/api/endpoints/test_project_ownership.py +++ b/tests/sentry/api/endpoints/test_project_ownership.py @@ -164,7 +164,7 @@ def test_audit_log_entry(self): with assume_test_silo_mode(SiloMode.CONTROL): auditlog = AuditLogEntry.objects.filter( organization_id=self.project.organization.id, - event=audit_log.get_event_id("PROJECT_EDIT"), + event=audit_log.get_event_id("PROJECT_OWNERSHIPRULE_EDIT"), target_object=self.project.id, ) assert len(auditlog) == 1 @@ -178,7 +178,7 @@ def test_audit_log_ownership_change(self): with assume_test_silo_mode(SiloMode.CONTROL): auditlog = AuditLogEntry.objects.filter( organization_id=self.project.organization.id, - event=audit_log.get_event_id("PROJECT_EDIT"), + event=audit_log.get_event_id("PROJECT_OWNERSHIPRULE_EDIT"), target_object=self.project.id, ) assert len(auditlog) == 1 diff --git a/tests/sentry/api/endpoints/test_project_rule_details.py b/tests/sentry/api/endpoints/test_project_rule_details.py index 1ea7e48d35be2c..16cbbfdde164de 100644 --- a/tests/sentry/api/endpoints/test_project_rule_details.py +++ b/tests/sentry/api/endpoints/test_project_rule_details.py @@ -454,6 +454,98 @@ def test_update_duplicate_rule(self): == f"This rule is an exact duplicate of '{rule.label}' in this project and may not be created." ) + def test_duplicate_rule_environment(self): + """Test that if one rule doesn't have an environment set (i.e. 'All Environments') and we compare it to a rule + that does have one set, we consider this when determining if it's a duplicate""" + conditions = [ + { + "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", + } + ] + actions = [ + { + "targetType": "IssueOwners", + "fallthroughType": "ActiveMembers", + "id": "sentry.mail.actions.NotifyEmailAction", + "targetIdentifier": "", + } + ] + self.create_project_rule( + project=self.project, action_match=actions, condition_match=conditions + ) + env_rule = self.create_project_rule( + project=self.project, action_match=actions, condition_match=conditions + ) + payload = { + "name": "hello world", + "actionMatch": "all", + "actions": actions, + "conditions": conditions, + } + resp = self.get_error_response( + self.organization.slug, + self.project.slug, + env_rule.id, + status_code=status.HTTP_400_BAD_REQUEST, + **payload, + ) + assert ( + resp.data["name"][0] + == f"This rule is an exact duplicate of '{env_rule.label}' in this project and may not be created." + ) + + # update env_rule to have an environment set - these should now be considered to be different + payload["environment"] = self.environment.name + resp = self.get_success_response( + self.organization.slug, + self.project.slug, + env_rule.id, + status_code=status.HTTP_200_OK, + **payload, + ) + + def test_duplicate_rule_actions(self): + """Test that if one rule doesn't have an action set (i.e. 'Do Nothing') and we compare it to a rule + that does have one set, we consider this when determining if it's a duplicate""" + + # XXX(CEO): After we migrate old data so that no rules have no actions, this test won't be needed + conditions = [ + { + "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition", + } + ] + actions = [ + { + "targetType": "IssueOwners", + "fallthroughType": "ActiveMembers", + "id": "sentry.mail.actions.NotifyEmailAction", + "targetIdentifier": "", + } + ] + Rule.objects.create( + project=self.project, + data={"conditions": conditions, "action_match": "all"}, + ) + action_rule = Rule.objects.create( + project=self.project, + data={"conditions": conditions, "action_match": "all"}, + ) + + payload = { + "name": "hello world", + "actionMatch": "all", + "actions": actions, + "conditions": conditions, + } + + self.get_success_response( + self.organization.slug, + self.project.slug, + action_rule.id, + status_code=status.HTTP_200_OK, + **payload, + ) + def test_edit_rule(self): """Test that you can edit an alert rule w/o it comparing it to itself as a dupe""" conditions = [ diff --git a/tests/sentry/api/endpoints/test_project_rules_configuration.py b/tests/sentry/api/endpoints/test_project_rules_configuration.py index 8b9a0a61b5a245..8ffdbbcc224dd1 100644 --- a/tests/sentry/api/endpoints/test_project_rules_configuration.py +++ b/tests/sentry/api/endpoints/test_project_rules_configuration.py @@ -169,3 +169,18 @@ def test_issue_type_and_category_filter_feature(self): filter_ids = {f["id"] for f in response.data["filters"]} assert IssueCategoryFilter.id in filter_ids + + def test_issue_severity_filter_feature(self): + # Hide the issue severity filter when issue-severity-alerts is off + with self.feature({"projects:first-event-severity-alerting": False}): + response = self.get_success_response(self.organization.slug, self.project.slug) + assert "sentry.rules.filters.issue_severity.IssueSeverityFilter" not in [ + filter["id"] for filter in response.data["filters"] + ] + + # Show the issue severity filter when issue-severity-alerts is on + with self.feature({"projects:first-event-severity-alerting": True}): + response = self.get_success_response(self.organization.slug, self.project.slug) + assert "sentry.rules.filters.issue_severity.IssueSeverityFilter" in [ + filter["id"] for filter in response.data["filters"] + ] diff --git a/tests/sentry/api/endpoints/test_relay_globalconfig_v4.py b/tests/sentry/api/endpoints/test_relay_globalconfig_v4.py index 9ba2ce97f7ee12..3e5668607596e7 100644 --- a/tests/sentry/api/endpoints/test_relay_globalconfig_v4.py +++ b/tests/sentry/api/endpoints/test_relay_globalconfig_v4.py @@ -1,6 +1,9 @@ +from unittest.mock import patch + import pytest from django.urls import reverse +from sentry.relay.globalconfig import get_global_config from sentry.testutils.pytest.fixtures import django_db_all from sentry.utils import json @@ -26,6 +29,41 @@ def inner(version, global_): return inner +def test_global_config(): + assert get_global_config() == { + "measurements": { + "builtinMeasurements": [ + {"name": "app_start_cold", "unit": "millisecond"}, + {"name": "app_start_warm", "unit": "millisecond"}, + {"name": "cls", "unit": "none"}, + {"name": "fcp", "unit": "millisecond"}, + {"name": "fid", "unit": "millisecond"}, + {"name": "fp", "unit": "millisecond"}, + {"name": "frames_frozen_rate", "unit": "ratio"}, + {"name": "frames_frozen", "unit": "none"}, + {"name": "frames_slow_rate", "unit": "ratio"}, + {"name": "frames_slow", "unit": "none"}, + {"name": "frames_total", "unit": "none"}, + {"name": "inp", "unit": "millisecond"}, + {"name": "lcp", "unit": "millisecond"}, + {"name": "stall_count", "unit": "none"}, + {"name": "stall_longest_time", "unit": "millisecond"}, + {"name": "stall_percentage", "unit": "ratio"}, + {"name": "stall_total_time", "unit": "millisecond"}, + {"name": "ttfb.requesttime", "unit": "millisecond"}, + {"name": "ttfb", "unit": "millisecond"}, + {"name": "time_to_full_display", "unit": "millisecond"}, + {"name": "time_to_initial_display", "unit": "millisecond"}, + ], + "maxCustomMeasurements": 10, + } + } + + +@patch( + "sentry.api.endpoints.relay.project_configs.get_global_config", + lambda *args, **kargs: {"global_mock_config": True}, +) @pytest.mark.parametrize( ("version, request_global_config, expect_global_config"), [ @@ -37,11 +75,14 @@ def inner(version, global_): ) @django_db_all def test_return_global_config_on_right_version( - call_endpoint, version, request_global_config, expect_global_config + call_endpoint, + version, + request_global_config, + expect_global_config, ): result, status_code = call_endpoint(version, request_global_config) assert status_code < 400 if not expect_global_config: assert "global" not in result else: - assert result.get("global") == {} + assert result.get("global") == {"global_mock_config": True} diff --git a/tests/sentry/api/endpoints/test_relay_projectconfigs_v4.py b/tests/sentry/api/endpoints/test_relay_projectconfigs_v4.py index 838fe825d53484..873b3c504a460a 100644 --- a/tests/sentry/api/endpoints/test_relay_projectconfigs_v4.py +++ b/tests/sentry/api/endpoints/test_relay_projectconfigs_v4.py @@ -54,6 +54,14 @@ def projectconfig_cache_get_mock_config(monkeypatch): ) +@pytest.fixture +def globalconfig_get_mock_config(monkeypatch): + monkeypatch.setattr( + "sentry.relay.globalconfig.get_global_config", + lambda *args, **kargs: {"global_mock_config": True}, + ) + + @pytest.fixture def single_mock_proj_cached(monkeypatch): def cache_get(*args, **kwargs): @@ -82,7 +90,10 @@ def project_config_get_mock(monkeypatch): @django_db_all def test_return_full_config_if_in_cache( - call_endpoint, default_projectkey, projectconfig_cache_get_mock_config + call_endpoint, + default_projectkey, + projectconfig_cache_get_mock_config, + globalconfig_get_mock_config, ): result, status_code = call_endpoint(full_config=True) assert status_code == 200 @@ -92,22 +103,28 @@ def test_return_full_config_if_in_cache( } +@patch( + "sentry.api.endpoints.relay.project_configs.get_global_config", + lambda *args, **kargs: {"global_mock_config": True}, +) @django_db_all def test_return_project_and_global_config( - call_endpoint, default_projectkey, projectconfig_cache_get_mock_config + call_endpoint, + default_projectkey, + projectconfig_cache_get_mock_config, ): result, status_code = call_endpoint(full_config=True, global_=True) assert status_code == 200 assert result == { "configs": {default_projectkey.public_key: {"is_mock_config": True}}, "pending": [], - "global": {}, + "global": {"global_mock_config": True}, } @django_db_all def test_proj_in_cache_and_another_pending( - call_endpoint, default_projectkey, single_mock_proj_cached + call_endpoint, default_projectkey, single_mock_proj_cached, globalconfig_get_mock_config ): result, status_code = call_endpoint( full_config=True, public_keys=["must_exist", default_projectkey.public_key] @@ -122,9 +139,7 @@ def test_proj_in_cache_and_another_pending( @patch("sentry.tasks.relay.build_project_config.delay") @django_db_all def test_enqueue_task_if_config_not_cached_not_queued( - schedule_mock, - call_endpoint, - default_projectkey, + schedule_mock, call_endpoint, default_projectkey, globalconfig_get_mock_config ): result, status_code = call_endpoint(full_config=True) assert status_code == 200 @@ -139,6 +154,7 @@ def test_debounce_task_if_proj_config_not_cached_already_enqueued( call_endpoint, default_projectkey, projectconfig_debounced_cache, + globalconfig_get_mock_config, ): result, status_code = call_endpoint(full_config=True) assert status_code == 200 @@ -149,9 +165,7 @@ def test_debounce_task_if_proj_config_not_cached_already_enqueued( @patch("sentry.relay.projectconfig_cache.backend.set_many") @django_db_all def test_task_writes_config_into_cache( - cache_set_many_mock, - default_projectkey, - project_config_get_mock, + cache_set_many_mock, default_projectkey, project_config_get_mock, globalconfig_get_mock_config ): build_project_config( public_key=default_projectkey.public_key, diff --git a/tests/sentry/api/endpoints/test_scim_team_details.py b/tests/sentry/api/endpoints/test_scim_team_details.py index fb97ca28b8ad87..a516087e3091ce 100644 --- a/tests/sentry/api/endpoints/test_scim_team_details.py +++ b/tests/sentry/api/endpoints/test_scim_team_details.py @@ -5,7 +5,7 @@ from sentry.models import OrganizationMemberTeam, Team, TeamStatus from sentry.testutils.cases import SCIMTestCase -from sentry.testutils.helpers.features import with_feature +from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import region_silo_test @@ -134,7 +134,7 @@ def test_scim_team_details_patch_rename_team(self, mock_metrics): "sentry.scim.team.update", tags={"organization": self.organization} ) - @with_feature("app:enterprise-prevent-numeric-slugs") + @override_options({"api.prevent-numeric-slugs": True}) def test_scim_team_details_patch_rename_team_invalid_slug(self): self.base_data["Operations"] = [ { diff --git a/tests/sentry/api/endpoints/test_scim_team_index.py b/tests/sentry/api/endpoints/test_scim_team_index.py index 5359e5b70c2ef0..aa22cce4185c07 100644 --- a/tests/sentry/api/endpoints/test_scim_team_index.py +++ b/tests/sentry/api/endpoints/test_scim_team_index.py @@ -6,7 +6,7 @@ from sentry.models import Team from sentry.signals import receivers_raise_on_send from sentry.testutils.cases import SCIMTestCase -from sentry.testutils.helpers.features import with_feature +from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import region_silo_test @@ -236,7 +236,7 @@ def test_scim_team_no_duplicate_names(self): ) assert response.data["detail"] == "A team with this slug already exists." - @with_feature("app:enterprise-prevent-numeric-slugs") + @override_options({"api.prevent-numeric-slugs": True}) def test_scim_team_invalid_numeric_slug(self): invalid_post_data = {**self.post_data, "displayName": "1234"} response = self.get_error_response( diff --git a/tests/sentry/api/endpoints/test_team_details.py b/tests/sentry/api/endpoints/test_team_details.py index fca1b12b576eb6..444f5f9b8c77c2 100644 --- a/tests/sentry/api/endpoints/test_team_details.py +++ b/tests/sentry/api/endpoints/test_team_details.py @@ -5,6 +5,7 @@ from sentry.testutils.asserts import assert_org_audit_log_exists from sentry.testutils.cases import APITestCase from sentry.testutils.helpers import with_feature +from sentry.testutils.helpers.options import override_options from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode, region_silo_test @@ -84,7 +85,7 @@ def test_simple(self): assert team.name == "hello world" assert team.slug == "foobar" - @with_feature("app:enterprise-prevent-numeric-slugs") + @override_options({"api.prevent-numeric-slugs": True}) def test_invalid_numeric_slug(self): response = self.get_error_response(self.organization.slug, self.team.slug, slug="1234") assert response.data["slug"][0] == ( diff --git a/tests/sentry/api/endpoints/test_team_projects.py b/tests/sentry/api/endpoints/test_team_projects.py index bd54a364ea4075..25a699e6b34c92 100644 --- a/tests/sentry/api/endpoints/test_team_projects.py +++ b/tests/sentry/api/endpoints/test_team_projects.py @@ -3,6 +3,7 @@ from sentry.notifications.types import FallthroughChoiceType from sentry.testutils.cases import APITestCase from sentry.testutils.helpers import with_feature +from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import region_silo_test @@ -62,7 +63,7 @@ def test_simple(self): assert project.platform == "python" assert project.teams.first() == self.team - @with_feature("app:enterprise-prevent-numeric-slugs") + @override_options({"api.prevent-numeric-slugs": True}) def test_invalid_numeric_slug(self): response = self.get_error_response( self.organization.slug, @@ -74,7 +75,7 @@ def test_invalid_numeric_slug(self): assert response.data["slug"][0] == DEFAULT_SLUG_ERROR_MESSAGE - @with_feature("app:enterprise-prevent-numeric-slugs") + @override_options({"api.prevent-numeric-slugs": True}) def test_generated_slug_not_entirely_numeric(self): response = self.get_success_response( self.organization.slug, diff --git a/tests/sentry/api/endpoints/test_user_authenticator_details.py b/tests/sentry/api/endpoints/test_user_authenticator_details.py index de9d39c7dac660..a87af5d7d829bf 100644 --- a/tests/sentry/api/endpoints/test_user_authenticator_details.py +++ b/tests/sentry/api/endpoints/test_user_authenticator_details.py @@ -351,22 +351,26 @@ def test_require_2fa__can_delete_last_auth_superuser(self): superuser = self.create_user(email="a@example.com", is_superuser=True) self.login_as(user=superuser, superuser=True) - # enroll in one auth method - interface = TotpInterface() - interface.enroll(self.user) - assert interface.authenticator is not None - auth = interface.authenticator + new_options = settings.SENTRY_OPTIONS.copy() + new_options["sms.twilio-account"] = "twilio-account" - with self.tasks(): - self.get_success_response( - self.user.id, - auth.id, - method="delete", - status_code=status.HTTP_204_NO_CONTENT, - ) - assert_security_email_sent("mfa-removed") + with self.settings(SENTRY_OPTIONS=new_options): + # enroll in one auth method + interface = TotpInterface() + interface.enroll(self.user) + assert interface.authenticator is not None + auth = interface.authenticator - assert not Authenticator.objects.filter(id=auth.id).exists() + with self.tasks(): + self.get_success_response( + self.user.id, + auth.id, + method="delete", + status_code=status.HTTP_204_NO_CONTENT, + ) + assert_security_email_sent("mfa-removed") + + assert not Authenticator.objects.filter(id=auth.id).exists() def test_require_2fa__delete_with_multiple_auth__ok(self): self._require_2fa_for_organization() diff --git a/tests/sentry/audit_log/test_register.py b/tests/sentry/audit_log/test_register.py index 90ac181c534181..7e2031c3063d38 100644 --- a/tests/sentry/audit_log/test_register.py +++ b/tests/sentry/audit_log/test_register.py @@ -31,6 +31,7 @@ def test_get_api_names(self): "project.accept-transfer", "project.enable", "project.disable", + "project.ownership-rule.edit", "tagkey.remove", "projectkey.create", "projectkey.edit", diff --git a/tests/sentry/backup/test_exports.py b/tests/sentry/backup/test_exports.py new file mode 100644 index 00000000000000..ff15e340d0713e --- /dev/null +++ b/tests/sentry/backup/test_exports.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import tempfile +from pathlib import Path + +from sentry.backup.helpers import get_exportable_final_derivations_of +from sentry.backup.scopes import ExportScope +from sentry.db.models.base import BaseModel +from sentry.testutils.helpers.backups import BackupTestCase, export_to_file +from tests.sentry.backup import run_backup_tests_only_on_single_db + + +@run_backup_tests_only_on_single_db +class ScopingTests(BackupTestCase): + """ + Ensures that only models with the allowed relocation scopes are actually exported. + """ + + @staticmethod + def get_models_for_scope(scope: ExportScope) -> set[str]: + matching_models = set() + for model in get_exportable_final_derivations_of(BaseModel): + if model.__relocation_scope__ in scope.value: + obj_name = model._meta.object_name + if obj_name is not None: + matching_models.add("sentry." + obj_name.lower()) + return matching_models + + def test_user_export_scoping(self): + with tempfile.TemporaryDirectory() as tmp_dir: + matching_models = self.get_models_for_scope(ExportScope.User) + self.create_exhaustive_instance(is_superadmin=True) + tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json") + data = export_to_file(tmp_path, ExportScope.User) + + for entry in data: + model_name = entry["model"] + if model_name not in matching_models: + raise AssertionError( + f"Model `${model_name}` was included in export despite not being `Relocation.User`" + ) + + def test_organization_export_scoping(self): + with tempfile.TemporaryDirectory() as tmp_dir: + matching_models = self.get_models_for_scope(ExportScope.Organization) + self.create_exhaustive_instance(is_superadmin=True) + tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json") + data = export_to_file(tmp_path, ExportScope.Organization) + + for entry in data: + model_name = entry["model"] + if model_name not in matching_models: + raise AssertionError( + f"Model `${model_name}` was included in export despite not being `Relocation.User` or `Relocation.Organization`" + ) diff --git a/tests/sentry/backup/test_imports.py b/tests/sentry/backup/test_imports.py new file mode 100644 index 00000000000000..aa37ccd600a327 --- /dev/null +++ b/tests/sentry/backup/test_imports.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +import tempfile +from copy import deepcopy +from functools import cached_property +from pathlib import Path + +import pytest +from django.db import IntegrityError + +from sentry.backup.helpers import get_exportable_final_derivations_of +from sentry.backup.imports import ( + import_in_global_scope, + import_in_organization_scope, + import_in_user_scope, +) +from sentry.backup.scopes import RelocationScope +from sentry.db.models.base import BaseModel +from sentry.models.user import User +from sentry.models.userpermission import UserPermission +from sentry.models.userrole import UserRole, UserRoleUser +from sentry.testutils.factories import get_fixture_path +from sentry.testutils.helpers.backups import ( + NOOP_PRINTER, + BackupTestCase, + clear_database_but_keep_sequences, +) +from sentry.utils import json +from tests.sentry.backup import run_backup_tests_only_on_single_db + + +@run_backup_tests_only_on_single_db +class SanitizationTests(BackupTestCase): + """ + Ensure that potentially damaging data is properly scrubbed at import time. + """ + + @cached_property + def json_of_exhaustive_user_with_maximum_privileges(self) -> json.JSONData: + with open(get_fixture_path("backup", "user-with-maximum-privileges.json")) as backup_file: + return json.load(backup_file) + + @cached_property + def json_of_exhaustive_user_with_minimum_privileges(self) -> json.JSONData: + with open(get_fixture_path("backup", "user-with-minimum-privileges.json")) as backup_file: + return json.load(backup_file) + + @staticmethod + def copy_user(exhaustive_user: json.JSONData, username: str) -> json.JSONData: + user = deepcopy(exhaustive_user) + + for model in user: + if model["model"] == "sentry.user": + model["fields"]["username"] = username + + return user + + def generate_tmp_json_file(self, tmp_path) -> json.JSONData: + """ + Generates a file filled with users with different combinations of admin privileges. + """ + + # A user with the maximal amount of "evil" settings. + max_user = self.copy_user(self.json_of_exhaustive_user_with_maximum_privileges, "max_user") + + # A user with no "evil" settings. + min_user = self.copy_user(self.json_of_exhaustive_user_with_minimum_privileges, "min_user") + + # A copy of the `min_user`, but with a maximal `UserPermissions` attached. + permission_user = self.copy_user(min_user, "permission_user") + deepcopy( + list(filter(lambda mod: mod["model"] == "sentry.userpermission", max_user)) + ) + + # A copy of the `min_user`, but with all of the "evil" flags set to `True`. + superadmin_user = self.copy_user(min_user, "superadmin_user") + for model in superadmin_user: + if model["model"] == "sentry.user": + model["fields"]["is_staff"] = True + model["fields"]["is_superuser"] = True + + data = max_user + min_user + permission_user + superadmin_user + with open(tmp_path, "w+") as tmp_file: + json.dump(data, tmp_file) + + def test_user_sanitized_in_user_scope(self): + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json") + self.generate_tmp_json_file(tmp_path) + with open(tmp_path) as tmp_file: + import_in_user_scope(tmp_file, NOOP_PRINTER) + + assert User.objects.count() == 4 + assert User.objects.filter(is_staff=False, is_superuser=False).count() == 4 + + assert User.objects.filter(is_staff=True).count() == 0 + assert User.objects.filter(is_superuser=True).count() == 0 + assert UserPermission.objects.count() == 0 + assert UserRole.objects.count() == 0 + assert UserRoleUser.objects.count() == 0 + + def test_user_sanitized_in_organization_scope(self): + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json") + self.generate_tmp_json_file(tmp_path) + with open(tmp_path) as tmp_file: + import_in_organization_scope(tmp_file, NOOP_PRINTER) + + assert User.objects.count() == 4 + assert User.objects.filter(is_staff=False, is_superuser=False).count() == 4 + + assert User.objects.filter(is_staff=True).count() == 0 + assert User.objects.filter(is_superuser=True).count() == 0 + assert UserPermission.objects.count() == 0 + assert UserRole.objects.count() == 0 + assert UserRoleUser.objects.count() == 0 + + def test_users_sanitized_in_global_scope(self): + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json") + self.generate_tmp_json_file(tmp_path) + with open(tmp_path) as tmp_file: + import_in_global_scope(tmp_file, NOOP_PRINTER) + + assert User.objects.count() == 4 + assert User.objects.filter(is_staff=True).count() == 2 + assert User.objects.filter(is_superuser=True).count() == 2 + assert User.objects.filter(is_staff=False, is_superuser=False).count() == 2 + + # 1 from `max_user`, 1 from `permission_user`. + assert UserPermission.objects.count() == 2 + + # 1 from `max_user`. + assert UserRole.objects.count() == 1 + assert UserRoleUser.objects.count() == 1 + + # TODO(getsentry/team-ospo#181): Should fix this behavior to handle duplicate + def test_bad_already_taken_username(self): + with tempfile.TemporaryDirectory() as tmp_dir: + self.create_user("testing@example.com") + tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json") + with open(tmp_path, "w+") as tmp_file: + json.dump(self.json_of_exhaustive_user_with_minimum_privileges, tmp_file) + + with open(tmp_path) as tmp_file: + with pytest.raises(IntegrityError): + import_in_user_scope(tmp_file, NOOP_PRINTER) + + +@run_backup_tests_only_on_single_db +class ScopingTests(BackupTestCase): + """ + Ensures that only models with the allowed relocation scopes are actually imported. + """ + + def test_user_import_scoping(self): + with tempfile.TemporaryDirectory() as tmp_dir: + self.create_exhaustive_instance(is_superadmin=True) + data = self.import_export_then_validate(self._testMethodName, reset_pks=True) + tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json") + with open(tmp_path, "w+") as tmp_file: + json.dump(data, tmp_file) + + clear_database_but_keep_sequences() + with open(tmp_path) as tmp_file: + import_in_user_scope(tmp_file, NOOP_PRINTER) + for model in get_exportable_final_derivations_of(BaseModel): + if model.__relocation_scope__ != RelocationScope.User: + assert model.objects.count() == 0 + + def test_organization_import_scoping(self): + with tempfile.TemporaryDirectory() as tmp_dir: + self.create_exhaustive_instance(is_superadmin=True) + data = self.import_export_then_validate(self._testMethodName, reset_pks=True) + tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json") + with open(tmp_path, "w+") as tmp_file: + json.dump(data, tmp_file) + + clear_database_but_keep_sequences() + with open(tmp_path) as tmp_file: + import_in_organization_scope(tmp_file, NOOP_PRINTER) + for model in get_exportable_final_derivations_of(BaseModel): + if model.__relocation_scope__ not in { + RelocationScope.User, + RelocationScope.Organization, + }: + assert model.objects.count() == 0 diff --git a/tests/sentry/db/models/test_utils.py b/tests/sentry/db/models/test_utils.py index 6b8d6b9f7eed84..6a9b07a85fc532 100644 --- a/tests/sentry/db/models/test_utils.py +++ b/tests/sentry/db/models/test_utils.py @@ -1,7 +1,7 @@ from sentry.db.models.utils import slugify_instance from sentry.models import Organization from sentry.testutils.cases import TestCase -from sentry.testutils.helpers.features import with_feature +from sentry.testutils.helpers.options import override_options class SlugifyInstanceTest(TestCase): @@ -27,7 +27,7 @@ def test_max_length(self): slugify_instance(org, org.name, max_length=2) assert org.slug == "ma" - @with_feature("app:enterprise-prevent-numeric-slugs") + @override_options({"api.prevent-numeric-slugs": True}) def test_appends_to_entirely_numeric(self): org = Organization(name="1234") slugify_instance(org, org.name) diff --git a/tests/sentry/integrations/slack/test_disable.py b/tests/sentry/integrations/slack/test_disable.py index 29d78835af8f2f..b2beadbe88ed58 100644 --- a/tests/sentry/integrations/slack/test_disable.py +++ b/tests/sentry/integrations/slack/test_disable.py @@ -15,7 +15,6 @@ from sentry.models import AuditLogEntry, Integration from sentry.shared_integrations.exceptions import ApiError from sentry.testutils.cases import TestCase -from sentry.testutils.helpers import with_feature from sentry.utils import json control_address = "http://controlserver" @@ -49,10 +48,9 @@ def tearDown(self): self.resp.__exit__(None, None, None) @responses.activate - @with_feature("organizations:slack-fatal-disable-on-broken") def test_fatal_and_disable_integration(self): """ - fatal fast shut off with disable flag on, integration should be broken and disabled + fatal fast shut off integration should be broken and disabled """ bodydict = {"ok": False, "error": "account_inactive"} @@ -98,27 +96,6 @@ def test_email(self): in msg.body ) - @responses.activate - def test_fatal_integration(self): - """ - fatal fast shut off with disable flag off, integration should be broken but not disabled - """ - bodydict = {"ok": False, "error": "account_inactive"} - self.resp.add( - method=responses.POST, - url="https://slack.com/api/chat.postMessage", - status=200, - content_type="application/json", - body=json.dumps(bodydict), - ) - client = SlackClient(integration_id=self.integration.id) - with pytest.raises(ApiError): - client.post("/chat.postMessage", data=self.payload) - buffer = IntegrationRequestBuffer(client._get_redis_key()) - assert buffer.is_integration_broken() is True - integration = Integration.objects.get(id=self.integration.id) - assert integration.status == ObjectStatus.ACTIVE - @responses.activate def test_error_integration(self): """ @@ -149,10 +126,9 @@ def test_error_integration(self): assert buffer.is_integration_broken() is False @responses.activate - @with_feature("organizations:slack-fatal-disable-on-broken") def test_slow_integration_is_not_broken_or_disabled(self): """ - slow test with disable flag on + slow test put errors and success in buffer for 10 days, assert integration is not broken or disabled """ bodydict = {"ok": False, "error": "The requested resource does not exist"} @@ -178,8 +154,9 @@ def test_slow_integration_is_not_broken_or_disabled(self): @responses.activate def test_a_slow_integration_is_broken(self): """ - slow shut off with disable flag off - put errors in buffer for 10 days, assert integration is broken but not disabled + slow shut off + put errors in buffer for 10 days, assert integration is broken and not disabled + since only fatal shut off should disable """ bodydict = {"ok": False, "error": "The requested resource does not exist"} self.resp.add( diff --git a/tests/sentry/monitors/endpoints/test_organization_monitor_details.py b/tests/sentry/monitors/endpoints/test_organization_monitor_details.py index 720718bbb23b2f..8e8fe3cc5db597 100644 --- a/tests/sentry/monitors/endpoints/test_organization_monitor_details.py +++ b/tests/sentry/monitors/endpoints/test_organization_monitor_details.py @@ -5,7 +5,7 @@ from sentry.models import Environment, RegionScheduledDeletion, Rule, RuleActivity, RuleActivityType from sentry.monitors.models import Monitor, MonitorEnvironment, ScheduleType from sentry.testutils.cases import MonitorTestCase -from sentry.testutils.helpers.features import with_feature +from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import region_silo_test @@ -98,7 +98,7 @@ def test_slug(self): self.organization.slug, monitor.slug, method="PUT", status_code=400, **{"slug": None} ) - @with_feature("app:enterprise-prevent-numeric-slugs") + @override_options({"api.prevent-numeric-slugs": True}) def test_invalid_numeric_slug(self): monitor = self._create_monitor() resp = self.get_error_response( diff --git a/tests/sentry/monitors/endpoints/test_organization_monitor_index.py b/tests/sentry/monitors/endpoints/test_organization_monitor_index.py index d5f5836faa164e..780d24763e501b 100644 --- a/tests/sentry/monitors/endpoints/test_organization_monitor_index.py +++ b/tests/sentry/monitors/endpoints/test_organization_monitor_index.py @@ -11,7 +11,7 @@ from sentry.models import Rule, RuleSource from sentry.monitors.models import Monitor, MonitorStatus, MonitorType, ScheduleType from sentry.testutils.cases import MonitorTestCase -from sentry.testutils.helpers.features import with_feature +from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import region_silo_test @@ -212,7 +212,7 @@ def test_slug(self): assert response.data["slug"] == "my-monitor" - @with_feature("app:enterprise-prevent-numeric-slugs") + @override_options({"api.prevent-numeric-slugs": True}) def test_invalid_numeric_slug(self): data = { "project": self.project.slug, @@ -224,7 +224,7 @@ def test_invalid_numeric_slug(self): response = self.get_error_response(self.organization.slug, **data, status_code=400) assert response.data["slug"][0] == DEFAULT_SLUG_ERROR_MESSAGE - @with_feature("app:enterprise-prevent-numeric-slugs") + @override_options({"api.prevent-numeric-slugs": True}) def test_generated_slug_not_entirely_numeric(self): data = { "project": self.project.slug, diff --git a/tests/sentry/monitors/test_tasks.py b/tests/sentry/monitors/test_tasks.py index e3f1bc39eec6ee..5c3e2daa0d3e5a 100644 --- a/tests/sentry/monitors/test_tasks.py +++ b/tests/sentry/monitors/test_tasks.py @@ -174,7 +174,7 @@ def test_missing_checkin_with_margin(self): == monitor_environment_updated.next_checkin + timedelta(minutes=5) ) - def assert_state_does_not_change_for_state(self, state): + def assert_state_does_not_change_for_status(self, state): org = self.create_organization() project = self.create_project(organization=org) @@ -210,13 +210,13 @@ def assert_state_does_not_change_for_state(self, state): ).exists() def test_missing_checkin_but_disabled(self): - self.assert_state_does_not_change_for_state(ObjectStatus.DISABLED) + self.assert_state_does_not_change_for_status(ObjectStatus.DISABLED) def test_missing_checkin_but_pending_deletion(self): - self.assert_state_does_not_change_for_state(ObjectStatus.PENDING_DELETION) + self.assert_state_does_not_change_for_status(ObjectStatus.PENDING_DELETION) def test_missing_checkin_but_deletion_in_progress(self): - self.assert_state_does_not_change_for_state(ObjectStatus.DELETION_IN_PROGRESS) + self.assert_state_does_not_change_for_status(ObjectStatus.DELETION_IN_PROGRESS) def test_not_missing_checkin(self): """ diff --git a/tests/sentry/relay/config/test_metric_extraction.py b/tests/sentry/relay/config/test_metric_extraction.py index 279d5bb96bcedd..9f6255aed09d08 100644 --- a/tests/sentry/relay/config/test_metric_extraction.py +++ b/tests/sentry/relay/config/test_metric_extraction.py @@ -1,6 +1,8 @@ from typing import Sequence from unittest.mock import ANY +import pytest + from sentry.incidents.models import AlertRule from sentry.models import ( Dashboard, @@ -20,13 +22,17 @@ ON_DEMAND_METRICS = "organizations:on-demand-metrics-extraction" ON_DEMAND_METRICS_WIDGETS = "organizations:on-demand-metrics-extraction-experimental" +ON_DEMAND_METRICS_PREFILL = "organizations:on-demand-metrics-prefill" +ON_DEMAND_METRIC_PREFILL_ENABLE = "organizations:enable-on-demand-metrics-prefill" -def create_alert(aggregate: str, query: str, project: Project) -> AlertRule: +def create_alert( + aggregate: str, query: str, project: Project, dataset: Dataset = Dataset.PerformanceMetrics +) -> AlertRule: snuba_query = SnubaQuery.objects.create( aggregate=aggregate, query=query, - dataset=Dataset.PerformanceMetrics.value, + dataset=dataset.value, time_window=300, resolution=60, environment=None, @@ -393,3 +399,78 @@ def test_get_metric_extraction_config_with_apdex(default_project): {"key": "query_hash", "value": ANY}, ], } + + +@django_db_all +@pytest.mark.parametrize( + "enabled_features, number_of_metrics", + [ + ([ON_DEMAND_METRICS], 1), # Only alerts. + ([ON_DEMAND_METRICS_WIDGETS, ON_DEMAND_METRICS_PREFILL], 0), # Nothing. + ([ON_DEMAND_METRICS, ON_DEMAND_METRICS_WIDGETS], 2), # Alerts and widgets. + ([ON_DEMAND_METRICS_PREFILL, ON_DEMAND_METRIC_PREFILL_ENABLE], 1), # Alerts. + ([ON_DEMAND_METRICS_PREFILL], 0), # Nothing. + ([ON_DEMAND_METRICS, ON_DEMAND_METRICS_PREFILL], 1), # Alerts. + ([ON_DEMAND_METRICS, ON_DEMAND_METRIC_PREFILL_ENABLE], 1), # Alerts. + ([], 0), # Nothing. + ], +) +def test_get_metrics_extraction_config_features_combinations( + enabled_features, number_of_metrics, default_project +): + create_alert("count()", "transaction.duration:>=10", default_project) + create_widget(["count()"], "transaction.duration:>=20", default_project) + + features = {feature: True for feature in enabled_features} + with Feature(features): + config = get_metric_extraction_config(default_project) + if number_of_metrics == 0: + assert config is None + else: + assert config is not None + assert len(config["metrics"]) == number_of_metrics + + +@django_db_all +def test_get_metric_extraction_config_with_transactions_dataset(default_project): + create_alert( + "count()", "transaction.duration:>=10", default_project, dataset=Dataset.PerformanceMetrics + ) + create_alert( + "count()", "transaction.duration:>=20", default_project, dataset=Dataset.Transactions + ) + + # We test with prefilling, and we expect that both alerts are fetched since we support both datasets. + with Feature({ON_DEMAND_METRICS_PREFILL: True, ON_DEMAND_METRIC_PREFILL_ENABLE: True}): + config = get_metric_extraction_config(default_project) + + assert config + assert len(config["metrics"]) == 2 + assert config["metrics"][0] == { + "category": "transaction", + "condition": {"name": "event.duration", "op": "gte", "value": 10.0}, + "field": None, + "mri": "c:transactions/on_demand@none", + "tags": [{"key": "query_hash", "value": ANY}], + } + assert config["metrics"][1] == { + "category": "transaction", + "condition": {"name": "event.duration", "op": "gte", "value": 20.0}, + "field": None, + "mri": "c:transactions/on_demand@none", + "tags": [{"key": "query_hash", "value": ANY}], + } + + # We test without prefilling, and we expect that only alerts for performance metrics are fetched. + with Feature({ON_DEMAND_METRICS: True}): + config = get_metric_extraction_config(default_project) + + assert config + assert len(config["metrics"]) == 1 + assert config["metrics"][0] == { + "category": "transaction", + "condition": {"name": "event.duration", "op": "gte", "value": 10.0}, + "field": None, + "mri": "c:transactions/on_demand@none", + "tags": [{"key": "query_hash", "value": ANY}], + } diff --git a/tests/sentry/rules/filters/test_issue_severity.py b/tests/sentry/rules/filters/test_issue_severity.py new file mode 100644 index 00000000000000..08e377e8782c6e --- /dev/null +++ b/tests/sentry/rules/filters/test_issue_severity.py @@ -0,0 +1,71 @@ +from unittest.mock import patch + +from sentry.rules import MatchType +from sentry.rules.filters.issue_severity import IssueSeverityFilter +from sentry.testutils.cases import RuleTestCase +from sentry.testutils.helpers.features import with_feature + + +class IssueSeverityFilterTest(RuleTestCase): + rule_cls = IssueSeverityFilter + + @patch("sentry.models.Group.objects.get_from_cache") + @with_feature("projects:first-event-severity-alerting") + def test_valid_input_values(self, mock_group): + event = self.get_event() + event.group.data["metadata"] = {"severity": "0.7"} + mock_group.return_value = event.group + + data_cases = [ + {"match": MatchType.GREATER_OR_EQUAL, "value": 0.5}, + {"match": MatchType.GREATER_OR_EQUAL, "value": 0.7}, + {"match": MatchType.LESS_OR_EQUAL, "value": 0.7}, + {"match": MatchType.LESS_OR_EQUAL, "value": 0.9}, + {"match": MatchType.LESS_OR_EQUAL, "value": "0.9"}, + ] + + for data_case in data_cases: + rule = self.get_rule(data=data_case) + self.assertPasses(rule, event) + assert self.passes_activity(rule) is True + + @with_feature("projects:first-event-severity-alerting") + def test_fail_on_no_group(self): + event = self.get_event() + event.group_id = None + event.groups = None + + self.assertDoesNotPass( + self.get_rule(data={"match": MatchType.GREATER_OR_EQUAL, "value": 0.5}), event + ) + + @with_feature("projects:first-event-severity-alerting") + def test_fail_on_no_severity(self): + event = self.get_event() + + assert not event.group.get_event_metadata().get("severity") + self.assertDoesNotPass( + self.get_rule(data={"match": MatchType.GREATER_OR_EQUAL, "value": 0.5}), event + ) + + @patch("sentry.models.Group.objects.get_from_cache") + @with_feature("projects:first-event-severity-alerting") + def test_failing_input_values(self, mock_group): + event = self.get_event() + event.group.data["metadata"] = {"severity": "0.7"} + mock_group.return_value = event.group + + data_cases = [ + {"match": MatchType.GREATER_OR_EQUAL, "value": 0.9}, + {"match": MatchType.GREATER_OR_EQUAL, "value": "0.9"}, + {"match": MatchType.LESS_OR_EQUAL, "value": "0.5"}, + {"match": MatchType.LESS_OR_EQUAL, "value": 0.5}, + {"value": 0.5}, + {"match": MatchType.GREATER_OR_EQUAL}, + {}, + ] + + for data_case in data_cases: + rule = self.get_rule(data=data_case) + self.assertDoesNotPass(rule, event) + assert self.passes_activity(rule) is False diff --git a/tests/sentry/tasks/test_sentry_apps.py b/tests/sentry/tasks/test_sentry_apps.py index 7cee9a87268360..90da856b0f6185 100644 --- a/tests/sentry/tasks/test_sentry_apps.py +++ b/tests/sentry/tasks/test_sentry_apps.py @@ -10,7 +10,7 @@ from freezegun import freeze_time from requests.exceptions import Timeout -from sentry import audit_log, features +from sentry import audit_log from sentry.api.serializers import serialize from sentry.constants import SentryAppStatus from sentry.integrations.notify_disable import notify_disable @@ -775,7 +775,6 @@ def test_saves_error_for_request_timeout(self, safe_urlopen): assert self.integration_buffer._get_all_from_buffer() == [] assert self.integration_buffer.is_integration_broken() is False - @with_feature("organizations:disable-sentryapps-on-broken") @patch( "sentry.utils.sentry_apps.webhooks.safe_urlopen", return_value=MockResponseWithHeadersInstance, @@ -798,7 +797,6 @@ def test_saves_error_event_id_if_in_header(self, safe_urlopen): assert first_request["error_id"] == "d5111da2c28645c5889d072017e3445d" assert first_request["project_id"] == "1" - @with_feature("organizations:disable-sentryapps-on-broken") @patch("sentry.utils.sentry_apps.webhooks.safe_urlopen", side_effect=Timeout) def test_does_not_raise_error_if_unpublished(self, safe_urlopen): """ @@ -820,7 +818,6 @@ def test_does_not_raise_error_if_unpublished(self, safe_urlopen): assert self.sentry_app.events == events # check that events are the same / app is enabled @patch("sentry.utils.sentry_apps.webhooks.safe_urlopen", side_effect=Timeout) - @with_feature("organizations:disable-sentryapps-on-broken") @override_settings(BROKEN_TIMEOUT_THRESHOLD=3) def test_timeout_disable(self, safe_urlopen): """ @@ -833,40 +830,18 @@ def test_timeout_disable(self, safe_urlopen): send_webhooks( installation=self.install, event="issue.assigned", data=data, actor=self.user ) - assert features.has("organizations:disable-sentryapps-on-broken", self.organization) assert safe_urlopen.called assert [len(item) == 0 for item in self.integration_buffer._get_broken_range_from_buffer()] assert len(self.integration_buffer._get_all_from_buffer()) == 0 self.sentry_app.refresh_from_db() # reload to get updated events assert len(self.sentry_app.events) == 0 # check that events are empty / app is disabled - @patch("sentry.utils.sentry_apps.webhooks.safe_urlopen", side_effect=Timeout) - @override_settings(BROKEN_TIMEOUT_THRESHOLD=3) - def test_timeout_would_disable(self, safe_urlopen): - """ - Tests that the integration would be disabled if the feature flag is enabled but is not - """ - self.sentry_app.update(status=SentryAppStatus.INTERNAL) - events = self.sentry_app.events # save events to check later - data = {"issue": serialize(self.issue)} - # we don't raise errors for unpublished and internal apps - for i in range(3): - send_webhooks( - installation=self.install, event="issue.assigned", data=data, actor=self.user - ) - assert safe_urlopen.called - assert (self.integration_buffer._get_all_from_buffer()[0]["timeout_count"]) == "3" - assert self.integration_buffer.is_integration_broken() is True - self.sentry_app.refresh_from_db() # reload to get updated events - assert self.sentry_app.events == events # check that events are the same / app is enabled - @patch( "sentry.utils.sentry_apps.webhooks.safe_urlopen", return_value=MockFailureResponseInstance ) - @with_feature("organizations:disable-sentryapps-on-broken") def test_slow_should_disable(self, safe_urlopen): """ - Tests that the integration is broken after 7 days of errors and disabled since flag is on + Tests that the integration is broken after 7 days of errors and disabled Slow shut off """ self.sentry_app.update(status=SentryAppStatus.INTERNAL) @@ -888,31 +863,6 @@ def test_slow_should_disable(self, safe_urlopen): organization_id=self.organization.id, ).exists() - @patch( - "sentry.utils.sentry_apps.webhooks.safe_urlopen", return_value=MockFailureResponseInstance - ) - @freeze_time("2022-01-01 03:30:00") - def test_slow_broken_not_disable(self, safe_urlopen): - """ - Tests that the integration is broken after 10 days of errors but still enabled since flag is off - Slow shut off - """ - self.sentry_app.update(status=SentryAppStatus.INTERNAL) - events = self.sentry_app.events # save events to check later - data = {"issue": serialize(self.issue)} - now = datetime.now() - for i in reversed(range(0, 10)): - with freeze_time(now - timedelta(days=i)): - send_webhooks( - installation=self.install, event="issue.assigned", data=data, actor=self.user - ) - - assert safe_urlopen.called - assert len(self.integration_buffer._get_all_from_buffer()) == 10 - assert self.integration_buffer.is_integration_broken() is True - self.sentry_app.refresh_from_db() # reload to get updated events - assert self.sentry_app.events == events # check that events are the same / app is enabled - def test_notify_disabled_email(self): with self.tasks(): notify_disable( diff --git a/tests/sentry/utils/test_audit.py b/tests/sentry/utils/test_audit.py index e8373acfb97c45..28f75902447146 100644 --- a/tests/sentry/utils/test_audit.py +++ b/tests/sentry/utils/test_audit.py @@ -301,6 +301,20 @@ def test_audit_entry_project_performance_setting_enable_detection(self): == "edited project performance issue detector settings to enable detection of File IO on Main Thread issue" ) + def test_audit_entry_project_ownership_rule_edit(self): + entry = create_audit_entry( + request=self.req, + organization=self.org, + target_object=self.project.id, + event=audit_log.get_event_id("PROJECT_OWNERSHIPRULE_EDIT"), + ) + audit_log_event = audit_log.get(entry.event) + + assert entry.actor == self.user + assert entry.target_object == self.project.id + assert entry.event == audit_log.get_event_id("PROJECT_OWNERSHIPRULE_EDIT") + assert audit_log_event.render(entry) == "modified ownership rules" + def test_audit_entry_integration_log(self): project = self.create_project() self.login_as(user=self.user) diff --git a/yarn.lock b/yarn.lock index 0e4053f60ae0cb..9bb61015c881bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5474,17 +5474,17 @@ eslint-config-prettier@^8.8.0: resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA== -eslint-config-sentry-app@1.122.0: - version "1.122.0" - resolved "https://registry.yarnpkg.com/eslint-config-sentry-app/-/eslint-config-sentry-app-1.122.0.tgz#51f73659477be68a1102ea638582307e17d5e697" - integrity sha512-+5d+VZK3ZwC02rvgOtwtGcoWUjdjOcE0XERsnIFAbIy2rCAJI01xu4U0h8/9nnc3ljxXAym7IeRZFyjycCHQkg== +eslint-config-sentry-app@1.123.0: + version "1.123.0" + resolved "https://registry.yarnpkg.com/eslint-config-sentry-app/-/eslint-config-sentry-app-1.123.0.tgz#b0ccb1d32d22673fa39aabdefe8f6e8a66d62268" + integrity sha512-rTvynJ94r+eYNl40me8UMrO+/AU2SruCYptzcW92daoXqh8z3rGPiljSBb1VuWCftU1e4ZFpydik9Zk1Zqj+zQ== dependencies: "@emotion/eslint-plugin" "^11.11.0" "@typescript-eslint/eslint-plugin" "^6.2.0" "@typescript-eslint/parser" "^6.2.0" eslint-config-prettier "^8.8.0" - eslint-config-sentry "^1.122.0" - eslint-config-sentry-react "^1.122.0" + eslint-config-sentry "^1.123.0" + eslint-config-sentry-react "^1.123.0" eslint-import-resolver-typescript "^2.7.1" eslint-import-resolver-webpack "^0.13.2" eslint-plugin-import "^2.27.5" @@ -5492,24 +5492,24 @@ eslint-config-sentry-app@1.122.0: eslint-plugin-no-lookahead-lookbehind-regexp "0.1.0" eslint-plugin-prettier "^4.2.1" eslint-plugin-react "^7.32.2" - eslint-plugin-sentry "^1.122.0" + eslint-plugin-sentry "^1.123.0" eslint-plugin-simple-import-sort "^10.0.0" -eslint-config-sentry-react@^1.122.0: - version "1.122.0" - resolved "https://registry.yarnpkg.com/eslint-config-sentry-react/-/eslint-config-sentry-react-1.122.0.tgz#afdad711ab48bffc714d8901f11b8e3979260de8" - integrity sha512-l74Io+JSUduvFW1EEkOZkGV9/cgrRvKZuFP9VGJ4rUmbYpKCemWc8IXUNU1PXanygrm8T3r7DzcIq8DuTnWRQA== +eslint-config-sentry-react@^1.123.0: + version "1.123.0" + resolved "https://registry.yarnpkg.com/eslint-config-sentry-react/-/eslint-config-sentry-react-1.123.0.tgz#120cd5f53f3fcffc77f7ea0de503713710a0d8df" + integrity sha512-88E4tFqx0tywmRjflKHdIszaTnBoaHUh8GwYYLi39Kl09SisfizdTx00+TovqUjHknrJiBGLH3XBXcKVr3DRQw== dependencies: - eslint-config-sentry "^1.122.0" + eslint-config-sentry "^1.123.0" eslint-plugin-jest-dom "^5.0.1" eslint-plugin-react-hooks "^4.6.0" eslint-plugin-testing-library "^5.11.0" eslint-plugin-typescript-sort-keys "^2.3.0" -eslint-config-sentry@^1.122.0: - version "1.122.0" - resolved "https://registry.yarnpkg.com/eslint-config-sentry/-/eslint-config-sentry-1.122.0.tgz#4e8fc860decf088c747a6c951ce4ab3c4bbd5c10" - integrity sha512-gHSOkyJw1ymGNy0hT7iBrrenr2aa3ITVTqllCE4mByWjvMuud5vd0qOED8/LPPPY5UsVhppBEC5Barm+g5Yfaw== +eslint-config-sentry@^1.123.0: + version "1.123.0" + resolved "https://registry.yarnpkg.com/eslint-config-sentry/-/eslint-config-sentry-1.123.0.tgz#f21946f974ccf431634cc9c89322261ec8a9a453" + integrity sha512-tI9cULUzADTcwUrWOGPbTSTmqbwIkYgz5WoznizO/Y1ISgVa8YGujZjUqROb63ob9eeAGlRRgelfdEKXkcHpLA== eslint-import-resolver-node@^0.3.7: version "0.3.7" @@ -5632,10 +5632,10 @@ eslint-plugin-react@^7.32.2: semver "^6.3.0" string.prototype.matchall "^4.0.8" -eslint-plugin-sentry@^1.122.0: - version "1.122.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-sentry/-/eslint-plugin-sentry-1.122.0.tgz#39b48517fa714287320c29230bd5d67b67dbf53c" - integrity sha512-1qG86KtKvYIa/mpxFF+fIIAJq3ZJalHJg9KfeoDVsrnJuB4McTo5djnu+VtCgCF278hwGk9ijk7th9KnIYQvDA== +eslint-plugin-sentry@^1.123.0: + version "1.123.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-sentry/-/eslint-plugin-sentry-1.123.0.tgz#b7480350ff9f6ad0c56fd1ca6ff05e6d77216506" + integrity sha512-m7NVJeZb8kP3ErKbGMbYddxK6hqLhBZLr29B6qA7dmNbLujRqOzkANAb1BGR4zNmAcNpsHp9dAn9vapjApllSQ== dependencies: requireindex "~1.2.0"