diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d1bfce0f68a0..9a7b27d8e79a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -856,8 +856,6 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get /.agents/skills/lint-fix/ @getsentry/design-engineering /.agents/skills/lint-new/ @getsentry/design-engineering /.agents/skills/react-component-documentation/ @getsentry/design-engineering -# Orphaned files we had to touch to fix errors -/static/gsAdmin/views/sentryAppDetails.spec.tsx @getsentry/design-engineering ## End of Frontend Platform # Coding Workflows diff --git a/.github/actions/setup-devservices/action.yml b/.github/actions/setup-devservices/action.yml deleted file mode 100644 index 061ff20d9b72..000000000000 --- a/.github/actions/setup-devservices/action.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: 'Early Devservices' -description: 'Starts devservices in the background so image pulls overlap with venv setup' -inputs: - mode: - description: 'devservices mode (must match the mode passed to setup-sentry)' - required: true - timeout-minutes: - description: 'Maximum minutes for devservices up' - required: false - default: '10' - -runs: - using: 'composite' - steps: - - uses: astral-sh/setup-uv@884ad927a57e558e7a70b92f2bccf9198a4be546 # v6 - with: - version: '0.9.28' - enable-cache: false - - - name: Start devservices in background - shell: bash --noprofile --norc -euo pipefail {0} - run: | - DS_VERSION=$(python3 -c " - import tomllib - with open('uv.lock', 'rb') as f: - lock = tomllib.load(f) - for pkg in lock['package']: - if pkg['name'] == 'devservices': - print(pkg['version']) - break - ") - echo "Installing devservices==${DS_VERSION}" - uv venv /tmp/ds-venv --python python3 -q - uv pip install --python /tmp/ds-venv/bin/python -q \ - --index-url https://pypi.devinfra.sentry.io/simple \ - "devservices==${DS_VERSION}" - (set +e; timeout ${{ inputs.timeout-minutes }}m /tmp/ds-venv/bin/devservices up --mode ${{ inputs.mode }}; echo $? > /tmp/ds-exit) \ - > /tmp/ds.log 2>&1 & diff --git a/.github/actions/setup-devservices/bootstrap-snuba.py b/.github/actions/setup-devservices/bootstrap-snuba.py deleted file mode 100755 index c0f4ea8a7e7b..000000000000 --- a/.github/actions/setup-devservices/bootstrap-snuba.py +++ /dev/null @@ -1,250 +0,0 @@ -#!/usr/bin/env python3 -"""Bootstrap per-worker Snuba instances for CI. - -Overlaps the expensive ClickHouse table setup with the devservices -health-check wait. - -Phase 1 (early): As soon as ClickHouse is accepting queries, create - per-worker databases and run ``snuba bootstrap --force``. -Phase 2 (after devservices): Stop snuba-snuba-1 and start per-worker - API containers. We must wait for devservices to finish first — - stopping the container while devservices is health-checking it would - cause a timeout. - -Requires: XDIST_WORKERS env var -Reads: /tmp/ds-exit (written by setup-devservices/wait.sh) -Writes: /tmp/snuba-bootstrap-exit -""" - -from __future__ import annotations - -import os -import subprocess -import sys -import time -from concurrent.futures import ThreadPoolExecutor, as_completed -from functools import partial -from pathlib import Path -from typing import Any, Callable -from urllib.error import URLError -from urllib.request import urlopen - -DS_EXIT = Path("/tmp/ds-exit") -SNUBA_EXIT = Path("/tmp/snuba-bootstrap-exit") - -SNUBA_ENV = { - "CLICKHOUSE_HOST": "clickhouse", - "CLICKHOUSE_PORT": "9000", - "CLICKHOUSE_HTTP_PORT": "8123", - "DEFAULT_BROKERS": "kafka:9093", - "REDIS_HOST": "redis", - "REDIS_PORT": "6379", - "REDIS_DB": "1", - "SNUBA_SETTINGS": "docker", -} - -ENV_ARGS = [flag for k, v in SNUBA_ENV.items() for flag in ("-e", f"{k}={v}")] - - -def retry( - fn: Callable[[], Any], *, attempts: int = 3, delay: int = 5, label: str = "operation" -) -> Any: - for i in range(attempts): - try: - return fn() - except Exception: - if i == attempts - 1: - raise - log(f"{label} failed (attempt {i + 1}/{attempts}), retrying in {delay}s...") - time.sleep(delay) - - -def log(msg: str) -> None: - print(msg, flush=True) - - -def fail(msg: str) -> None: - log(f"::error::{msg}") - SNUBA_EXIT.write_text("1") - sys.exit(1) - - -def http_ok(url: str) -> bool: - try: - with urlopen(url, timeout=3): - return True - except (URLError, OSError): - return False - - -def docker( - *args: str, check: bool = False, timeout: int | None = None -) -> subprocess.CompletedProcess[str]: - return subprocess.run( - ["docker", *args], capture_output=True, text=True, check=check, timeout=timeout - ) - - -def docker_inspect(container: str, fmt: str) -> str: - r = docker("inspect", container, "--format", fmt) - return r.stdout.strip() if r.returncode == 0 else "" - - -def inspect_snuba_container() -> tuple[str, str]: - image = docker_inspect("snuba-snuba-1", "{{.Config.Image}}") - network = docker_inspect( - "snuba-snuba-1", - "{{range $k, $v := .NetworkSettings.Networks}}{{$k}}{{end}}", - ) - if not image or not network: - fail("Could not inspect snuba-snuba-1 container") - return image, network - - -def run_parallel(fn: Callable[[int], Any], workers: range, *, fail_fast: bool = True) -> int: - """Run fn(i) in parallel for each i in workers. Returns 0 on full success.""" - rc = 0 - with ThreadPoolExecutor(max_workers=len(workers)) as pool: - futs = {pool.submit(fn, i): i for i in workers} - for fut in as_completed(futs): - try: - fut.result() - except Exception as e: - if fail_fast: - fail(str(e)) - log(f"ERROR: {e}") - rc = 1 - return rc - - -def wait_for_prerequisites(timeout: int = 300) -> None: - log("Waiting for ClickHouse and Snuba container...") - start = time.monotonic() - while True: - if time.monotonic() - start > timeout: - fail("Timed out waiting for Snuba bootstrap prerequisites") - if http_ok("http://localhost:8123/") and docker_inspect("snuba-snuba-1", "{{.Id}}"): - break - time.sleep(2) - log(f"Prerequisites ready ({time.monotonic() - start:.0f}s)") - - -def wait_for_devservices(timeout: int = 300) -> None: - start = time.monotonic() - while not DS_EXIT.exists(): - if time.monotonic() - start > timeout: - fail("Timed out waiting for devservices to finish") - time.sleep(1) - rc = int(DS_EXIT.read_text().strip()) - if rc != 0: - fail(f"devservices failed (exit {rc}), skipping Phase 2") - - -def bootstrap_worker(worker_id: int, *, image: str, network: str) -> None: - """Create a ClickHouse database and run snuba bootstrap.""" - db = f"default_gw{worker_id}" - - def create_db() -> None: - with urlopen( - "http://localhost:8123/", f"CREATE DATABASE IF NOT EXISTS {db}".encode(), timeout=30 - ): - pass - - retry(create_db, label=f"CREATE DATABASE {db}") - - def run_bootstrap() -> None: - r = docker( - "run", - "--rm", - "--network", - network, - "-e", - f"CLICKHOUSE_DATABASE={db}", - *ENV_ARGS, - image, - "bootstrap", - "--force", - ) - for line in (r.stdout + r.stderr).strip().splitlines()[-3:]: - log(line) - if r.returncode != 0: - raise RuntimeError(f"snuba bootstrap failed for worker {worker_id}") - - retry(run_bootstrap, label=f"snuba bootstrap gw{worker_id}") - - -def start_worker_container(worker_id: int, *, image: str, network: str) -> None: - """Start a per-worker Snuba API container and wait for health.""" - db = f"default_gw{worker_id}" - port = 1230 + worker_id - name = f"snuba-gw{worker_id}" - - docker("rm", "-f", name) - - r = docker( - "run", - "-d", - "--name", - name, - "--network", - network, - "-p", - f"{port}:1218", - "-e", - f"CLICKHOUSE_DATABASE={db}", - *ENV_ARGS, - "-e", - "DEBUG=1", - image, - "api", - ) - if r.returncode != 0: - raise RuntimeError(f"docker run {name} failed: {r.stderr.strip()}") - - for attempt in range(1, 31): - if http_ok(f"http://127.0.0.1:{port}/health"): - log(f"{name} healthy on port {port}") - return - if attempt == 30: - r = docker("logs", name) - for line in (r.stdout + r.stderr).strip().splitlines()[-20:]: - log(line) - raise RuntimeError(f"{name} failed health check after 30 attempts") - time.sleep(2) - - -def main() -> None: - workers_str = os.environ.get("XDIST_WORKERS") - if not workers_str: - fail("XDIST_WORKERS must be set") - workers = range(int(workers_str)) - start = time.monotonic() - - wait_for_prerequisites() - image, network = inspect_snuba_container() - - log("Phase 1: bootstrapping ClickHouse databases") - run_parallel(partial(bootstrap_worker, image=image, network=network), workers) - log(f"Phase 1 done ({time.monotonic() - start:.0f}s)") - - wait_for_devservices() - try: - docker("stop", "snuba-snuba-1", timeout=30) - except subprocess.TimeoutExpired: - log("WARNING: docker stop snuba-snuba-1 timed out, killing") - docker("kill", "snuba-snuba-1") - - log("Phase 2: starting per-worker Snuba API containers") - rc = run_parallel( - partial(start_worker_container, image=image, network=network), - workers, - fail_fast=False, - ) - - log(f"Snuba bootstrap complete ({time.monotonic() - start:.0f}s total)") - SNUBA_EXIT.write_text(str(rc)) - sys.exit(rc) - - -if __name__ == "__main__": - main() diff --git a/.github/actions/setup-devservices/wait.sh b/.github/actions/setup-devservices/wait.sh deleted file mode 100755 index 00c319469d73..000000000000 --- a/.github/actions/setup-devservices/wait.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Wait for the background devservices process started by the setup-devservices action. -# Usage: wait.sh [timeout_seconds] -TIMEOUT=${1:-600} - -SECONDS=0 -while [ ! -f /tmp/ds-exit ]; do - if [ $SECONDS -gt "$TIMEOUT" ]; then - echo "::error::Timed out waiting for devservices after ${TIMEOUT}s" - cat /tmp/ds.log - exit 1 - fi - sleep 2 -done - -DS_RC=$(< /tmp/ds-exit) -if [ "$DS_RC" -ne 0 ]; then - echo "::error::devservices up failed (exit $DS_RC)" - cat /tmp/ds.log - exit 1 -fi - -echo "DJANGO_LIVE_TEST_SERVER_ADDRESS=$(docker network inspect bridge --format='{{(index .IPAM.Config 0).Gateway}}')" >> "$GITHUB_ENV" -docker ps -a diff --git a/.github/codeowners-coverage-baseline.txt b/.github/codeowners-coverage-baseline.txt index 734fcb38d43f..1f28e295e561 100644 --- a/.github/codeowners-coverage-baseline.txt +++ b/.github/codeowners-coverage-baseline.txt @@ -591,6 +591,7 @@ static/app/components/events/breadcrumbs/testUtils.tsx static/app/components/events/breadcrumbs/utils.tsx static/app/components/events/device.tsx static/app/components/events/errorLevel.tsx +static/app/components/events/eventAnnotation.tsx static/app/components/events/eventAttachmentActions.tsx static/app/components/events/eventAttachments.spec.tsx static/app/components/events/eventAttachments.tsx diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 652d2ca72b93..8bad8cfafc06 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -68,12 +68,6 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 name: Checkout sentry - - run: mkdir -p config/chartcuterie - - - uses: ./.github/actions/setup-devservices - with: - mode: acceptance-ci - - name: Step configurations id: config run: | @@ -104,12 +98,6 @@ jobs: id: setup with: mode: acceptance-ci - skip-devservices: 'true' - - - name: Wait for devservices - run: | - sentry init - ./.github/actions/setup-devservices/wait.sh - name: Run acceptance tests (#${{ steps.setup.outputs.matrix-instance-number }} of ${{ steps.setup.outputs.matrix-instance-total }}) run: make run-acceptance @@ -117,11 +105,6 @@ jobs: - name: Inspect failure if: failure() run: | - if [ -f /tmp/ds.log ]; then - echo "--- devservices startup log ---" - cat /tmp/ds.log - fi - if command -v devservices; then devservices logs fi diff --git a/.github/workflows/backend-with-coverage.yml b/.github/workflows/backend-with-coverage.yml new file mode 100644 index 000000000000..65f1496ce765 --- /dev/null +++ b/.github/workflows/backend-with-coverage.yml @@ -0,0 +1,199 @@ +name: backend - with test coverage + +on: + workflow_dispatch: + workflow_call: + # Every 30 minutes + schedule: + - cron: '0,30 * * * *' + +env: + SENTRY_SKIP_SELENIUM_PLUGIN: '1' + +jobs: + # Skip generating coverage if already exists in GCS + check-existing-coverage: + name: check for existing coverage + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + id-token: write + outputs: + has-coverage: ${{ steps.check-coverage.outputs.exists }} + commit-sha: ${{ steps.get-sha.outputs.sha }} + steps: + - name: Determine commit SHA + id: get-sha + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + COMMIT_SHA="${{ github.event.pull_request.head.sha }}" + else + COMMIT_SHA="${{ github.sha }}" + fi + echo "sha=${COMMIT_SHA}" >> "$GITHUB_OUTPUT" + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2.1.3 + with: + project_id: sentry-dev-tooling + workload_identity_provider: ${{ secrets.SENTRY_GCP_DEV_WORKLOAD_IDENTITY_POOL }} + service_account: ${{ secrets.COLLECT_TEST_DATA_SERVICE_ACCOUNT_EMAIL }} + + - name: Check if coverage exists + id: check-coverage + env: + COMMIT_SHA: ${{ steps.get-sha.outputs.sha }} + run: | + if gcloud storage ls "gs://sentry-coverage-data/${COMMIT_SHA}/.coverage.combined" &>/dev/null; then + echo "Coverage already exists for commit ${COMMIT_SHA}, skipping test run" + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "No coverage found for commit ${COMMIT_SHA}, will run tests" + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + calculate-shards: + name: calculate test shards + if: needs.check-existing-coverage.outputs.has-coverage != 'true' + needs: [check-existing-coverage] + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + shard-count: ${{ steps.calculate-shards.outputs.shard-count }} + shard-indices: ${{ steps.calculate-shards.outputs.shard-indices }} + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: Setup sentry env + uses: ./.github/actions/setup-sentry + id: setup + with: + mode: backend-ci + skip-devservices: true + + - name: Calculate test shards + id: calculate-shards + run: python3 .github/workflows/scripts/calculate-backend-test-shards.py + + backend-test-with-cov-context: + name: backend test + if: needs.check-existing-coverage.outputs.has-coverage != 'true' + runs-on: ubuntu-24.04 + needs: [check-existing-coverage, calculate-shards] + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + instance: ${{ fromJSON(needs.calculate-shards.outputs.shard-indices) }} + env: + MATRIX_INSTANCE_TOTAL: ${{ needs.calculate-shards.outputs.shard-count }} + TEST_GROUP_STRATEGY: roundrobin + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: Setup sentry env + uses: ./.github/actions/setup-sentry + id: setup + with: + mode: backend-ci + + - name: Download odiff binary + run: | + curl -sL https://registry.npmjs.org/odiff-bin/-/odiff-bin-4.3.2.tgz \ + | tar -xz --strip-components=2 package/raw_binaries/odiff-linux-x64 + sudo install -m 755 odiff-linux-x64 /usr/local/bin/odiff + rm odiff-linux-x64 + + - name: Run backend test with coverage (${{ steps.setup.outputs.matrix-instance-number }} of ${{ steps.setup.outputs.matrix-instance-total }}) + run: make test-backend-ci-with-coverage + + - name: Validate coverage database + if: always() + run: | + set -euxo pipefail + if [[ ! -f .coverage ]]; then + echo "Error: No .coverage file found after tests" + exit 1 + fi + python -c "import sqlite3; sqlite3.connect('.coverage').execute('SELECT COUNT(*) FROM file')" + + - name: Upload raw coverage sqlite as artifact + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: pycoverage-sqlite-${{ github.run_id }}-${{ steps.setup.outputs.matrix-instance-number }} + path: .coverage + if-no-files-found: error + retention-days: 7 + include-hidden-files: true + + combine-coverage: + name: combine coverage + # Only upload coverage if all test shards pass - incomplete coverage could cause selective testing to skip tests incorrectly + if: needs.check-existing-coverage.outputs.has-coverage != 'true' + runs-on: ubuntu-24.04 + permissions: + contents: read + id-token: write + actions: read # used for DIM metadata + needs: [check-existing-coverage, backend-test-with-cov-context, calculate-shards] + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - uses: astral-sh/setup-uv@884ad927a57e558e7a70b92f2bccf9198a4be546 # v6 + with: + version: '0.9.28' + enable-cache: false + + - name: Download all coverage artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: pycoverage-sqlite-${{ github.run_id }}-* + path: .artifacts/all-coverage + + - name: Verify all shards produced coverage + env: + EXPECTED_SHARD_COUNT: ${{ needs.calculate-shards.outputs.shard-count }} + run: | + set -euxo pipefail + + echo "Downloaded coverage artifacts:" + ls -la .artifacts/all-coverage || true + + COVERAGE_FILE_COUNT=$(find .artifacts/all-coverage -name ".coverage" -type f | wc -l) + echo "Found ${COVERAGE_FILE_COUNT} coverage files, expected ${EXPECTED_SHARD_COUNT}" + + if [[ "$COVERAGE_FILE_COUNT" -ne "$EXPECTED_SHARD_COUNT" ]]; then + echo "Error: Missing coverage files. Expected ${EXPECTED_SHARD_COUNT}, found ${COVERAGE_FILE_COUNT}" + echo "This indicates some test shards failed to produce coverage." + find .artifacts/all-coverage -name ".coverage" -type f + exit 1 + fi + + - name: Combine all coverage databases + run: | + set -euxo pipefail + uvx --with covdefaults --with sentry-covdefaults-disable-branch-coverage \ + coverage combine $(find .artifacts/all-coverage -name ".coverage" -type f) + + if [[ ! -f .coverage ]]; then + echo "Error: Combined coverage file was not created" + exit 1 + fi + + mv .coverage ".coverage.combined" + + - name: Authenticate to Google Cloud + id: gcloud-auth + uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2.1.3 + with: + project_id: sentry-dev-tooling + workload_identity_provider: ${{ secrets.SENTRY_GCP_DEV_WORKLOAD_IDENTITY_POOL }} + service_account: ${{ secrets.COLLECT_TEST_DATA_SERVICE_ACCOUNT_EMAIL }} + + - name: Upload coverage to GCS + uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2.2.4 + with: + path: .coverage.combined + destination: sentry-coverage-data/${{ needs.check-existing-coverage.outputs.commit-sha }} diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index fbcde1793f94..23b62cfad424 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -59,23 +59,11 @@ jobs: node-version-file: '.node-version' - uses: pnpm/action-setup@9b5745cdf0a2e8c2620f0746130f809adb911c19 # v4 - - - name: Start devservices early - uses: ./.github/actions/setup-devservices - with: - mode: default - - name: Setup sentry python env uses: ./.github/actions/setup-sentry id: setup with: mode: default - skip-devservices: 'true' - - - name: Wait for devservices - run: | - sentry init - ./.github/actions/setup-devservices/wait.sh - name: Run API docs tests run: | @@ -246,28 +234,11 @@ jobs: steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - name: Start devservices early - uses: ./.github/actions/setup-devservices - with: - mode: backend-ci - - name: Setup sentry env uses: ./.github/actions/setup-sentry id: setup with: mode: backend-ci - skip-devservices: 'true' - - - name: Wait for devservices - run: | - sentry init - # Start Snuba per-worker bootstrap in background — it polls for - # ClickHouse readiness itself, so it can begin before all containers - # are healthy, overlapping with the remaining devservices health checks. - if [ "${XDIST_PER_WORKER_SNUBA}" = "1" ]; then - python3 ./.github/actions/setup-devservices/bootstrap-snuba.py & - fi - ./.github/actions/setup-devservices/wait.sh - name: Download odiff binary run: | @@ -276,23 +247,59 @@ jobs: sudo install -m 755 odiff-linux-x64 /usr/local/bin/odiff rm odiff-linux-x64 - - name: Wait for Snuba bootstrap + - name: Bootstrap per-worker Snuba instances if: env.XDIST_PER_WORKER_SNUBA == '1' run: | - SECONDS=0 - while [ ! -f /tmp/snuba-bootstrap-exit ]; do - if [ $SECONDS -gt 600 ]; then - echo "::error::Timed out waiting for Snuba bootstrap after 600s" - exit 1 - fi - sleep 2 - done - SNUBA_RC=$(cat /tmp/snuba-bootstrap-exit) - if [ "$SNUBA_RC" -ne 0 ]; then - echo "::error::Snuba per-worker bootstrap failed" + set -eo pipefail + SNUBA_IMAGE=$(docker inspect snuba-snuba-1 --format '{{.Config.Image}}') + SNUBA_NETWORK=$(docker inspect snuba-snuba-1 --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}{{end}}') + if [ -z "$SNUBA_IMAGE" ] || [ -z "$SNUBA_NETWORK" ]; then + echo "ERROR: Could not inspect snuba-snuba-1 container. Is devservices running?" exit 1 fi + docker stop snuba-snuba-1 || true + + PIDS=() + for i in $(seq 0 $(( ${XDIST_WORKERS} - 1 ))); do + ( + WORKER_DB="default_gw${i}" + WORKER_PORT=$((1230 + i)) + curl -sf 'http://localhost:8123/' --data-binary "CREATE DATABASE IF NOT EXISTS ${WORKER_DB}" + docker run --rm --network "$SNUBA_NETWORK" \ + -e "CLICKHOUSE_DATABASE=${WORKER_DB}" -e "CLICKHOUSE_HOST=clickhouse" \ + -e "CLICKHOUSE_PORT=9000" -e "CLICKHOUSE_HTTP_PORT=8123" \ + -e "DEFAULT_BROKERS=kafka:9093" -e "REDIS_HOST=redis" \ + -e "REDIS_PORT=6379" -e "REDIS_DB=1" -e "SNUBA_SETTINGS=docker" \ + "$SNUBA_IMAGE" bootstrap --force 2>&1 | tail -3 + docker run -d --name "snuba-gw${i}" --network "$SNUBA_NETWORK" \ + -p "${WORKER_PORT}:1218" \ + -e "CLICKHOUSE_DATABASE=${WORKER_DB}" -e "CLICKHOUSE_HOST=clickhouse" \ + -e "CLICKHOUSE_PORT=9000" -e "CLICKHOUSE_HTTP_PORT=8123" \ + -e "DEFAULT_BROKERS=kafka:9093" -e "REDIS_HOST=redis" \ + -e "REDIS_PORT=6379" -e "REDIS_DB=1" -e "SNUBA_SETTINGS=docker" \ + -e "DEBUG=1" "$SNUBA_IMAGE" api + + for attempt in $(seq 1 30); do + if curl -sf "http://127.0.0.1:${WORKER_PORT}/health" > /dev/null 2>&1; then + echo "snuba-gw${i} healthy on port ${WORKER_PORT}" + break + fi + if [ "$attempt" -eq 30 ]; then + echo "ERROR: snuba-gw${i} failed health check after 30 attempts" + docker logs "snuba-gw${i}" 2>&1 | tail -20 || true + exit 1 + fi + sleep 2 + done + ) & + PIDS+=($!) + done + + for pid in "${PIDS[@]}"; do + wait "$pid" || { echo "ERROR: Snuba bootstrap subshell (PID $pid) failed"; exit 1; } + done + - name: Download selected tests artifact if: needs.select-tests.outputs.has-selected-tests == 'true' uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 @@ -332,11 +339,6 @@ jobs: - name: Inspect failure if: failure() run: | - if [ -f /tmp/ds.log ]; then - echo "--- devservices startup log ---" - cat /tmp/ds.log - fi - if command -v devservices; then devservices logs fi @@ -373,22 +375,11 @@ jobs: steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - name: Start devservices early - uses: ./.github/actions/setup-devservices - with: - mode: default - - name: Setup sentry env uses: ./.github/actions/setup-sentry id: setup with: mode: default - skip-devservices: 'true' - - - name: Wait for devservices - run: | - sentry init - ./.github/actions/setup-devservices/wait.sh - name: run tests run: | @@ -422,22 +413,11 @@ jobs: steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - name: Start devservices early - uses: ./.github/actions/setup-devservices - with: - mode: migrations - - name: Setup sentry env uses: ./.github/actions/setup-sentry id: setup with: mode: migrations - skip-devservices: 'true' - - - name: Wait for devservices - run: | - sentry init - ./.github/actions/setup-devservices/wait.sh - name: Run test env: @@ -509,21 +489,10 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - name: Start devservices early - uses: ./.github/actions/setup-devservices - with: - mode: backend-ci - - name: Setup sentry env uses: ./.github/actions/setup-sentry with: mode: backend-ci - skip-devservices: 'true' - - - name: Wait for devservices - run: | - sentry init - ./.github/actions/setup-devservices/wait.sh - name: Sync API Urls to TypeScript run: | @@ -546,22 +515,11 @@ jobs: - name: Checkout sentry uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - name: Start devservices early - uses: ./.github/actions/setup-devservices - with: - mode: migrations - - name: Setup sentry env uses: ./.github/actions/setup-sentry id: setup with: mode: migrations - skip-devservices: 'true' - - - name: Wait for devservices - run: | - sentry init - ./.github/actions/setup-devservices/wait.sh - name: Migration & lockfile checks env: @@ -591,22 +549,11 @@ jobs: steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - name: Start devservices early - uses: ./.github/actions/setup-devservices - with: - mode: migrations - - name: Setup sentry env uses: ./.github/actions/setup-sentry id: setup with: mode: migrations - skip-devservices: 'true' - - - name: Wait for devservices - run: | - sentry init - ./.github/actions/setup-devservices/wait.sh - name: Run test run: | diff --git a/.github/workflows/changelog-preview.yml b/.github/workflows/changelog-preview.yml new file mode 100644 index 000000000000..63088abe3b5a --- /dev/null +++ b/.github/workflows/changelog-preview.yml @@ -0,0 +1,21 @@ +name: Changelog Preview +on: + pull_request_target: + types: + - opened + - synchronize + - reopened + - edited + - labeled + - unlabeled +permissions: + contents: write + pull-requests: write + statuses: write + +jobs: + changelog-preview: + uses: getsentry/craft/.github/workflows/changelog-preview.yml@f4889d04564e47311038ecb6b910fef6b6cf1363 # v2 + with: + comment: false + secrets: inherit diff --git a/.github/workflows/codeowners-coverage.yml b/.github/workflows/codeowners-coverage.yml index ed6e31c42858..e66921860362 100644 --- a/.github/workflows/codeowners-coverage.yml +++ b/.github/workflows/codeowners-coverage.yml @@ -6,8 +6,7 @@ on: # push: # branches: # - master - # pull_request: - workflow_dispatch: + pull_request: # Cancel in progress workflows on pull_requests. # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value diff --git a/.github/workflows/openapi-diff.yml b/.github/workflows/openapi-diff.yml index 556d036b53b4..9c510beafba3 100644 --- a/.github/workflows/openapi-diff.yml +++ b/.github/workflows/openapi-diff.yml @@ -27,25 +27,12 @@ jobs: token: ${{ github.token }} filters: .github/file-filters.yml - - name: Start devservices early - uses: ./.github/actions/setup-devservices - if: steps.changes.outputs.api_docs == 'true' - with: - mode: migrations - - name: Setup sentry env uses: ./.github/actions/setup-sentry with: mode: migrations - skip-devservices: 'true' if: steps.changes.outputs.api_docs == 'true' - - name: Wait for devservices - if: steps.changes.outputs.api_docs == 'true' - run: | - sentry init - ./.github/actions/setup-devservices/wait.sh - - name: Checkout getsentry/sentry-api-schema uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 if: steps.changes.outputs.api_docs == 'true' diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 1ed5a0720e9a..35a7bad47fde 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -32,25 +32,12 @@ jobs: token: ${{ github.token }} filters: .github/file-filters.yml - - name: Start devservices early - uses: ./.github/actions/setup-devservices - if: steps.changes.outputs.api_docs == 'true' - with: - mode: migrations - - name: Setup sentry env uses: ./.github/actions/setup-sentry with: mode: migrations - skip-devservices: 'true' if: steps.changes.outputs.api_docs == 'true' - - name: Wait for devservices - if: steps.changes.outputs.api_docs == 'true' - run: | - sentry init - ./.github/actions/setup-devservices/wait.sh - - name: Checkout getsentry/sentry-api-schema if: steps.changes.outputs.api_docs == 'true' uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 diff --git a/AGENTS.md b/AGENTS.md index 017e343af6c1..535b35d1917e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,19 +130,13 @@ make reset-db #### Development Setup ```bash -# Start the full development server (requires devservices up) +# Start the development server pnpm run dev # Start only the UI development server with hot reload -# Proxies API requests to production sentry.io pnpm run dev-ui ``` -**Dev server URLs:** - -- Full devserver: http://dev.getsentry.net:8000 -- Frontend-only (`dev-ui`): https://sentry.dev.getsentry.net:7999/ - #### Typechecking Typechecking only works on the entire project. Individual files cannot be checked. diff --git a/eslint.config.ts b/eslint.config.ts index e965a5c74563..f2f7497d420f 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -405,7 +405,6 @@ export default typescript.config([ 'prefer-promise-reject-errors': 'off', // Disabled in favor of @typescript-eslint/prefer-promise-reject-errors 'object-shorthand': ['error', 'properties'], 'prefer-arrow-callback': ['error', {allowNamedFunctions: true}], - quotes: ['error', 'single', {avoidEscape: true, allowTemplateLiterals: false}], radix: 'error', 'require-await': 'off', // Disabled in favor of @typescript-eslint/require-await 'spaced-comment': [ diff --git a/jest.config.ts b/jest.config.ts index 006dc2c36b22..04d12ff49b36 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -273,14 +273,7 @@ if ( * node_modules, but some packages which use ES6 syntax only NEED to be * transformed. */ -const ESM_NODE_MODULES = [ - 'screenfull', - 'cbor2', - 'nuqs', - 'color', - 'marked', - '@sentry\\+sqlish', -]; +const ESM_NODE_MODULES = ['screenfull', 'cbor2', 'nuqs', 'color', 'marked']; const config: Config.InitialOptions = { verbose: false, @@ -308,11 +301,6 @@ const config: Config.InitialOptions = { '^echarts/(.*)': '/tests/js/sentry-test/mocks/echartsMock.js', '^zrender/(.*)': '/tests/js/sentry-test/mocks/echartsMock.js', - // @sentry/sqlish is ESM-only with `exports` that only define `import` - // conditions. Jest's CJS resolver can't follow them without explicit mapping. - '^@sentry/sqlish/react$': '/node_modules/@sentry/sqlish/dist/react.js', - '^@sentry/sqlish$': '/node_modules/@sentry/sqlish/dist/index.js', - // Disabled @sentry/toolbar in tests. It depends on iframes and global // window/cookies state. '@sentry/toolbar': '/tests/js/sentry-test/mocks/sentryToolbarMock.js', diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 921fc0bf5d34..cfa5664e557e 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -31,7 +31,7 @@ replays: 0007_organizationmember_replay_access seer: 0005_delete_seerorganizationsettings -sentry: 1064_eventattachment_date_expires_now +sentry: 1063_remove_customdynamicsamplingrule social_auth: 0003_social_auth_json_field diff --git a/package.json b/package.json index cb6d7974503f..2584865f8c2d 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,6 @@ "@sentry/node": "10.41.0-beta.0", "@sentry/react": "10.41.0-beta.0", "@sentry/release-parser": "^1.3.1", - "@sentry/sqlish": "1.0.1", "@sentry/status-page-list": "^0.6.1", "@sentry/toolbar": "1.0.0-beta.16", "@sentry/webpack-plugin": "4.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a00fd18d152f..21ac43b1c167 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -210,9 +210,6 @@ importers: '@sentry/release-parser': specifier: ^1.3.1 version: 1.3.1 - '@sentry/sqlish': - specifier: 1.0.1 - version: 1.0.1(react@19.2.3) '@sentry/status-page-list': specifier: ^0.6.1 version: 0.6.1 @@ -3503,14 +3500,6 @@ packages: '@sentry/release-parser@1.3.1': resolution: {integrity: sha512-/dGpCq+j3sJhqQ14RNEEL45Ot/rgq3jAlZDD/8ufeqq+W8p4gUhSrbGWCRL82NEIWY9SYwxYXGXjRcVPSHiA1Q==} - '@sentry/sqlish@1.0.1': - resolution: {integrity: sha512-8Ioewv2Qo4Y18T/O8M9FdoUD70VFUs+gu3XKsXQmEqFbgLl1fVkInmk1IL98fJzNADGCyRXsHGptsISPVW8OsA==} - peerDependencies: - react: '>=17' - peerDependenciesMeta: - react: - optional: true - '@sentry/status-page-list@0.6.1': resolution: {integrity: sha512-MCDLIEvzFKeDae1rSEU30O8QpcWqSECvDQaViW3eeGGLzxhr9rEI7rdKuHmouWT9hXCmYEK1W1DWKE534+Vv1g==} engines: {node: '>=14'} @@ -13266,10 +13255,6 @@ snapshots: '@sentry/release-parser@1.3.1': {} - '@sentry/sqlish@1.0.1(react@19.2.3)': - optionalDependencies: - react: 19.2.3 - '@sentry/status-page-list@0.6.1': {} '@sentry/toolbar@1.0.0-beta.16(react@19.2.3)': diff --git a/scripts/analyze-styled.ts b/scripts/analyze-styled.ts index 2dafee92cbbf..54b30c56c5b7 100644 --- a/scripts/analyze-styled.ts +++ b/scripts/analyze-styled.ts @@ -422,7 +422,7 @@ if (!config.startDate) { if (config.targetFile) { logger.fatal(`❌ File not found: ${config.targetFile}`); } else { - logger.fatal('❌ No .tsx files found in search directory'); + logger.fatal(`❌ No .tsx files found in search directory`); } process.exit(1); } diff --git a/scripts/build-js-loader.ts b/scripts/build-js-loader.ts index a64776b652fc..c8805d7f7807 100644 --- a/scripts/build-js-loader.ts +++ b/scripts/build-js-loader.ts @@ -17,7 +17,7 @@ import * as ts from 'typescript'; * then verify the build output of both generated `js.tmpl` files and check all three files in. */ -const header = '{% load sentry_helpers %}'; +const header = `{% load sentry_helpers %}`; const loaderScriptPath = './src/sentry/templates/sentry/js-sdk-loader.ts'; const loaderTmplPath = './src/sentry/templates/sentry/js-sdk-loader.js.tmpl'; const loaderMinTmplPath = './src/sentry/templates/sentry/js-sdk-loader.min.js.tmpl'; diff --git a/scripts/routes.ts b/scripts/routes.ts index 7ed56c4a0bae..5cb174a233b1 100644 --- a/scripts/routes.ts +++ b/scripts/routes.ts @@ -148,7 +148,7 @@ const useDefaults = (values.defaults as boolean) ?? false; if (!showAll && Object.keys(userParams).length === 0 && !useDefaults) { const bin = path.basename(process.argv[1] ?? ''); console.error(`Usage: node ${bin} [OPTIONS] -- [...]`); - console.error('Run with --help for full usage.'); + console.error(`Run with --help for full usage.`); process.exit(1); } diff --git a/scripts/type-coverage-diff.ts b/scripts/type-coverage-diff.ts index 4d67ad2bc719..1734df6a932f 100644 --- a/scripts/type-coverage-diff.ts +++ b/scripts/type-coverage-diff.ts @@ -529,7 +529,7 @@ async function main() { console.log( colors.dim( - '\n📝 Note: Only showing type issues on lines that were literally added (+) or removed (-) in git diff' + `\n📝 Note: Only showing type issues on lines that were literally added (+) or removed (-) in git diff` ) ); diff --git a/src/sentry/analytics/events/__init__.py b/src/sentry/analytics/events/__init__.py index 52be4e1b0c1e..b3d4d362bce5 100644 --- a/src/sentry/analytics/events/__init__.py +++ b/src/sentry/analytics/events/__init__.py @@ -8,7 +8,6 @@ from .api_token_deleted import * # noqa: F401,F403 from .auth_v2 import * # noqa: F401,F403 from .autofix_automation_events import * # noqa: F401,F403 -from .autofix_events import * # noqa: F401,F403 from .checkin_processing_error_stored import * # noqa: F401,F403 from .codeowners_assignment import * # noqa: F401,F403 from .codeowners_created import * # noqa: F401,F403 diff --git a/src/sentry/analytics/events/autofix_events.py b/src/sentry/analytics/events/autofix_events.py deleted file mode 100644 index 09e52c9acc53..000000000000 --- a/src/sentry/analytics/events/autofix_events.py +++ /dev/null @@ -1,89 +0,0 @@ -from sentry import analytics - - -@analytics.eventclass() -class AiAutofixPhaseEvent(analytics.Event): - organization_id: int - project_id: int - group_id: int - referrer: str | None - - -@analytics.eventclass("ai.autofix.root_cause.started") -class AiAutofixRootCauseStartedEvent(AiAutofixPhaseEvent): - pass - - -@analytics.eventclass("ai.autofix.solution.started") -class AiAutofixSolutionStartedEvent(AiAutofixPhaseEvent): - pass - - -@analytics.eventclass("ai.autofix.code_changes.started") -class AiAutofixCodeChangesStartedEvent(AiAutofixPhaseEvent): - pass - - -@analytics.eventclass("ai.autofix.root_cause.completed") -class AiAutofixRootCauseCompletedEvent(AiAutofixPhaseEvent): - pass - - -@analytics.eventclass("ai.autofix.solution.completed") -class AiAutofixSolutionCompletedEvent(AiAutofixPhaseEvent): - pass - - -@analytics.eventclass("ai.autofix.code_changes.completed") -class AiAutofixCodeChangesCompletedEvent(AiAutofixPhaseEvent): - pass - - -@analytics.eventclass("ai.autofix.impact_assessment.started") -class AiAutofixImpactAssessmentStartedEvent(AiAutofixPhaseEvent): - pass - - -@analytics.eventclass("ai.autofix.impact_assessment.completed") -class AiAutofixImpactAssessmentCompletedEvent(AiAutofixPhaseEvent): - pass - - -@analytics.eventclass("ai.autofix.triage.started") -class AiAutofixTriageStartedEvent(AiAutofixPhaseEvent): - pass - - -@analytics.eventclass("ai.autofix.triage.completed") -class AiAutofixTriageCompletedEvent(AiAutofixPhaseEvent): - pass - - -@analytics.eventclass("ai.autofix.pr_created.started") -class AiAutofixPrCreatedStartedEvent(AiAutofixPhaseEvent): - pass - - -@analytics.eventclass("ai.autofix.pr_created.completed") -class AiAutofixPrCreatedCompletedEvent(AiAutofixPhaseEvent): - pass - - -@analytics.eventclass("ai.autofix.agent_handoff") -class AiAutofixAgentHandoffEvent(AiAutofixPhaseEvent): - pass - - -analytics.register(AiAutofixRootCauseStartedEvent) -analytics.register(AiAutofixSolutionStartedEvent) -analytics.register(AiAutofixCodeChangesStartedEvent) -analytics.register(AiAutofixRootCauseCompletedEvent) -analytics.register(AiAutofixSolutionCompletedEvent) -analytics.register(AiAutofixCodeChangesCompletedEvent) -analytics.register(AiAutofixImpactAssessmentStartedEvent) -analytics.register(AiAutofixImpactAssessmentCompletedEvent) -analytics.register(AiAutofixTriageStartedEvent) -analytics.register(AiAutofixTriageCompletedEvent) -analytics.register(AiAutofixPrCreatedStartedEvent) -analytics.register(AiAutofixPrCreatedCompletedEvent) -analytics.register(AiAutofixAgentHandoffEvent) diff --git a/src/sentry/api/base.py b/src/sentry/api/base.py index 5d1069ce0749..0fdaf25536f3 100644 --- a/src/sentry/api/base.py +++ b/src/sentry/api/base.py @@ -75,7 +75,6 @@ "all_silo_endpoint", "internal_cell_silo_endpoint", "internal_all_silo_endpoint", - "internal_control_silo_endpoint", ] PAGINATION_DEFAULT_PER_PAGE = 100 @@ -724,12 +723,6 @@ def __call__(self, decorated_obj: Any) -> Any: mode 404s will be returned. """ -internal_control_silo_endpoint = EndpointSiloLimit(SiloMode.CONTROL, internal=True) -""" -Apply to endpoints that exist in CONTROL silo that -should not be included in the frontend URL mapping -""" - cell_silo_endpoint = EndpointSiloLimit(SiloMode.CELL) """ Apply to endpoints that exist in CELL silo. diff --git a/src/sentry/api/bases/organization_events.py b/src/sentry/api/bases/organization_events.py index 355501cdff00..291dfa07692f 100644 --- a/src/sentry/api/bases/organization_events.py +++ b/src/sentry/api/bases/organization_events.py @@ -29,7 +29,7 @@ from sentry.api.helpers.teams import get_teams from sentry.api.serializers.snuba import SnubaTSResultSerializer from sentry.api.utils import handle_query_errors -from sentry.discover.arithmetic import is_equation, parse_arithmetic, strip_equation +from sentry.discover.arithmetic import is_equation, strip_equation from sentry.discover.models import ( DatasetSourcesTypes, DiscoverSavedQuery, @@ -52,7 +52,7 @@ from sentry.search.eap.occurrences.rollout_utils import EAPOccurrencesComparator from sentry.search.eap.types import AdditionalQueries, SupportedTraceItemType from sentry.search.events.constants import DURATION_UNITS, SIZE_UNITS -from sentry.search.events.fields import get_function_alias, is_function +from sentry.search.events.fields import get_function_alias from sentry.search.events.types import SAMPLING_MODES, SnubaParams from sentry.snuba import discover from sentry.snuba.dataset import Dataset @@ -346,25 +346,22 @@ def handle_unit_meta( def _get_rate_unit(self, field: str) -> str | None: """Get the rate unit for a field by checking for known rate functions.""" - per_second_fns = {"eps", "sps", "tps", "sample_eps", "per_second", "per_second_if"} - per_minute_fns = {"epm", "spm", "tpm", "sample_epm", "per_minute", "per_minute_if"} - + # Only use the opening parenthesis to identify rate functions because + # some functions may contain arguments, making a direct string match not work. + per_second_fns = {"eps(", "sps(", "tps(", "sample_eps(", "per_second("} + per_minute_fns = {"epm(", "spm(", "tpm(", "sample_epm(", "per_minute("} + + if any(field.startswith(fn) for fn in per_second_fns): + return "1/second" + if any(field.startswith(fn) for fn in per_minute_fns): + return "1/minute" # For equation fields, check if any known rate function appears in the expression - if is_equation(field): - _, _, functions = parse_arithmetic(strip_equation(field)) - for function in functions: - if function_match := is_function(function): - function_name = function_match.group("function") - if function_name in per_second_fns: - return "1/second" - elif function_name in per_minute_fns: - return "1/minute" - else: - if function_match := is_function(field): - function_name = function_match.group("function") - if function_name in per_second_fns: + if field.startswith("equation|"): + for fn in per_second_fns: + if fn in field: return "1/second" - if function_name in per_minute_fns: + for fn in per_minute_fns: + if fn in field: return "1/minute" return None diff --git a/src/sentry/api/endpoints/internal/rpc.py b/src/sentry/api/endpoints/internal/rpc.py index aadac806d60d..14040dbf7a36 100644 --- a/src/sentry/api/endpoints/internal/rpc.py +++ b/src/sentry/api/endpoints/internal/rpc.py @@ -17,7 +17,6 @@ ) from sentry.hybridcloud.rpc.sig import SerializableFunctionValueException from sentry.utils.env import in_test_environment -from sentry.viewer_context import ViewerContext, viewer_context_scope @internal_all_silo_endpoint @@ -62,18 +61,8 @@ def post(self, request: Request, service_name: str, method_name: str) -> Respons sentry_sdk.capture_exception() raise ParseError from e - meta = request.data.get("meta") or {} - vc_data = meta.get("viewer_context") - vc = ViewerContext() - if vc_data: - try: - vc = ViewerContext.deserialize(vc_data) - except Exception as e: - sentry_sdk.capture_exception() - raise ParseError from e - try: - with viewer_context_scope(vc), auth_context.applied_to_request(request): + with auth_context.applied_to_request(request): result = dispatch_to_local_service(service_name, method_name, arguments) except RpcValidationException as e: return Response( diff --git a/src/sentry/api/endpoints/organization_ai_conversation_details.py b/src/sentry/api/endpoints/organization_ai_conversation_details.py index 5d1fc3ca5816..d36e96b5be78 100644 --- a/src/sentry/api/endpoints/organization_ai_conversation_details.py +++ b/src/sentry/api/endpoints/organization_ai_conversation_details.py @@ -12,7 +12,6 @@ from sentry.api.paginator import GenericOffsetPaginator from sentry.api.utils import handle_query_errors from sentry.models.organization import Organization -from sentry.search.eap.occurrences.query_utils import build_escaped_term_filter from sentry.search.eap.types import SearchResolverConfig from sentry.snuba.referrer import Referrer from sentry.snuba.spans_rpc import Spans @@ -126,7 +125,7 @@ def _fetch_conversation_spans( ): result = Spans.run_table_query( params=snuba_params, - query_string=build_escaped_term_filter("gen_ai.conversation.id", [conversation_id]), + query_string=f"gen_ai.conversation.id:{conversation_id}", selected_columns=selected_columns, orderby=["precise.start_ts"], offset=offset, diff --git a/src/sentry/api/endpoints/organization_ai_conversations.py b/src/sentry/api/endpoints/organization_ai_conversations.py index 59ca71c75d86..d3a48688f8c2 100644 --- a/src/sentry/api/endpoints/organization_ai_conversations.py +++ b/src/sentry/api/endpoints/organization_ai_conversations.py @@ -17,7 +17,6 @@ from sentry.api.serializers.rest_framework import OrganizationAIConversationsSerializer from sentry.api.utils import handle_query_errors from sentry.models.organization import Organization -from sentry.search.eap.occurrences.query_utils import build_escaped_term_filter from sentry.search.eap.resolver import SearchResolver from sentry.search.eap.types import EAPResponse, SearchResolverConfig from sentry.search.events.constants import NON_FAILURE_STATUS @@ -342,7 +341,7 @@ def _build_aggregations_query( ) -> TableQuery: return TableQuery( name="aggregations", - query_string=build_escaped_term_filter("gen_ai.conversation.id", conversation_ids), + query_string=f"gen_ai.conversation.id:[{','.join(conversation_ids)}]", selected_columns=[ "gen_ai.conversation.id", "failure_count()", @@ -366,7 +365,7 @@ def _build_enrichment_query( ) -> TableQuery: return TableQuery( name="enrichment", - query_string=f"{build_escaped_term_filter('gen_ai.conversation.id', conversation_ids)} has:gen_ai.operation.type", + query_string=f"gen_ai.conversation.id:[{','.join(conversation_ids)}] has:gen_ai.operation.type", selected_columns=[ "gen_ai.conversation.id", "gen_ai.operation.type", @@ -393,7 +392,7 @@ def _build_first_last_io_query( ) -> TableQuery: return TableQuery( name="first_last_io", - query_string=f"{build_escaped_term_filter('gen_ai.conversation.id', conversation_ids)} gen_ai.operation.type:ai_client", + query_string=f"gen_ai.conversation.id:[{','.join(conversation_ids)}] gen_ai.operation.type:ai_client", selected_columns=[ "gen_ai.conversation.id", "gen_ai.input.messages", diff --git a/src/sentry/api/endpoints/organization_insights_tree.py b/src/sentry/api/endpoints/organization_insights_tree.py index a0dbf053fbd7..f089e7cb8533 100644 --- a/src/sentry/api/endpoints/organization_insights_tree.py +++ b/src/sentry/api/endpoints/organization_insights_tree.py @@ -1,6 +1,5 @@ import logging import re -from typing import NamedTuple from rest_framework.request import Request from rest_framework.response import Response @@ -13,33 +12,6 @@ logger = logging.getLogger(__name__) -class _ExtractionPattern(NamedTuple): - """Extraction pattern for component type and path from span.description. - group(1) = kind/type (mapped through kind_map when present), group(2) = path. - """ - - regex: re.Pattern[str] - kind_map: dict[str, str] | None = None - - -# SDK <10.32.0: '{component_type} ({path})' -# e.g. 'Page Server Component (/dashboard)' -_ParenthesisPattern = _ExtractionPattern( - regex=re.compile(r"^(.*?)\s+\((.*?)\)$"), -) - -# SDK >=10.32.0: 'resolve {page|root layout|layout} server component "{route_or_segment}"' -# e.g. 'resolve page server component "/dashboard"' -_ResolvePattern = _ExtractionPattern( - regex=re.compile(r'^resolve (page|root layout|layout) server component(?:\s+"(.*)")?$'), - kind_map={ - "page": "Page Server Component", - "layout": "Layout Server Component", - "root layout": "Layout Server Component", - }, -) - - @cell_silo_endpoint class OrganizationInsightsTreeEndpoint(OrganizationEventsEndpoint): """ @@ -69,27 +41,22 @@ def get(self, request: Request, organization: Organization) -> Response: response = super().get(request, organization) return self._separate_span_description_info(response) - _EXTRACTION_PATTERNS = (_ParenthesisPattern, _ResolvePattern) - - @staticmethod - def _parse_path(path: str | None) -> list[str]: - if not path: - return [] - parts = path.strip("/").split("/") - return parts if parts != [""] else [] - def _separate_span_description_info(self, response): - for line in response.data["data"]: - component_type = None - path_components: list[str] = [] - - for pattern, kind_map in self._EXTRACTION_PATTERNS: - match = pattern.match(line["span.description"]) - if match: - component_type = kind_map[match.group(1)] if kind_map else match.group(1) - path_components = self._parse_path(match.group(2)) - break + # Regex to split string into '{component_type}{space}({path})' + pattern = re.compile(r"^(.*?)\s+\((.*?)\)$") + for line in response.data["data"]: + match = pattern.match(line["span.description"]) + if match: + component_type = match.group(1) + path = match.group(2) + path_components = path.strip("/").split("/") + if not path_components or (len(path_components) == 1 and path_components[0] == ""): + path_components = [] # Handle root path case + + else: + component_type = None + path_components = [] line["function.nextjs.component_type"] = component_type line["function.nextjs.path"] = path_components diff --git a/src/sentry/api/serializers/models/groupsearchview.py b/src/sentry/api/serializers/models/groupsearchview.py index 1035985595b4..a8f3f88f0868 100644 --- a/src/sentry/api/serializers/models/groupsearchview.py +++ b/src/sentry/api/serializers/models/groupsearchview.py @@ -1,8 +1,6 @@ from collections.abc import MutableMapping from typing import Any, TypedDict -from django.db.models import prefetch_related_objects - from sentry.api.serializers import Serializer, register from sentry.models.groupsearchview import GroupSearchView from sentry.models.groupsearchviewlastvisited import GroupSearchViewLastVisited @@ -35,7 +33,6 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def get_attrs(self, item_list, user, **kwargs) -> MutableMapping[Any, Any]: - prefetch_related_objects(item_list, "projects") attrs: MutableMapping[Any, Any] = {} last_visited_views = GroupSearchViewLastVisited.objects.filter( @@ -71,7 +68,7 @@ def get_attrs(self, item_list, user, **kwargs) -> MutableMapping[Any, Any]: return attrs def serialize(self, obj, attrs, user, **kwargs) -> GroupSearchViewSerializerResponse: - projects = [-1] if obj.is_all_projects else [p.id for p in obj.projects.all()] + projects = [-1] if obj.is_all_projects else list(obj.projects.values_list("id", flat=True)) return { "id": str(obj.id), diff --git a/src/sentry/api/serializers/models/groupsearchviewstarred.py b/src/sentry/api/serializers/models/groupsearchviewstarred.py index 361ba0eab860..2335a063b8f0 100644 --- a/src/sentry/api/serializers/models/groupsearchviewstarred.py +++ b/src/sentry/api/serializers/models/groupsearchviewstarred.py @@ -3,6 +3,7 @@ from sentry.api.serializers import Serializer, register, serialize from sentry.api.serializers.models.groupsearchview import ( GroupSearchViewSerializer, + GroupSearchViewSerializerResponse, ) from sentry.models.groupsearchviewstarred import GroupSearchViewStarred from sentry.models.savedsearch import SORT_LITERALS @@ -27,14 +28,13 @@ def __init__(self, *args, **kwargs): self.organization = kwargs.pop("organization", None) super().__init__(*args, **kwargs) - def get_attrs(self, item_list, user, **kwargs): - views = [item.group_search_view for item in item_list] - serialized_views = serialize( - views, + def serialize(self, obj, attrs, user, **kwargs) -> GroupSearchViewStarredSerializerResponse: + serialized_view: GroupSearchViewSerializerResponse = serialize( + obj.group_search_view, user, - serializer=GroupSearchViewSerializer(organization=self.organization), + serializer=GroupSearchViewSerializer( + organization=self.organization, + ), ) - return dict(zip(item_list, serialized_views)) - def serialize(self, obj, attrs, user, **kwargs) -> GroupSearchViewStarredSerializerResponse: - return attrs + return serialized_view diff --git a/src/sentry/api/serializers/models/team.py b/src/sentry/api/serializers/models/team.py index 50125537f5f6..750394497ba4 100644 --- a/src/sentry/api/serializers/models/team.py +++ b/src/sentry/api/serializers/models/team.py @@ -249,9 +249,7 @@ def get_attrs( if self._expand("projects"): project_teams = ProjectTeam.objects.get_for_teams_with_org_cache(item_list) - - # A project can be on multiple teams, dedupe before serializing - projects = list({pt.project_id: pt.project for pt in project_teams}.values()) + projects = [pt.project for pt in project_teams] projects_by_id = { project.id: data diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index 938539fc20af..fad013d5f153 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -990,8 +990,6 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: "sentry.workflow_engine.tasks.cleanup", "sentry.tasks.seer.explorer_index", "sentry.tasks.seer.context_engine_index", - "sentry.tasks.seer.lightweight_rca_cluster", - "sentry.tasks.seer.night_shift", # Used for tests "sentry.taskworker.tasks.examples", ) @@ -1174,11 +1172,6 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: # Run once a month at midnight "schedule": crontab("0", "0", "*", "1", "*"), }, - "seer-night-shift": { - "task": "seer:sentry.tasks.seer.night_shift.schedule_night_shift", - # Run daily at 10:00 AM UTC (2/3 AM Pacific) - "schedule": crontab("0", "10", "*", "*", "*"), - }, "refresh-artifact-bundles-in-use": { "task": "attachments:sentry.debug_files.tasks.refresh_artifact_bundles_in_use", "schedule": crontab("*/1", "*", "*", "*", "*"), diff --git a/src/sentry/dashboards/models/generate_dashboard_artifact.py b/src/sentry/dashboards/models/generate_dashboard_artifact.py index 76f8dd4cd3b9..3f54da0d96e7 100644 --- a/src/sentry/dashboards/models/generate_dashboard_artifact.py +++ b/src/sentry/dashboards/models/generate_dashboard_artifact.py @@ -1,39 +1,33 @@ from __future__ import annotations -from typing import Any, Literal, TypeAlias +from typing import Any, Literal -from pydantic import BaseModel, Field, root_validator, validator +from pydantic import BaseModel, Field, validator from sentry.models.dashboard import Dashboard -from sentry.models.dashboard_widget import DashboardWidgetDisplayTypes, DashboardWidgetTypes GRID_WIDTH = 6 -DISPLAY_TYPE_BLOCKLIST: set[str] = { - "details", - "server_tree", - "rage_and_dead_clicks", - "wheel", - "agents_traces_table", -} - -# Most of these are deprecated, not selectable in the UI, or don't make sense for generated dashboards. -WIDGET_TYPE_BLOCKLIST: set[str] = { - "discover", - "metrics", - "transaction-like", - "preprod-app-size", -} - -_ALLOWED_DISPLAY_TYPES = tuple( - t for t in DashboardWidgetDisplayTypes.TYPE_NAMES if t not in DISPLAY_TYPE_BLOCKLIST -) -_ALLOWED_WIDGET_TYPES = tuple( - t for t in DashboardWidgetTypes.TYPE_NAMES if t not in WIDGET_TYPE_BLOCKLIST -) - -DisplayType: TypeAlias = Literal[tuple(_ALLOWED_DISPLAY_TYPES)] # type: ignore[valid-type] -WidgetType: TypeAlias = Literal[tuple(_ALLOWED_WIDGET_TYPES)] # type: ignore[valid-type] +# Hardcode maintained lists for now, not all display types are well suited for dashboard generation. +DisplayType = Literal[ + "line", + "area", + "stacked_area", + "bar", + "table", + "big_number", + "top_n", + "categorical_bar", +] + +# Hardcode maintained lists for now, not all widget types are well suited for dashboard generation. +WidgetType = Literal[ + "issue", + "error-events", + "spans", + "logs", + "tracemetrics", +] Intervals = Literal["5m", "15m", "30m", "1h", "4h", "12h", "24h"] @@ -95,9 +89,7 @@ class GeneratedWidgetLayout(BaseModel): le=GRID_WIDTH, ) h: int = Field( - default=2, - description="Height in rows (1+). A height of 2 is approximately 256px. For non big_number widgets, this should be at least 2.", - ge=1, + default=2, description="Height in rows (1+). A height of 2 is approximately 256px.", ge=1 ) min_h: int = Field(default=2, description="Minimum height in rows (1+).") @@ -130,13 +122,12 @@ class GeneratedWidget(BaseModel): title: str = Field(..., max_length=255) # Matches serializer description: str = Field( - ..., - description="A short description of the widget. This is displayed in the dashboard UI as a hoverable tooltip. For text widget types, this is the markdown text content displayed to the user. Should not exceed 255 characters for non-text widgets.", - ) + ..., max_length=255 + ) # Length matches serializer, required field for generation display_type: DisplayType - widget_type: WidgetType | None = Field( + widget_type: WidgetType = Field( ..., - description="Dataset to query. Use 'spans' as the default — it covers most use cases. Use 'error-events' for error-specific data, 'issue' for issue tracking, 'logs' for log data, 'tracemetrics' for trace metrics. Text widgets do not have a widget_type and should be set to None. Required for non-text widgets.", + description="Dataset to query. Use 'spans' as the default — it covers most use cases. Use 'error-events' for error-specific data, 'issue' for issue tracking, 'logs' for log data, 'tracemetrics' for trace metrics.", ) queries: list[GeneratedWidgetQuery] layout: GeneratedWidgetLayout @@ -148,37 +139,9 @@ class GeneratedWidget(BaseModel): ) interval: Intervals = Field(default="1h") - @root_validator - def check_text_widget_constraints(cls, values: dict[str, Any]) -> dict[str, Any]: - display_type = values.get("display_type") - is_text = display_type == "text" - - description = values.get("description", "") - if not is_text and len(description) > 255: - raise ValueError( - f"Description must not exceed 255 characters for non-text widgets (got {len(description)})" - ) - - widget_type = values.get("widget_type") - if not is_text and widget_type is None: - raise ValueError("widget_type is required for non-text widgets") - - if is_text and widget_type is not None: - raise ValueError("widget_type is not allowed for text widgets") - - return values - class GeneratedDashboard(BaseModel): """A complete dashboard definition on a 6-column grid. Widget widths per row must sum to 6. This is the sole output artifact.""" title: str = Field(..., max_length=255) # Matches serializer - projects: list[int] = Field( - default=[], - description='Project ids to scope the dashboard to. Empty list means "My Projects".', - ) - environment: list[str] = Field( - default=[], - description="Environment names to filter by (e.g. ['production', 'staging']). Empty list means all environments.", - ) widgets: list[GeneratedWidget] = Field(..., max_items=Dashboard.MAX_WIDGETS) diff --git a/src/sentry/dashboards/on_completion_hook.py b/src/sentry/dashboards/on_completion_hook.py index 9e4142945b85..a148cdf86447 100644 --- a/src/sentry/dashboards/on_completion_hook.py +++ b/src/sentry/dashboards/on_completion_hook.py @@ -37,15 +37,8 @@ def _validate_with_serializer( if project is None: return None - # Strip fields that require request-scoped context (project permissions, - # environment access) which we don't have in the completion hook. - # The serializer validation here targets widget-level correctness. - artifact_data = artifact.dict() - artifact_data.pop("projects", None) - artifact_data.pop("environment", None) - serializer = DashboardSerializer( - data=artifact_data, + data=artifact.dict(), context={ "organization": organization, "request": SimpleNamespace(user=None), # mock request to satisfy serializer diff --git a/src/sentry/deletions/defaults/detector.py b/src/sentry/deletions/defaults/detector.py index 305f117f53fa..359100a8dd56 100644 --- a/src/sentry/deletions/defaults/detector.py +++ b/src/sentry/deletions/defaults/detector.py @@ -10,29 +10,10 @@ class DetectorDeletionTask(ModelDeletionTask[Detector]): manager_name = "objects_for_deletion" def get_child_relations(self, instance: Detector) -> list[BaseRelation]: - from sentry.incidents.models.alert_rule import AlertRule - from sentry.workflow_engine.models import ( - AlertRuleDetector, - DataConditionGroup, - DataSource, - ) + from sentry.workflow_engine.models import DataConditionGroup, DataSource model_relations: list[BaseRelation] = [] - # If this detector was dual-written from an AlertRule, cascade deletion - # to the AlertRule. AlertRuleDeletionTask will handle cleaning up - # AlertRuleDetector, AlertRuleWorkflow, triggers, and incidents. - # NOTE: AlertRule must be deleted before DataSource so that - # QuerySubscriptionDeletionTask sees no remaining AlertRule referencing - # the SnubaQuery and can safely delete it. - alert_rule_ids = list( - AlertRuleDetector.objects.filter(detector_id=instance.id).values_list( - "alert_rule_id", flat=True - ) - ) - if alert_rule_ids: - model_relations.append(ModelRelation(AlertRule, {"id__in": alert_rule_ids})) - # check that no other rows are related to the data source data_source_ids = DataSource.objects.filter(detector=instance.id).values_list( "id", flat=True @@ -45,9 +26,9 @@ def get_child_relations(self, instance: Detector) -> list[BaseRelation]: ): model_relations.append(ModelRelation(DataSource, {"detector": instance.id})) - if instance.workflow_condition_group_id: + if instance.workflow_condition_group: model_relations.append( - ModelRelation(DataConditionGroup, {"id": instance.workflow_condition_group_id}) + ModelRelation(DataConditionGroup, {"id": instance.workflow_condition_group.id}) ) return model_relations diff --git a/src/sentry/deletions/defaults/workflow.py b/src/sentry/deletions/defaults/workflow.py index 57244fd59ff0..8f0b43555145 100644 --- a/src/sentry/deletions/defaults/workflow.py +++ b/src/sentry/deletions/defaults/workflow.py @@ -21,9 +21,9 @@ def get_child_relations(self, instance: Workflow) -> list[BaseRelation]: model_relations.append(ModelRelation(DataConditionGroup, {"id__in": action_filter_ids})) - if instance.when_condition_group_id: + if instance.when_condition_group: model_relations.append( - ModelRelation(DataConditionGroup, {"id": instance.when_condition_group_id}) + ModelRelation(DataConditionGroup, {"id": instance.when_condition_group.id}) ) return model_relations diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 07576521edb3..e5800f7556f5 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -2006,7 +2006,7 @@ def _get_severity_metadata_for_group( Returns {} if conditions aren't met or on exception. """ - from sentry.workflow_engine.receivers.project_workflows import PLATFORMS_WITH_PRIORITY_ALERTS + from sentry.receivers.rules import PLATFORMS_WITH_PRIORITY_ALERTS if killswitch_matches_context( "issues.severity.skip-seer-requests", {"project_id": event.project_id} diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index f48a566b091f..25d5dd4152b3 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -92,8 +92,6 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:dashboards-text-widgets", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable AI-powered dashboard generation via Seer manager.add("organizations:dashboards-ai-generate", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Enable AI-powered dashboard editing via Seer - manager.add("organizations:dashboards-ai-generate-edit", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Data Secrecy manager.add("organizations:data-secrecy", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Data Secrecy v2 (with Break the Glass feature) @@ -300,8 +298,6 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:seer-explorer", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable Seer Explorer Index job manager.add("organizations:seer-explorer-index", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Enable Seer Night Shift nightly autofix cron - manager.add("organizations:seer-night-shift", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable streaming responses for Seer Explorer manager.add("organizations:seer-explorer-streaming", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable context engine for Seer Explorer @@ -330,6 +326,8 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:autofix-on-explorer-v2", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable Seer Workflows in Slack manager.add("organizations:seer-slack-workflows", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) + # Enable new compact issue alert UI in Slack + manager.add("organizations:slack-compact-alerts", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable Seer Explorer in Slack via @mentions manager.add("organizations:seer-slack-explorer", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable search query attribute validation @@ -462,17 +460,11 @@ def register_temporary_features(manager: FeatureManager) -> None: # Use workflow engine exclusively for legacy issue alert rule.get results. # See src/sentry/workflow_engine/docs/legacy_backport.md for context. manager.add("organizations:workflow-engine-issue-alert-endpoints-get", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) - # Use workflow engine exclusively for OrganizationIncidentIndexEndpoint.get results. - # See src/sentry/workflow_engine/docs/legacy_backport.md for context. - manager.add("organizations:workflow-engine-orgincidentindex-get", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Use workflow engine exclusively for OrganizationAlertRuleDetailsEndpoint.get results. # See src/sentry/workflow_engine/docs/legacy_backport.md for context. manager.add("organizations:workflow-engine-orgalertruledetails-get", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable metric detector limits by plan type manager.add("organizations:workflow-engine-metric-detector-limit", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Use workflow engine exclusively for OrganizationAlertRuleIndexEndpoint.get results. - # See src/sentry/workflow_engine/docs/legacy_backport.md for context. - manager.add("organizations:workflow-engine-orgalertruleindex-get", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable EventUniqueUserFrequencyConditionWithConditions special alert condition manager.add("organizations:event-unique-user-frequency-condition-with-conditions", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable our logs product (known internally as ourlogs) in UI and backend diff --git a/src/sentry/hybridcloud/rpc/service.py b/src/sentry/hybridcloud/rpc/service.py index 34070c61d1c6..5a4bb2058ce3 100644 --- a/src/sentry/hybridcloud/rpc/service.py +++ b/src/sentry/hybridcloud/rpc/service.py @@ -35,7 +35,6 @@ from sentry.types.cell import Cell, CellMappingNotFound from sentry.utils import json, metrics from sentry.utils.env import in_test_environment -from sentry.viewer_context import get_viewer_context if TYPE_CHECKING: from sentry.hybridcloud.rpc.resolvers import CellResolutionStrategy @@ -572,13 +571,8 @@ def get_method_timeout(self) -> float: return settings.RPC_TIMEOUT def _send_to_remote_silo(self, use_test_client: bool) -> Any: - vc = get_viewer_context() - meta: dict[str, Any] = {} - if vc is not None: - meta["viewer_context"] = vc.serialize() - request_body = { - "meta": meta, + "meta": {}, # reserved for future use "args": self.serial_arguments, } data = json.dumps(request_body).encode(_RPC_CONTENT_CHARSET) diff --git a/src/sentry/identity/oauth2.py b/src/sentry/identity/oauth2.py index a6b29b09c93d..03f59c1c0a41 100644 --- a/src/sentry/identity/oauth2.py +++ b/src/sentry/identity/oauth2.py @@ -147,24 +147,22 @@ def get_pipeline_views(self) -> list[PipelineView[IdentityPipeline]]: ), ] - def make_oauth_api_step(self, **kwargs: Any) -> OAuth2ApiStep: + def get_pipeline_api_steps(self) -> list[OAuth2ApiStep]: redirect_url = self.config.get( "redirect_url", reverse("sentry-extension-setup", kwargs={"provider_id": "default"}), ) - return OAuth2ApiStep( - authorize_url=self.get_oauth_authorize_url(), - client_id=self.get_oauth_client_id(), - client_secret=self.get_oauth_client_secret(), - access_token_url=self.get_oauth_access_token_url(), - scope=" ".join(self.get_oauth_scopes()), - redirect_url=redirect_url, - verify_ssl=self.config.get("verify_ssl", True), - **kwargs, - ) - - def get_pipeline_api_steps(self) -> list[OAuth2ApiStep]: - return [self.make_oauth_api_step()] + return [ + OAuth2ApiStep( + authorize_url=self.get_oauth_authorize_url(), + client_id=self.get_oauth_client_id(), + client_secret=self.get_oauth_client_secret(), + access_token_url=self.get_oauth_access_token_url(), + scope=" ".join(self.get_oauth_scopes()), + redirect_url=redirect_url, + verify_ssl=self.config.get("verify_ssl", True), + ), + ] def get_refresh_token_params( self, refresh_token: str, identity: Identity | RpcIdentity, **kwargs: Any diff --git a/src/sentry/incidents/endpoints/organization_alert_rule_index.py b/src/sentry/incidents/endpoints/organization_alert_rule_index.py index ca4b2ba50b8f..ca0662b1ed06 100644 --- a/src/sentry/incidents/endpoints/organization_alert_rule_index.py +++ b/src/sentry/incidents/endpoints/organization_alert_rule_index.py @@ -235,10 +235,6 @@ class AlertRuleFetchMixin(Endpoint): Can be used with any endpoint base class (OrganizationEndpoint, ProjectEndpoint, etc). """ - # Subclasses may set a per-method granular flag (e.g. for GET) that is OR'd - # with the broad workflow-engine-rule-serializers flag. - workflow_engine_method_flags: dict[str, str] = {} - def fetch_metric_alerts( self, request: Request, @@ -256,11 +252,8 @@ def fetch_metric_alerts( extra={"organization": organization.id}, ) - method_flag = self.workflow_engine_method_flags.get(request.method or "") - use_workflow_engine = ( - features.has("organizations:workflow-engine-rule-serializers", organization) - or method_flag is not None - and features.has(method_flag, organization) + use_workflow_engine = features.has( + "organizations:workflow-engine-rule-serializers", organization ) if use_workflow_engine: @@ -915,9 +908,6 @@ class OrganizationAlertRuleIndexEndpoint(OrganizationAlertRuleBaseEndpoint, Aler "POST": ApiPublishStatus.PUBLIC, } permission_classes = (OrganizationAlertRulePermission,) - workflow_engine_method_flags = { - "GET": "organizations:workflow-engine-orgalertruleindex-get", - } @extend_schema( operation_id="(DEPRECATED) List an Organization's Metric Alert Rules", diff --git a/src/sentry/incidents/endpoints/organization_incident_index.py b/src/sentry/incidents/endpoints/organization_incident_index.py index 7eddb7f2d9ae..756adf3aa3e5 100644 --- a/src/sentry/incidents/endpoints/organization_incident_index.py +++ b/src/sentry/incidents/endpoints/organization_incident_index.py @@ -81,7 +81,7 @@ def get(self, request: Request, organization: Organization) -> Response: use_workflow_engine = features.has( "organizations:workflow-engine-rule-serializers", organization - ) or features.has("organizations:workflow-engine-orgincidentindex-get", organization) + ) if use_workflow_engine: return self._get_workflow_engine( diff --git a/src/sentry/incidents/serializers/alert_rule.py b/src/sentry/incidents/serializers/alert_rule.py index 313510949d5a..38098ea15515 100644 --- a/src/sentry/incidents/serializers/alert_rule.py +++ b/src/sentry/incidents/serializers/alert_rule.py @@ -211,6 +211,13 @@ def validate(self, data): ) self._validate_critical_warning_triggers(threshold_type, critical, warning) + if data.get("dataset") == Dataset.EventsAnalyticsPlatform: + time_window = data["time_window"] + if time_window < 5: + raise serializers.ValidationError( + "Invalid Time Window: Time window for this alert type must be at least 5 minutes." + ) + return data def _translate_thresholds(self, threshold_type, comparison_delta, triggers, data): diff --git a/src/sentry/integrations/api/endpoints/integration_proxy.py b/src/sentry/integrations/api/endpoints/integration_proxy.py index 030f70d8f676..18a427d92b76 100644 --- a/src/sentry/integrations/api/endpoints/integration_proxy.py +++ b/src/sentry/integrations/api/endpoints/integration_proxy.py @@ -17,7 +17,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import Endpoint, internal_control_silo_endpoint +from sentry.api.base import Endpoint, control_silo_endpoint from sentry.auth.exceptions import IdentityNotValid from sentry.constants import ObjectStatus from sentry.integrations.models.organization_integration import OrganizationIntegration @@ -72,7 +72,7 @@ class IntegrationProxyFailureMetricType(StrEnum): FAILED_VALIDATION = "failed_validation" -@internal_control_silo_endpoint +@control_silo_endpoint class InternalIntegrationProxyEndpoint(Endpoint): publish_status = defaultdict(lambda: ApiPublishStatus.PRIVATE) owner = ApiOwner.HYBRID_CLOUD diff --git a/src/sentry/integrations/github/tasks/sync_repos.py b/src/sentry/integrations/github/tasks/sync_repos.py index c008433b1308..06740e1c7a49 100644 --- a/src/sentry/integrations/github/tasks/sync_repos.py +++ b/src/sentry/integrations/github/tasks/sync_repos.py @@ -246,7 +246,7 @@ def github_repo_sync_beat() -> None: name="github_repo_sync", schedule_key="github-repo-sync-beat", queryset=OrganizationIntegration.objects.filter( - integration__provider__in=["github", "github_enterprise"], + integration__provider="github", integration__status=ObjectStatus.ACTIVE, status=ObjectStatus.ACTIVE, ), diff --git a/src/sentry/integrations/github/webhook.py b/src/sentry/integrations/github/webhook.py index 06b3b70bee15..a73d4c583266 100644 --- a/src/sentry/integrations/github/webhook.py +++ b/src/sentry/integrations/github/webhook.py @@ -13,7 +13,6 @@ import orjson import sentry_sdk from dateutil.parser import parse as parse_date -from django.conf import settings from django.db import IntegrityError, router, transaction from django.http import HttpRequest, HttpResponse from django.utils.crypto import constant_time_compare @@ -1159,7 +1158,6 @@ def handle(self, request: HttpRequest) -> HttpResponse: "type": IntegrationProviderSlug.GITHUB.value, }, silo="region" if SiloMode.get_current_mode() == SiloMode.CELL else "control", - is_dev=settings.IS_DEV, ) return HttpResponse(status=204) diff --git a/src/sentry/integrations/github_enterprise/webhook.py b/src/sentry/integrations/github_enterprise/webhook.py index fc9c005b0ccc..61c3b59ef649 100644 --- a/src/sentry/integrations/github_enterprise/webhook.py +++ b/src/sentry/integrations/github_enterprise/webhook.py @@ -8,7 +8,6 @@ import orjson import sentry_sdk -from django.conf import settings from django.http import HttpRequest, HttpResponse from django.utils.crypto import constant_time_compare from django.utils.decorators import method_decorator @@ -22,7 +21,6 @@ from sentry.integrations.github.webhook import ( GitHubWebhook, InstallationEventWebhook, - InstallationRepositoriesEventWebhook, IssuesEventWebhook, PullRequestEventWebhook, PushEventWebhook, @@ -108,12 +106,6 @@ class GitHubEnterpriseInstallationEventWebhook(GitHubEnterpriseWebhook, Installa pass -class GitHubEnterpriseInstallationRepositoriesEventWebhook( - GitHubEnterpriseWebhook, InstallationRepositoriesEventWebhook -): - pass - - class GitHubEnterprisePushEventWebhook(GitHubEnterpriseWebhook, PushEventWebhook): pass @@ -341,7 +333,6 @@ def _handle(self, request: HttpRequest) -> HttpResponse: "type": IntegrationProviderSlug.GITHUB_ENTERPRISE.value, }, silo="region", - is_dev=settings.IS_DEV, ) return HttpResponse(status=204) @@ -357,7 +348,6 @@ class GitHubEnterpriseWebhookEndpoint(GitHubEnterpriseWebhookBase): "push": GitHubEnterprisePushEventWebhook, "pull_request": GitHubEnterprisePullRequestEventWebhook, "installation": GitHubEnterpriseInstallationEventWebhook, - "installation_repositories": GitHubEnterpriseInstallationRepositoriesEventWebhook, "issues": GitHubEnterpriseIssuesEventWebhook, } diff --git a/src/sentry/integrations/slack/integration.py b/src/sentry/integrations/slack/integration.py index 0108de382649..6d04539c69f6 100644 --- a/src/sentry/integrations/slack/integration.py +++ b/src/sentry/integrations/slack/integration.py @@ -10,7 +10,6 @@ from slack_sdk.errors import SlackApiError from sentry.identity.pipeline import IdentityPipeline -from sentry.identity.slack.provider import SlackIdentityProvider from sentry.integrations.base import ( FeatureDescription, IntegrationData, @@ -37,7 +36,7 @@ ) from sentry.notifications.platform.target import IntegrationNotificationTarget from sentry.organizations.services.organization.model import RpcOrganization -from sentry.pipeline.views.base import ApiPipelineSteps, PipelineView +from sentry.pipeline.views.base import PipelineView from sentry.pipeline.views.nested import NestedPipelineView from sentry.shared_integrations.exceptions import IntegrationError from sentry.utils.http import absolute_uri @@ -336,7 +335,6 @@ def _get_oauth_scopes(self) -> frozenset[str]: return self.identity_oauth_scopes setup_dialog_config = {"width": 600, "height": 900} - setup_url_path = "/extensions/slack/setup/" def _identity_pipeline_view(self) -> PipelineView[IntegrationPipeline]: return NestedPipelineView( @@ -346,28 +344,13 @@ def _identity_pipeline_view(self) -> PipelineView[IntegrationPipeline]: config={ "oauth_scopes": self._get_oauth_scopes(), "user_scopes": self.user_scopes, - "redirect_url": absolute_uri(self.setup_url_path), + "redirect_url": absolute_uri("/extensions/slack/setup/"), }, ) def get_pipeline_views(self) -> Sequence[PipelineView[IntegrationPipeline]]: return [self._identity_pipeline_view()] - def _make_identity_provider(self) -> SlackIdentityProvider: - return SlackIdentityProvider( - oauth_scopes=self._get_oauth_scopes(), - redirect_url=absolute_uri(self.setup_url_path), - ) - - def get_pipeline_api_steps(self) -> ApiPipelineSteps[IntegrationPipeline]: - provider = self._make_identity_provider() - return [ - provider.make_oauth_api_step( - bind_key="oauth_data", - extra_authorize_params={"user_scope": " ".join(self.user_scopes)}, - ), - ] - def _get_team_info(self, access_token: str) -> Any: # Manually add authorization since this method is part of slack installation @@ -382,13 +365,7 @@ def _get_team_info(self, access_token: str) -> Any: raise IntegrationError("Could not retrieve Slack team information.") def build_integration(self, state: Mapping[str, Any]) -> IntegrationData: - # TODO: legacy views write token data to state["identity"]["data"] via - # NestedPipelineView. API steps write directly to state["oauth_data"]. - # Remove the legacy path once the old views are retired. - if "oauth_data" in state: - data = state["oauth_data"] - else: - data = state["identity"]["data"] + data = state["identity"]["data"] assert data["ok"] access_token = data["access_token"] diff --git a/src/sentry/integrations/slack/staging/integration.py b/src/sentry/integrations/slack/staging/integration.py index 68ebee8e77f6..a0914bd9827b 100644 --- a/src/sentry/integrations/slack/staging/integration.py +++ b/src/sentry/integrations/slack/staging/integration.py @@ -5,7 +5,6 @@ from typing import Any from sentry.identity.pipeline import IdentityPipeline -from sentry.identity.slack.provider import SlackStagingIdentityProvider from sentry.integrations.base import ( IntegrationData, ) @@ -23,17 +22,10 @@ class SlackStagingIntegrationProvider(SlackIntegrationProvider): key = IntegrationProviderSlug.SLACK_STAGING.value name = "Slack (Staging)" requires_feature_flag = True - setup_url_path = "/extensions/slack-staging/setup/" def _get_oauth_scopes(self) -> frozenset[str]: return self.identity_oauth_scopes | self.extended_oauth_scopes - def _make_identity_provider(self) -> SlackStagingIdentityProvider: - return SlackStagingIdentityProvider( - oauth_scopes=self._get_oauth_scopes(), - redirect_url=absolute_uri(self.setup_url_path), - ) - def _identity_pipeline_view(self) -> PipelineView[IntegrationPipeline]: return NestedPipelineView( bind_key="identity", @@ -42,7 +34,7 @@ def _identity_pipeline_view(self) -> PipelineView[IntegrationPipeline]: config={ "oauth_scopes": self._get_oauth_scopes(), "user_scopes": self.user_scopes, - "redirect_url": absolute_uri(self.setup_url_path), + "redirect_url": absolute_uri("/extensions/slack-staging/setup/"), }, ) diff --git a/src/sentry/management/commands/generate_controlsilo_urls.py b/src/sentry/management/commands/generate_controlsilo_urls.py index 5b66c8911525..c9b9aa335af2 100644 --- a/src/sentry/management/commands/generate_controlsilo_urls.py +++ b/src/sentry/management/commands/generate_controlsilo_urls.py @@ -133,11 +133,7 @@ def handle(self, **options): continue silo_limit = view_func.silo_limit - if ( - len(silo_limit.modes) == 1 - and SiloMode.CONTROL in silo_limit.modes - and not silo_limit.internal - ): + if len(silo_limit.modes) == 1 and SiloMode.CONTROL in silo_limit.modes: simple_pattern = simplify_regex(info.pattern) url_patterns.append(simple_pattern) diff --git a/src/sentry/migrations/1060_eventattachment_date_expires.py b/src/sentry/migrations/1060_eventattachment_date_expires.py index f65eee772f4e..e238ab8a268a 100644 --- a/src/sentry/migrations/1060_eventattachment_date_expires.py +++ b/src/sentry/migrations/1060_eventattachment_date_expires.py @@ -2,16 +2,9 @@ import datetime -import django.db.models.expressions -import django.db.models.functions.datetime from django.db import migrations, models from sentry.new_migrations.migrations import CheckedMigration -from sentry.objectstore import default_attachment_retention - -# Initial default applies to existing attachments. New rows will be written with -# a runtime-defined default, so we follow-up with a static 30d default. -_RETENTION_DAYS = default_attachment_retention() class Migration(CheckedMigration): @@ -38,22 +31,7 @@ class Migration(CheckedMigration): model_name="eventattachment", name="date_expires", field=models.DateTimeField( - db_default=django.db.models.expressions.CombinedExpression( - django.db.models.functions.datetime.Now(), - "+", - models.Value(datetime.timedelta(days=_RETENTION_DAYS)), - ), - ), - ), - migrations.AlterField( - model_name="eventattachment", - name="date_expires", - field=models.DateTimeField( - db_default=django.db.models.expressions.CombinedExpression( - django.db.models.functions.datetime.Now(), - "+", - models.Value(datetime.timedelta(days=30)), - ), + db_default=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), ), ), ] diff --git a/src/sentry/migrations/1061_eventattachment_date_expires_index.py b/src/sentry/migrations/1061_eventattachment_date_expires_index.py index 62967a337149..72d7de02889f 100644 --- a/src/sentry/migrations/1061_eventattachment_date_expires_index.py +++ b/src/sentry/migrations/1061_eventattachment_date_expires_index.py @@ -1,9 +1,6 @@ # Generated by Django 5.2.12 on 2026-04-01 12:13 import datetime - -import django.db.models.expressions -import django.db.models.functions.datetime from django.db import migrations, models from sentry.new_migrations.migrations import CheckedMigration @@ -33,11 +30,7 @@ class Migration(CheckedMigration): model_name="eventattachment", name="date_expires", field=models.DateTimeField( - db_default=django.db.models.expressions.CombinedExpression( - django.db.models.functions.datetime.Now(), - "+", - models.Value(datetime.timedelta(days=30)), - ), + db_default=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), db_index=True, ), ), diff --git a/src/sentry/migrations/1062_backfill_eventattachment_date_expires.py b/src/sentry/migrations/1062_backfill_eventattachment_date_expires.py index 379260677e4a..543a4aee51e1 100644 --- a/src/sentry/migrations/1062_backfill_eventattachment_date_expires.py +++ b/src/sentry/migrations/1062_backfill_eventattachment_date_expires.py @@ -1,9 +1,61 @@ +from __future__ import annotations + +from datetime import datetime, timedelta +from datetime import timezone as dt_timezone + +import itertools + +import click from django.db import migrations +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.migrations.state import StateApps +from django.db.models import ExpressionWrapper, F +from django.db.models.fields import DateTimeField from sentry.new_migrations.migrations import CheckedMigration +BATCH_SIZE = 10_000 +SENTINEL = datetime(1970, 1, 1, 0, 0, 0, tzinfo=dt_timezone.utc) +DEFAULT_RETENTION_DAYS = 30 + + +def backfill_eventattachment_date_expires( + apps: StateApps, schema_editor: BaseDatabaseSchemaEditor +) -> None: + EventAttachment = apps.get_model("sentry", "EventAttachment") + + total_updated = 0 + for i in itertools.count(): + batch = EventAttachment.objects.filter(date_expires=SENTINEL).values("id")[:BATCH_SIZE] + updated = EventAttachment.objects.filter(id__in=batch).update( + date_expires=ExpressionWrapper( + F("date_added") + timedelta(days=DEFAULT_RETENTION_DAYS), + output_field=DateTimeField(), + ) + ) + if not updated: + break + + total_updated += updated + if i % 100 == 0: + click.echo(f"Backfilled {total_updated} rows so far...") + + click.echo(f"Done. Backfilled {total_updated} rows total.") + class Migration(CheckedMigration): + # This flag is used to mark that a migration shouldn't be automatically run in production. + # 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 post deployment: + # - Large data migrations. Typically we want these to be run manually 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 + # run this outside deployments so that we don't block them. Note that while adding an index + # is a schema change, it's completely safe to run the operation after the code has deployed. + # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment + is_post_deployment = True dependencies = [ @@ -12,7 +64,7 @@ class Migration(CheckedMigration): operations = [ migrations.RunPython( - migrations.RunPython.noop, + backfill_eventattachment_date_expires, migrations.RunPython.noop, hints={"tables": ["sentry_eventattachment"]}, ), diff --git a/src/sentry/migrations/1064_eventattachment_date_expires_now.py b/src/sentry/migrations/1064_eventattachment_date_expires_now.py deleted file mode 100644 index 6355479b1014..000000000000 --- a/src/sentry/migrations/1064_eventattachment_date_expires_now.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 5.2.12 on 2026-04-07 11:17 - -import datetime -import django.db.models.expressions -import django.db.models.functions.datetime -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. - # 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 post deployment: - # - Large data migrations. Typically we want these to be run manually 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 - # run this outside deployments so that we don't block them. Note that while adding an index - # is a schema change, it's completely safe to run the operation after the code has deployed. - # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment - - is_post_deployment = False - - dependencies = [ - ("sentry", "1063_remove_customdynamicsamplingrule"), - ] - - operations = [ - migrations.AlterField( - model_name="eventattachment", - name="date_expires", - field=models.DateTimeField( - db_default=django.db.models.expressions.CombinedExpression( - django.db.models.functions.datetime.Now(), - "+", - models.Value(datetime.timedelta(days=30)), - ), - db_index=True, - ), - ), - ] diff --git a/src/sentry/models/eventattachment.py b/src/sentry/models/eventattachment.py index 350c934b5a47..d556750ede0b 100644 --- a/src/sentry/models/eventattachment.py +++ b/src/sentry/models/eventattachment.py @@ -3,7 +3,8 @@ import mimetypes import os from dataclasses import dataclass -from datetime import timedelta +from datetime import datetime, timedelta +from datetime import timezone as dt_timezone from hashlib import sha1 from io import BytesIO from typing import IO, Any @@ -12,7 +13,6 @@ from django.core.cache import cache from django.db import models from django.db.models.expressions import DatabaseDefault -from django.db.models.functions import Now from django.utils import timezone from objectstore_client import TimeToLive @@ -26,6 +26,9 @@ from sentry.objectstore.metrics import measure_storage_operation from sentry.options.rollout import in_random_rollout +# Sentinel value stored in `date_expires` to mean "no explicit expiry — use default retention". +DATE_EXPIRES_SENTINEL = datetime(1970, 1, 1, 0, 0, 0, tzinfo=dt_timezone.utc) + # Attachment file types that are considered a crash report (PII relevant) CRASH_REPORT_TYPES = ("event.minidump", "event.applecrashreport") @@ -103,7 +106,7 @@ class EventAttachment(Model): date_added = models.DateTimeField(default=timezone.now, db_index=True) date_expires = models.DateTimeField( - db_default=Now() + timedelta(days=30), + db_default=DATE_EXPIRES_SENTINEL, db_index=True, ) diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index 8725c6ad5c15..bdacbbe0554a 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -1373,25 +1373,6 @@ default=0.0, flags=FLAG_MODIFIABLE_RATE | FLAG_AUTOMATOR_MODIFIABLE, ) -register( - "seer.night_shift.enable", - type=Bool, - default=False, - flags=FLAG_MODIFIABLE_BOOL | FLAG_AUTOMATOR_MODIFIABLE, -) -register( - "seer.night_shift.issues_per_org", - default=5, - flags=FLAG_AUTOMATOR_MODIFIABLE, -) - -# Supergroups / Lightweight RCA -register( - "supergroups.lightweight-enabled-orgs", - type=Sequence, - default=[], - flags=FLAG_ALLOW_EMPTY | FLAG_AUTOMATOR_MODIFIABLE, -) # ## sentry.killswitches # diff --git a/src/sentry/preprod/api/endpoints/preprod_artifact_snapshot.py b/src/sentry/preprod/api/endpoints/preprod_artifact_snapshot.py index c5703b8cc596..b52166275c28 100644 --- a/src/sentry/preprod/api/endpoints/preprod_artifact_snapshot.py +++ b/src/sentry/preprod/api/endpoints/preprod_artifact_snapshot.py @@ -394,11 +394,9 @@ def get(self, request: Request, organization: Organization, snapshot_id: str) -> source="github", ) ) - is_auto_approved = any((a.extras or {}).get("auto_approval") is True for a in approved) approval_info = SnapshotApprovalInfo( status="approved", approvers=approver_list, - is_auto_approved=is_auto_approved, ) elif all_approvals: # If records exist but none are APPROVED, they must be NEEDS_APPROVAL diff --git a/src/sentry/preprod/api/models/snapshots/project_preprod_snapshot_models.py b/src/sentry/preprod/api/models/snapshots/project_preprod_snapshot_models.py index 054b91e89347..a9c3c8efc98c 100644 --- a/src/sentry/preprod/api/models/snapshots/project_preprod_snapshot_models.py +++ b/src/sentry/preprod/api/models/snapshots/project_preprod_snapshot_models.py @@ -59,7 +59,6 @@ class SnapshotApprover(BaseModel): class SnapshotApprovalInfo(BaseModel): status: Literal["approved", "requires_approval"] approvers: list[SnapshotApprover] = [] - is_auto_approved: bool = False class SnapshotDetailsApiResponse(BaseModel): diff --git a/src/sentry/preprod/snapshots/tasks.py b/src/sentry/preprod/snapshots/tasks.py index d93b513e5381..5ce11f0f8b4a 100644 --- a/src/sentry/preprod/snapshots/tasks.py +++ b/src/sentry/preprod/snapshots/tasks.py @@ -7,13 +7,12 @@ import orjson from django.db import IntegrityError from django.utils import timezone -from objectstore_client import Session from objectstore_client.client import RequestError from pydantic import ValidationError from taskbroker_client.retry import Retry from sentry.objectstore import get_preprod_session -from sentry.preprod.models import PreprodArtifact, PreprodComparisonApproval +from sentry.preprod.models import PreprodArtifact from sentry.preprod.snapshots.image_diff.compare import compare_images_batch from sentry.preprod.snapshots.image_diff.odiff import OdiffServer from sentry.preprod.snapshots.manifest import ( @@ -114,128 +113,6 @@ def _create_pixel_batches( return batches -class ImageFingerprint(NamedTuple): - name: str - status: str - head_hash: str | None = None - previous_image_file_name: str | None = None - - -def _build_comparison_fingerprints(manifest: ComparisonManifest) -> set[ImageFingerprint]: - fingerprints: set[ImageFingerprint] = set() - for name, image in manifest.images.items(): - if image.status == "unchanged": - continue - if image.status in ("changed", "added"): - if not image.head_hash: - continue - fingerprints.add(ImageFingerprint(name, image.status, image.head_hash)) - elif image.status == "renamed": - if not image.head_hash or not image.previous_image_file_name: - continue - fingerprints.add( - ImageFingerprint(name, "renamed", image.head_hash, image.previous_image_file_name) - ) - else: - fingerprints.add(ImageFingerprint(name, image.status)) - return fingerprints - - -def _try_auto_approve_snapshot( - head_artifact: PreprodArtifact, - comparison_manifest: ComparisonManifest, - session: Session, -) -> None: - cc = head_artifact.commit_comparison - if not cc or not cc.pr_number or not cc.head_repo_name: - return - - head_fingerprints = _build_comparison_fingerprints(comparison_manifest) - if not head_fingerprints: - return - - approved_sibling = ( - PreprodArtifact.objects.filter( - project_id=head_artifact.project_id, - app_id=head_artifact.app_id, - build_configuration=head_artifact.build_configuration, - commit_comparison__pr_number=cc.pr_number, - commit_comparison__head_repo_name=cc.head_repo_name, - preprodcomparisonapproval__preprod_feature_type=PreprodComparisonApproval.FeatureType.SNAPSHOTS, - preprodcomparisonapproval__approval_status=PreprodComparisonApproval.ApprovalStatus.APPROVED, - preprodsnapshotmetrics__snapshot_comparisons_head_metrics__state=PreprodSnapshotComparison.State.SUCCESS, - ) - .exclude(id=head_artifact.id) - .order_by("-date_added") - .first() - ) - - if not approved_sibling: - return - - sibling_comparison = ( - PreprodSnapshotComparison.objects.filter( - head_snapshot_metrics__preprod_artifact=approved_sibling, - state=PreprodSnapshotComparison.State.SUCCESS, - ) - .order_by("-date_updated") - .first() - ) - - if not sibling_comparison: - return - - sibling_comparison_key = (sibling_comparison.extras or {}).get("comparison_key") - if not sibling_comparison_key: - return - - try: - sibling_manifest = ComparisonManifest( - **orjson.loads(session.get(sibling_comparison_key).payload.read()) - ) - except Exception: - logger.exception( - "auto_approve: failed to load sibling comparison manifest", - extra={ - "head_artifact_id": head_artifact.id, - "sibling_artifact_id": approved_sibling.id, - "comparison_key": sibling_comparison_key, - }, - ) - return - - sibling_fingerprints = _build_comparison_fingerprints(sibling_manifest) - - if head_fingerprints != sibling_fingerprints: - logger.info( - "auto_approve: fingerprints do not match", - extra={ - "head_artifact_id": head_artifact.id, - "sibling_artifact_id": approved_sibling.id, - }, - ) - return - - PreprodComparisonApproval.objects.create( - preprod_artifact=head_artifact, - preprod_feature_type=PreprodComparisonApproval.FeatureType.SNAPSHOTS, - approval_status=PreprodComparisonApproval.ApprovalStatus.APPROVED, - approved_at=timezone.now(), - extras={ - "auto_approval": True, - "prev_approved_artifact_id": approved_sibling.id, - }, - ) - - logger.info( - "auto_approve: snapshot auto-approved", - extra={ - "head_artifact_id": head_artifact.id, - "prev_approved_artifact_id": approved_sibling.id, - }, - ) - - @instrumented_task( name="sentry.preprod.tasks.compare_snapshots", namespace=preprod_tasks, @@ -704,14 +581,6 @@ def _fetch_hash(h: str) -> None: ): metrics.incr("preprod.snapshots.diff.zero_changes", sample_rate=1.0, tags=metric_tags) - try: - _try_auto_approve_snapshot(head_artifact, comparison_manifest, session) - except Exception: - logger.exception( - "Auto-approve failed after successful comparison", - extra={"head_artifact_id": head_artifact_id}, - ) - create_preprod_snapshot_status_check_task.apply_async( kwargs={ "preprod_artifact_id": head_artifact_id, diff --git a/src/sentry/preprod/tasks.py b/src/sentry/preprod/tasks.py index 64b7ac77d80c..b6bbc37641a7 100644 --- a/src/sentry/preprod/tasks.py +++ b/src/sentry/preprod/tasks.py @@ -562,7 +562,6 @@ def _assemble_preprod_artifact_size_analysis( "was_created": was_created, "project_id": project.id, "organization_id": org_id, - "organization_slug": organization.slug, }, ) diff --git a/src/sentry/preprod/vcs/pr_comments/tasks.py b/src/sentry/preprod/vcs/pr_comments/tasks.py index 4bd9b715e6d1..951158ea1767 100644 --- a/src/sentry/preprod/vcs/pr_comments/tasks.py +++ b/src/sentry/preprod/vcs/pr_comments/tasks.py @@ -130,7 +130,7 @@ def create_preprod_pr_comment_task( return existing_comment_id = _find_existing_comment_id(all_for_pr) - comment_body = format_pr_comment(installable_siblings, project=artifact.project) + comment_body = format_pr_comment(installable_siblings) try: if existing_comment_id: diff --git a/src/sentry/preprod/vcs/pr_comments/templates.py b/src/sentry/preprod/vcs/pr_comments/templates.py index c57206eca37c..dd6508e99340 100644 --- a/src/sentry/preprod/vcs/pr_comments/templates.py +++ b/src/sentry/preprod/vcs/pr_comments/templates.py @@ -1,11 +1,10 @@ from __future__ import annotations -from sentry.models.project import Project from sentry.preprod.models import PreprodArtifact from sentry.preprod.url_utils import get_preprod_artifact_url -def format_pr_comment(artifacts: list[PreprodArtifact], project: Project) -> str: +def format_pr_comment(artifacts: list[PreprodArtifact]) -> str: if not artifacts: raise ValueError("No installable artifacts to format") @@ -47,9 +46,4 @@ def format_pr_comment(artifacts: list[PreprodArtifact], project: Project) -> str else: sections.append(f"{header}\n{separator}\n" + "\n".join(android_rows)) - settings_url = project.organization.absolute_url( - f"/settings/projects/{project.slug}/mobile-builds/", query="tab=distribution" - ) - sections.append(f"[Configure {project.name} build distribution settings]({settings_url})") - return "\n\n".join(sections) diff --git a/src/sentry/processing_errors/detection.py b/src/sentry/processing_errors/detection.py index a5ae79e72afa..4a02464b0575 100644 --- a/src/sentry/processing_errors/detection.py +++ b/src/sentry/processing_errors/detection.py @@ -161,20 +161,6 @@ def _detect_for_config( if result.is_triggered: _set_detector_triggered(config, project_id) metrics.incr(f"processing_errors.{config.slug}.triggered") - error_types = sorted( - config.handler_cls.error_types.intersection( - filter(None, (e.get("type") for e in errors)) - ) - ) - logger.info( - "processing_errors.%s.occurrence_created", - config.slug, - extra={ - "organization_slug": event.project.organization.slug, - "project_id": project_id, - "error_types": error_types, - }, - ) def detect_processing_issues(job: PostProcessJob) -> None: diff --git a/src/sentry/receivers/__init__.py b/src/sentry/receivers/__init__.py index e9af536ddc46..83b66df9be09 100644 --- a/src/sentry/receivers/__init__.py +++ b/src/sentry/receivers/__init__.py @@ -11,6 +11,7 @@ from .owners import * # noqa: F401,F403 from .releases import * # noqa: F401,F403 from .rule_snooze import * # noqa: F401,F403 +from .rules import * # noqa: F401,F403 from .sentry_apps import * # noqa: F401,F403 from .stats import * # noqa: F401,F403 from .superuser import * # noqa: F401,F403 diff --git a/src/sentry/workflow_engine/receivers/project_workflows.py b/src/sentry/receivers/rules.py similarity index 66% rename from src/sentry/workflow_engine/receivers/project_workflows.py rename to src/sentry/receivers/rules.py index 021f294c88c3..b9699cb29c94 100644 --- a/src/sentry/workflow_engine/receivers/project_workflows.py +++ b/src/sentry/receivers/rules.py @@ -1,5 +1,4 @@ import logging -from typing import Any from django.db import router, transaction @@ -8,8 +7,7 @@ from sentry.notifications.types import FallthroughChoiceType from sentry.signals import alert_rule_created, project_created from sentry.users.services.user.model import RpcUser -from sentry.workflow_engine.defaults.workflows import ensure_default_workflows -from sentry.workflow_engine.models import AlertRuleWorkflow +from sentry.workflow_engine.migration_helpers.issue_alert_migration import IssueAlertMigrator logger = logging.getLogger("sentry") @@ -36,38 +34,21 @@ PLATFORMS_WITH_PRIORITY_ALERTS = ["python", "javascript"] -def create_default_workflows( - project: Project, - default_rules: bool = True, - RuleModel: type[Rule] = Rule, - **kwargs: Any, -) -> None: - rule_data = DEFAULT_RULE_DATA - +def create_default_rules(project: Project, default_rules=True, RuleModel=Rule, **kwargs): if not default_rules: return + rule_data = DEFAULT_RULE_DATA + with transaction.atomic(router.db_for_write(RuleModel)): - workflows = ensure_default_workflows(project) + rule = RuleModel.objects.create(project=project, label=DEFAULT_RULE_LABEL, data=rule_data) - # TODO - we can remove the legacy code below once - # we launch the new UI (and stop referencing legacy models) - rule = RuleModel.objects.create( - project=project, - label=DEFAULT_RULE_LABEL, - data=rule_data, + workflow = IssueAlertMigrator(rule).run() + logger.info( + "workflow_engine.default_issue_alert.migrated", + extra={"rule_id": rule.id, "workflow_id": workflow.id}, ) - legacy_references = [ - AlertRuleWorkflow( - rule_id=rule.id, - workflow=workflow, - ) - for workflow in workflows - ] - - AlertRuleWorkflow.objects.bulk_create(legacy_references) - try: user: RpcUser = project.organization.get_default_owner() except IndexError: @@ -90,8 +71,4 @@ def create_default_workflows( ) -project_created.connect( - create_default_workflows, - dispatch_uid="create_default_workflows", - weak=False, -) +project_created.connect(create_default_rules, dispatch_uid="create_default_rules", weak=False) diff --git a/src/sentry/runner/commands/cleanup.py b/src/sentry/runner/commands/cleanup.py index 061e7f630f88..103630fbcecf 100644 --- a/src/sentry/runner/commands/cleanup.py +++ b/src/sentry/runner/commands/cleanup.py @@ -739,10 +739,8 @@ def remove_old_nodestore_values(days: int) -> None: def remove_cross_project_bulk_query_models( bulk_query_deletes: list[tuple[type[BaseModel], str, str | None]], ) -> list[tuple[type[BaseModel], str, str | None]]: - from sentry.models.groupopenperiodactivity import GroupOpenPeriodActivity from sentry.workflow_engine.models.workflow_fire_history import WorkflowFireHistory - bulk_query_deletes.remove((GroupOpenPeriodActivity, "date_added", None)) bulk_query_deletes.remove((WorkflowFireHistory, "date_added", None)) return bulk_query_deletes @@ -751,7 +749,6 @@ def generate_bulk_query_deletes() -> list[tuple[type[BaseModel], str, str | None from django.apps import apps from sentry.models.groupemailthread import GroupEmailThread - from sentry.models.groupopenperiodactivity import GroupOpenPeriodActivity from sentry.models.userreport import UserReport from sentry.workflow_engine.models.workflow_fire_history import WorkflowFireHistory @@ -766,7 +763,6 @@ def generate_bulk_query_deletes() -> list[tuple[type[BaseModel], str, str | None BULK_QUERY_DELETES = [ (UserReport, "date_added", None), (GroupEmailThread, "date", None), - (GroupOpenPeriodActivity, "date_added", None), (WorkflowFireHistory, "date_added", None), ] + additional_bulk_query_deletes diff --git a/src/sentry/scm/errors.py b/src/sentry/scm/errors.py index 05fd94a15e2e..491de34507e2 100644 --- a/src/sentry/scm/errors.py +++ b/src/sentry/scm/errors.py @@ -42,12 +42,6 @@ class SCMProviderException(SCMError): pass -class SCMProviderEventNotSupported(SCMError): - def __init__(self, message: str) -> None: - self.message = message - super().__init__(message) - - class SCMProviderNotSupported(SCMError): def __init__(self, message: str) -> None: self.message = message diff --git a/src/sentry/scm/private/ipc.py b/src/sentry/scm/private/ipc.py index 844103258eb2..ef795ee1851f 100644 --- a/src/sentry/scm/private/ipc.py +++ b/src/sentry/scm/private/ipc.py @@ -16,7 +16,7 @@ import msgspec import sentry_sdk -from sentry.scm.errors import SCMProviderEventNotSupported, SCMProviderNotSupported +from sentry.scm.errors import SCMProviderNotSupported from sentry.scm.private.event_stream import SourceCodeManagerEventStream, scm_event_stream from sentry.scm.private.webhooks.github import deserialize_github_event from sentry.scm.types import ( @@ -356,8 +356,10 @@ def produce_to_listeners( """ parsed_event = deserialize_raw_event(event) + # Most events are not supported. We drop them. They could be processed elsewhere but they're + # not processed by the unified SCM platform. if parsed_event is None: - raise SCMProviderEventNotSupported(f"Unsupported event type `{event['event_type_hint']}`.") + return None message = serialize_event(parsed_event) diff --git a/src/sentry/scm/private/stream_producer.py b/src/sentry/scm/private/stream_producer.py index 7dd4fbbb8c89..0ec687c7b030 100644 --- a/src/sentry/scm/private/stream_producer.py +++ b/src/sentry/scm/private/stream_producer.py @@ -1,7 +1,6 @@ -import logging from collections.abc import Callable -from sentry.scm.errors import SCMProviderEventNotSupported, SCMProviderNotSupported +from sentry.scm.errors import SCMProviderNotSupported from sentry.scm.private.ipc import ( PRODUCE_TO_LISTENER, produce_to_listener, @@ -12,7 +11,6 @@ from sentry.scm.types import HybridCloudSilo, SubscriptionEvent from sentry.scm.utils import check_rollout_option -logger = logging.getLogger("sentry.scm") PREFIX = "sentry.scm.produce_event_to_scm_stream" @@ -20,7 +18,6 @@ def produce_event_to_scm_stream( event: SubscriptionEvent, silo: HybridCloudSilo, *, - is_dev: bool = False, produce_to_listener: PRODUCE_TO_LISTENER = produce_to_listener, record_count: Callable[[str, int, dict[str, str]], None] = record_count_metric, report_error: Callable[[Exception], None] = report_error_to_sentry, @@ -40,25 +37,11 @@ def produce_event_to_scm_stream( try: produce_to_listeners(event, silo, produce_to_listener=produce_to_listener) record_count(f"{PREFIX}.success", 1, {}) - if is_dev: - logger.info(f"Successfully processed SCM webhook event: {event['event_type_hint']}.") except SCMProviderNotSupported: - record_count( - f"{PREFIX}.failed", 1, {"reason": "provider-not-supported", "provider": event["type"]} - ) - if is_dev: - logger.exception("Failed to process SCM webhook event.") - except SCMProviderEventNotSupported: - record_count( - f"{PREFIX}.failed", 1, {"reason": "event-not-supported", "provider": event["type"]} - ) - if is_dev: - logger.exception("Failed to process SCM webhook event.") + record_count(f"{PREFIX}.failed", 1, {"reason": "not-supported", "provider": event["type"]}) except Exception as e: record_count(f"{PREFIX}.failed", 1, {"reason": "processing"}) report_error(e) - if is_dev: - logger.exception("Failed to process SCM webhook event.") __all__ = [ diff --git a/src/sentry/search/eap/columns.py b/src/sentry/search/eap/columns.py index 80485a3e13d2..c53a264dd99c 100644 --- a/src/sentry/search/eap/columns.py +++ b/src/sentry/search/eap/columns.py @@ -43,7 +43,6 @@ class ResolverSettings(TypedDict): snuba_params: SnubaParams query_result_cache: dict[str, EAPResponse] search_config: SearchResolverConfig - conditional: bool @dataclass(frozen=True, kw_only=True) @@ -252,31 +251,45 @@ class ResolvedTraceMetricAggregate(ResolvedFunction): is_aggregate: bool = field(default=True, init=False) trace_metric: TraceMetric | None - trace_filter: TraceItemFilter | None @property def proto_definition(self) -> AttributeAggregation | AttributeConditionalAggregation: - if self.trace_metric is None and self.trace_filter is None: + if self.trace_metric is None: return AttributeAggregation( aggregate=self.internal_name, key=self.key, label=self.public_alias, extrapolation_mode=self.extrapolation_mode, ) + return AttributeConditionalAggregation( + aggregate=self.internal_name, + key=self.key, + filter=self.trace_metric.get_filter(), + label=self.public_alias, + extrapolation_mode=self.extrapolation_mode, + ) - if self.trace_filter is None and self.trace_metric is not None: - trace_filter = self.trace_metric.get_filter() - elif self.trace_filter is not None and self.trace_metric is None: - trace_filter = self.trace_filter - elif self.trace_filter is not None and self.trace_metric is not None: - trace_filter = TraceItemFilter( - and_filter=AndFilter(filters=[self.trace_metric.get_filter(), self.trace_filter]) - ) +@dataclass(frozen=True, kw_only=True) +class ResolvedConditionalTraceMetricAggregate(ResolvedTraceMetricAggregate): + trace_filter: TraceItemFilter + + @property + def proto_definition(self) -> AttributeAggregation | AttributeConditionalAggregation: + if self.trace_metric is None: + return AttributeConditionalAggregation( + aggregate=self.internal_name, + key=self.key, + filter=self.trace_filter, + label=self.public_alias, + extrapolation_mode=self.extrapolation_mode, + ) return AttributeConditionalAggregation( aggregate=self.internal_name, key=self.key, - filter=trace_filter, + filter=TraceItemFilter( + and_filter=AndFilter(filters=[self.trace_metric.get_filter(), self.trace_filter]) + ), label=self.public_alias, extrapolation_mode=self.extrapolation_mode, ) @@ -421,6 +434,27 @@ def resolve( snuba_params: SnubaParams, query_result_cache: dict[str, EAPResponse], search_config: SearchResolverConfig, + ) -> ResolvedFunction: + return self._resolve( + alias, + search_type, + resolved_arguments, + snuba_params, + query_result_cache, + search_config, + ResolvedTraceMetricAggregate, + ) + + def _resolve( + self, + alias: str, + search_type: constants.SearchType, + resolved_arguments: ResolvedArguments, + snuba_params: SnubaParams, + query_result_cache: dict[str, EAPResponse], + search_config: SearchResolverConfig, + aggregate: type[ResolvedConditionalTraceMetricAggregate] + | type[ResolvedTraceMetricAggregate], ) -> ResolvedFunction: if not isinstance(resolved_arguments[self.offset], AttributeKey): raise InvalidSearchQuery( @@ -449,21 +483,22 @@ def resolve( ): resolved_search_type = cast(constants.SearchType, trace_metric.metric_unit) - return ResolvedTraceMetricAggregate( - public_alias=alias, - internal_name=self.internal_function, - search_type=resolved_search_type, - internal_type=self.internal_type, - processor=self.processor, - extrapolation_mode=resolve_extrapolation_mode( + kwargs = { + "public_alias": alias, + "internal_name": self.internal_function, + "search_type": resolved_search_type, + "internal_type": self.internal_type, + "processor": self.processor, + "extrapolation_mode": resolve_extrapolation_mode( search_config, self.extrapolation_mode_override ), - key=cast(AttributeKey, resolved_attribute), - trace_metric=trace_metric, - trace_filter=cast(TraceItemFilter, resolved_arguments[0]) - if isinstance(self, ConditionalTraceMetricAggregateDefinition) - else None, - ) + "key": resolved_attribute, + "trace_metric": trace_metric, + } + if aggregate == ResolvedConditionalTraceMetricAggregate: + kwargs["trace_filter"] = resolved_arguments[0] + + return aggregate(**kwargs) # type: ignore[arg-type] @dataclass(kw_only=True) @@ -471,6 +506,25 @@ class ConditionalTraceMetricAggregateDefinition(TraceMetricAggregateDefinition): offset = 1 expected_args = 5 + def resolve( + self, + alias: str, + search_type: constants.SearchType, + resolved_arguments: ResolvedArguments, + snuba_params: SnubaParams, + query_result_cache: dict[str, EAPResponse], + search_config: SearchResolverConfig, + ) -> ResolvedFunction: + return self._resolve( + alias, + search_type, + resolved_arguments, + snuba_params, + query_result_cache, + search_config, + ResolvedConditionalTraceMetricAggregate, + ) + @dataclass(kw_only=True) class ConditionalAggregateDefinition(AggregateDefinition): @@ -536,7 +590,6 @@ def resolve( snuba_params=snuba_params, query_result_cache=query_result_cache, search_config=search_config, - conditional=False, ) return ResolvedFormula( @@ -551,13 +604,8 @@ def resolve( @dataclass(kw_only=True) class TraceMetricFormulaDefinition(FormulaDefinition): - offset = 0 - expected_args = 4 - def __post_init__(self) -> None: - validate_trace_metric_aggregate_arguments( - self.arguments, offset=self.offset, expected_args=self.expected_args - ) + validate_trace_metric_aggregate_arguments(self.arguments) def resolve( self, @@ -575,20 +623,14 @@ def resolve( snuba_params=snuba_params, query_result_cache=query_result_cache, search_config=search_config, - conditional=isinstance(self, ConditionalTraceMetricFormulaDefinition), ) - trace_metric = extract_trace_metric_aggregate_arguments( - resolved_arguments, offset=self.offset - ) + trace_metric = extract_trace_metric_aggregate_arguments(resolved_arguments) return ResolvedTraceMetricFormula( public_alias=alias, search_type=search_type, - formula=self.formula_resolver( - resolved_arguments, - resolver_settings, - ), + formula=self.formula_resolver(resolved_arguments, resolver_settings), is_aggregate=self.is_aggregate, internal_type=self.internal_type, processor=self.processor, @@ -596,12 +638,6 @@ def resolve( ) -@dataclass(kw_only=True) -class ConditionalTraceMetricFormulaDefinition(TraceMetricFormulaDefinition): - offset = 1 - expected_args = 5 - - def simple_sentry_field( field: str, search_type: constants.SearchType = "string", diff --git a/src/sentry/search/eap/trace_metrics/aggregates.py b/src/sentry/search/eap/trace_metrics/aggregates.py index 4d1aa1de6207..7df48923658a 100644 --- a/src/sentry/search/eap/trace_metrics/aggregates.py +++ b/src/sentry/search/eap/trace_metrics/aggregates.py @@ -376,7 +376,7 @@ def if_query_validator(term: str) -> bool: def if_combinator(definition: AggregateDefinition) -> AggregateDefinition: # Copy the TraceMetricAggregateDefinition but make it Conditional - return ConditionalTraceMetricAggregateDefinition( + if_definition = ConditionalTraceMetricAggregateDefinition( attribute_resolver=definition.attribute_resolver, default_search_type=definition.default_search_type, extrapolation_mode_override=definition.extrapolation_mode_override, @@ -390,16 +390,16 @@ def if_combinator(definition: AggregateDefinition) -> AggregateDefinition: *definition.arguments, ], ) + return if_definition -TRACE_METRICS_AGGREGATE_COMBINATORS: dict[ - str, Callable[[AggregateDefinition], AggregateDefinition] -] = { +TRACE_METRICS_COMBINATORS: dict[str, Callable[[AggregateDefinition], AggregateDefinition]] = { "if": if_combinator, } + combinator_aggregate_definitions: dict[str, AggregateDefinition] = {} -for combinator, apply_combinator in TRACE_METRICS_AGGREGATE_COMBINATORS.items(): +for combinator, apply_combinator in TRACE_METRICS_COMBINATORS.items(): for function, definition in TRACE_METRICS_AGGREGATE_DEFINITIONS.items(): combinator_aggregate_definitions[f"{function}_{combinator}"] = apply_combinator(definition) diff --git a/src/sentry/search/eap/trace_metrics/formulas.py b/src/sentry/search/eap/trace_metrics/formulas.py index fe844f497d73..8de72543fb10 100644 --- a/src/sentry/search/eap/trace_metrics/formulas.py +++ b/src/sentry/search/eap/trace_metrics/formulas.py @@ -1,5 +1,3 @@ -from typing import Callable, cast - from sentry_protos.snuba.v1.attribute_conditional_aggregation_pb2 import ( AttributeConditionalAggregation, ) @@ -10,12 +8,10 @@ AttributeKey, Function, ) -from sentry_protos.snuba.v1.trace_item_filter_pb2 import AndFilter, TraceItemFilter from sentry.exceptions import InvalidSearchQuery from sentry.search.eap.columns import ( AttributeArgumentDefinition, - ConditionalTraceMetricFormulaDefinition, FormulaDefinition, ResolvedArguments, ResolverSettings, @@ -46,49 +42,31 @@ def _rate_internal( else settings["snuba_params"].interval ) - trace_metric = search_config.metric or extract_trace_metric_aggregate_arguments( - args, offset=0 if not settings["conditional"] else 1 - ) - - trace_filter: TraceItemFilter | None - if trace_metric is not None: - trace_filter = ( - TraceItemFilter( - and_filter=AndFilter( - filters=[cast(TraceItemFilter, args[0]), trace_metric.get_filter()] - ) - ) - if settings["conditional"] - else trace_metric.get_filter() - ) - if trace_metric.metric_type == "counter": - aggregate = Function.FUNCTION_SUM - key = AttributeKey(type=AttributeKey.TYPE_DOUBLE, name="sentry.value") - else: - aggregate = Function.FUNCTION_COUNT - key = AttributeKey(name="sentry.project_id", type=AttributeKey.Type.TYPE_INT) - else: - if settings["conditional"]: - trace_filter = cast(TraceItemFilter, args[0]) - else: - trace_filter = None - aggregate = Function.FUNCTION_COUNT - key = AttributeKey(name="sentry.project_id", type=AttributeKey.Type.TYPE_INT) + trace_metric = search_config.metric or extract_trace_metric_aggregate_arguments(args) - if trace_filter is None: + if trace_metric is None: left = Column( aggregation=AttributeAggregation( aggregate=Function.FUNCTION_COUNT, - key=key, + key=AttributeKey(name="sentry.project_id", type=AttributeKey.Type.TYPE_INT), + extrapolation_mode=extrapolation_mode, + ) + ) + elif trace_metric.metric_type == "counter": + left = Column( + conditional_aggregation=AttributeConditionalAggregation( + aggregate=Function.FUNCTION_SUM, + key=AttributeKey(type=AttributeKey.TYPE_DOUBLE, name="sentry.value"), + filter=trace_metric.get_filter(), extrapolation_mode=extrapolation_mode, ) ) else: left = Column( conditional_aggregation=AttributeConditionalAggregation( - aggregate=aggregate, - key=key, - filter=trace_filter, + aggregate=Function.FUNCTION_COUNT, + key=AttributeKey(name="sentry.project_id", type=AttributeKey.Type.TYPE_INT), + filter=trace_metric.get_filter(), extrapolation_mode=extrapolation_mode, ) ) @@ -176,39 +154,3 @@ def per_minute(args: ResolvedArguments, settings: ResolverSettings) -> Column.Bi infer_search_type_from_arguments=False, ), } - - -def if_query_validator(term: str) -> bool: - if not term.startswith("`") or not term.endswith("`"): - return False - return True - - -def if_combinator(formula: FormulaDefinition) -> FormulaDefinition: - return ConditionalTraceMetricFormulaDefinition( - formula_resolver=formula.formula_resolver, - is_aggregate=formula.is_aggregate, - default_search_type=formula.default_search_type, - infer_search_type_from_arguments=formula.infer_search_type_from_arguments, - internal_type=formula.internal_type, - extrapolation_mode_override=formula.extrapolation_mode_override, - processor=formula.processor, - private=formula.private, - arguments=[ - ValueArgumentDefinition(argument_types={"query"}, validator=if_query_validator), - *formula.arguments, - ], - ) - - -TRACE_METRICS_FORMULA_COMBINATORS: dict[str, Callable[[FormulaDefinition], FormulaDefinition]] = { - "if": if_combinator, -} - -combinator_formula_definitions: dict[str, FormulaDefinition] = {} -for combinator, apply_combinator in TRACE_METRICS_FORMULA_COMBINATORS.items(): - for formula, definition in TRACE_METRICS_FORMULA_DEFINITIONS.items(): - if definition.is_aggregate: - combinator_formula_definitions[f"{formula}_{combinator}"] = apply_combinator(definition) - -TRACE_METRICS_FORMULA_DEFINITIONS.update(combinator_formula_definitions) diff --git a/src/sentry/seer/autofix/autofix_agent.py b/src/sentry/seer/autofix/autofix_agent.py index 6cda53499d28..2e0898cc375f 100644 --- a/src/sentry/seer/autofix/autofix_agent.py +++ b/src/sentry/seer/autofix/autofix_agent.py @@ -8,22 +8,6 @@ from pydantic import BaseModel from rest_framework.exceptions import PermissionDenied -from sentry import analytics -from sentry.analytics.events.autofix_events import ( - AiAutofixAgentHandoffEvent, - AiAutofixCodeChangesCompletedEvent, - AiAutofixCodeChangesStartedEvent, - AiAutofixImpactAssessmentCompletedEvent, - AiAutofixImpactAssessmentStartedEvent, - AiAutofixPhaseEvent, - AiAutofixPrCreatedStartedEvent, - AiAutofixRootCauseCompletedEvent, - AiAutofixRootCauseStartedEvent, - AiAutofixSolutionCompletedEvent, - AiAutofixSolutionStartedEvent, - AiAutofixTriageCompletedEvent, - AiAutofixTriageStartedEvent, -) from sentry.constants import ENABLE_SEER_CODING_DEFAULT from sentry.seer.autofix.artifact_schemas import ( ImpactAssessmentArtifact, @@ -98,14 +82,10 @@ def __init__( artifact_schema: type[BaseModel] | None, prompt_fn: Callable[..., str], enable_coding: bool = False, - started_event: type[AiAutofixPhaseEvent] | None = None, - completed_event: type[AiAutofixPhaseEvent] | None = None, ): self.artifact_schema = artifact_schema self.prompt_fn = prompt_fn self.enable_coding = enable_coding - self.started_event = started_event - self.completed_event = completed_event # Step configurations mapping step to its artifact schema and prompt @@ -113,33 +93,23 @@ def __init__( AutofixStep.ROOT_CAUSE: StepConfig( artifact_schema=RootCauseArtifact, prompt_fn=root_cause_prompt, - started_event=AiAutofixRootCauseStartedEvent, - completed_event=AiAutofixRootCauseCompletedEvent, ), AutofixStep.SOLUTION: StepConfig( artifact_schema=SolutionArtifact, prompt_fn=solution_prompt, - started_event=AiAutofixSolutionStartedEvent, - completed_event=AiAutofixSolutionCompletedEvent, ), AutofixStep.CODE_CHANGES: StepConfig( artifact_schema=None, # Code changes read from file_patches prompt_fn=code_changes_prompt, enable_coding=True, - started_event=AiAutofixCodeChangesStartedEvent, - completed_event=AiAutofixCodeChangesCompletedEvent, ), AutofixStep.IMPACT_ASSESSMENT: StepConfig( artifact_schema=ImpactAssessmentArtifact, prompt_fn=impact_assessment_prompt, - started_event=AiAutofixImpactAssessmentStartedEvent, - completed_event=AiAutofixImpactAssessmentCompletedEvent, ), AutofixStep.TRIAGE: StepConfig( artifact_schema=TriageArtifact, prompt_fn=triage_prompt, - started_event=AiAutofixTriageStartedEvent, - completed_event=AiAutofixTriageCompletedEvent, ), } @@ -230,7 +200,6 @@ def trigger_autofix_explorer( stopping_point: AutofixStoppingPoint | None = None, intelligence_level: Literal["low", "medium", "high"] = "low", user_context: str | None = None, - insert_index: int | None = None, ) -> int: """ Start or continue an Explorer-based autofix run. @@ -246,16 +215,6 @@ def trigger_autofix_explorer( """ config = STEP_CONFIGS[step] - - if config.started_event is not None: - analytics.record( - config.started_event( - organization_id=group.organization.id, - project_id=group.project_id, - group_id=group.id, - referrer=referrer.value, - ) - ) client = get_autofix_explorer_client( group, intelligence_level=intelligence_level, @@ -288,7 +247,6 @@ def trigger_autofix_explorer( prompt_metadata=prompt_metadata, artifact_key=artifact_key, artifact_schema=artifact_schema, - insert_index=insert_index, ) payload = { @@ -542,15 +500,6 @@ def trigger_coding_agent_handoff( auto_create_pr=auto_create_pr, ) - analytics.record( - AiAutofixAgentHandoffEvent( - organization_id=group.organization.id, - project_id=group.project_id, - group_id=group.id, - referrer=referrer.value, - ) - ) - metrics.incr( "autofix.explorer.trigger", tags={"step": "coding_agent_handoff", "referrer": referrer.value}, @@ -583,15 +532,6 @@ def trigger_push_changes( if group_id != group.id: raise SeerPermissionError("Unknown run id for group") - analytics.record( - AiAutofixPrCreatedStartedEvent( - organization_id=group.organization.id, - project_id=group.project_id, - group_id=group.id, - referrer=referrer.value, - ) - ) - client.push_changes( run_id, repo_name=repo_name, diff --git a/src/sentry/seer/autofix/constants.py b/src/sentry/seer/autofix/constants.py index bc37d7be38c4..28c08290afcf 100644 --- a/src/sentry/seer/autofix/constants.py +++ b/src/sentry/seer/autofix/constants.py @@ -54,7 +54,6 @@ class AutofixReferrer(enum.StrEnum): ISSUE_SUMMARY_POST_PROCESS_FIXABILITY = "issue_summary.post_process_fixability" SLACK = "slack" ON_COMPLETION_HOOK = "autofix.on_completion_hook" - NIGHT_SHIFT = "night_shift" UNKNOWN = "unknown" @@ -62,4 +61,3 @@ class SeerAutomationSource(enum.Enum): ISSUE_DETAILS = "issue_details" ALERT = "alert" POST_PROCESS = "post_process" - NIGHT_SHIFT = "night_shift" diff --git a/src/sentry/seer/autofix/issue_summary.py b/src/sentry/seer/autofix/issue_summary.py index 677e3ce7e273..08361e53317b 100644 --- a/src/sentry/seer/autofix/issue_summary.py +++ b/src/sentry/seer/autofix/issue_summary.py @@ -45,7 +45,7 @@ make_signed_seer_api_request, make_summarize_issue_request, ) -from sentry.seer.supergroups.explorer_lightweight_rca import trigger_explorer_lightweight_rca +from sentry.seer.supergroups.lightweight_rca import trigger_lightweight_rca from sentry.services import eventstore from sentry.services.eventstore.models import Event, GroupEvent from sentry.tasks.base import instrumented_task @@ -226,10 +226,10 @@ def _trigger_autofix_task( stopping_point=stopping_point, ) try: - trigger_explorer_lightweight_rca(group) + trigger_lightweight_rca(group) except Exception: logger.exception( - "explorer_lightweight_rca.trigger_error_in_trigger_autofix_task", + "lightweight_rca.trigger_error_in_trigger_autofix_task", extra={"group_id": group_id}, ) else: diff --git a/src/sentry/seer/autofix/on_completion_hook.py b/src/sentry/seer/autofix/on_completion_hook.py index b392eedd63f5..0f86910bb207 100644 --- a/src/sentry/seer/autofix/on_completion_hook.py +++ b/src/sentry/seer/autofix/on_completion_hook.py @@ -5,13 +5,11 @@ from django.utils import timezone -from sentry import analytics, features -from sentry.analytics.events.autofix_events import AiAutofixPrCreatedCompletedEvent +from sentry import features from sentry.models.group import Group from sentry.models.organization import Organization from sentry.models.project import Project from sentry.seer.autofix.autofix_agent import ( - STEP_CONFIGS, AutofixStep, trigger_autofix_explorer, trigger_coding_agent_handoff, @@ -138,8 +136,6 @@ def _send_step_webhook( # to find which step just completed webhook_action_type: SeerActionType | None = None - is_pr_created = False - if current_step is not None: artifact = cls.find_latest_artifact_for_step(state, current_step) if artifact is not None: @@ -170,15 +166,6 @@ def _send_step_webhook( } for pull_request in state.repo_pr_states.values() ] - is_pr_created = True - analytics.record( - AiAutofixPrCreatedCompletedEvent( - organization_id=organization.id, - project_id=group.project_id, - group_id=group.id, - referrer=None if current_referrer is None else current_referrer.value, - ) - ) else: webhook_action_type = SeerActionType.CODING_COMPLETED diffs_by_repo = state.get_diffs_by_repo() @@ -243,21 +230,11 @@ def _send_step_webhook( }, ) - if current_step is not None and not is_pr_created: + if current_step is not None: referrer = current_referrer.value if current_referrer is not None else None metrics.incr( "autofix.explorer.complete", tags={"step": current_step.value, "referrer": referrer} ) - completed_event_cls = STEP_CONFIGS[current_step].completed_event - if completed_event_cls is not None: - analytics.record( - completed_event_cls( - organization_id=organization.id, - project_id=group.project_id, - group_id=group.id, - referrer=referrer, - ) - ) @classmethod def _maybe_trigger_supergroups_embedding( diff --git a/src/sentry/seer/endpoints/group_ai_autofix.py b/src/sentry/seer/endpoints/group_ai_autofix.py index a9c3bc6fa20b..d2ccb4f613c8 100644 --- a/src/sentry/seer/endpoints/group_ai_autofix.py +++ b/src/sentry/seer/endpoints/group_ai_autofix.py @@ -129,10 +129,6 @@ class ExplorerAutofixRequestSerializer(CamelSnakeSerializer): required=False, help_text="Optional repository name for which to create the pull request. Do not pass a repository name to create pull requests in all relevant repositories.", ) - insert_index = serializers.IntegerField( - required=False, - help_text="Block index to insert at. When provided, truncates blocks after this point for retry-from-step.", - ) def validate(self, data: dict[str, Any]) -> dict[str, Any]: stopping_point = data.get("stopping_point", None) @@ -293,7 +289,6 @@ def _post_explorer(self, request: Request, group: Group) -> Response: run_id=run_id, intelligence_level=data["intelligence_level"], user_context=data.get("user_context"), - insert_index=data.get("insert_index"), ) return Response({"run_id": run_id}, status=status.HTTP_202_ACCEPTED) except SeerPermissionError as e: diff --git a/src/sentry/seer/explorer/client_utils.py b/src/sentry/seer/explorer/client_utils.py index fa0be21574d6..c05971dab8bc 100644 --- a/src/sentry/seer/explorer/client_utils.py +++ b/src/sentry/seer/explorer/client_utils.py @@ -345,7 +345,7 @@ def poll_until_done( def _render_node(node: dict[str, Any], depth: int) -> str: """Recursively render an LLMContextSnapshot node and its children as markdown.""" heading = "#" * min(depth + 1, 6) - lines = [f"{heading} {(node.get('nodeType') or 'unknown').capitalize()}"] + lines = [f"{heading} {node.get('nodeType', 'unknown')}"] data = node.get("data") if isinstance(data, dict): diff --git a/src/sentry/seer/signed_seer_api.py b/src/sentry/seer/signed_seer_api.py index 33752a24de8b..f69c3bb11a0d 100644 --- a/src/sentry/seer/signed_seer_api.py +++ b/src/sentry/seer/signed_seer_api.py @@ -376,14 +376,6 @@ class SupergroupsEmbeddingRequest(TypedDict): artifact_data: dict[str, Any] -class LightweightRCAClusterRequest(TypedDict): - group_id: int - issue: dict[str, Any] - organization_slug: str - organization_id: int - project_id: int - - class SupergroupsGetRequest(TypedDict): organization_id: int supergroup_id: int @@ -394,22 +386,6 @@ class SupergroupsGetByGroupIdsRequest(TypedDict): group_ids: list[int] -class SupergroupDetailData(TypedDict): - id: int - title: str - summary: str - error_type: str - code_area: str - group_ids: list[int] - project_ids: list[int] - created_at: str - updated_at: str - - -class SupergroupsByGroupIdsResponse(TypedDict): - data: list[SupergroupDetailData] - - class ServiceMapUpdateRequest(TypedDict): organization_id: int nodes: list[dict[str, Any]] @@ -509,20 +485,6 @@ def make_supergroups_embedding_request( ) -def make_lightweight_rca_cluster_request( - body: LightweightRCAClusterRequest, - timeout: int | float | None = None, - viewer_context: SeerViewerContext | None = None, -) -> BaseHTTPResponse: - return make_signed_seer_api_request( - seer_autofix_default_connection_pool, - "/v0/issues/supergroups/cluster-lightweight", - body=orjson.dumps(body, option=orjson.OPT_NON_STR_KEYS), - timeout=timeout, - viewer_context=viewer_context, - ) - - def make_supergroups_get_request( body: SupergroupsGetRequest, viewer_context: SeerViewerContext, diff --git a/src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py b/src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py index fc2c50de7a59..c3cadbddde86 100644 --- a/src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py +++ b/src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py @@ -16,7 +16,6 @@ from sentry.models.organization import Organization from sentry.seer.signed_seer_api import ( SeerViewerContext, - SupergroupsByGroupIdsResponse, make_supergroups_get_by_group_ids_request, ) @@ -56,19 +55,21 @@ def get(self, request: Request, organization: Organization) -> Response: status=status_codes.HTTP_400_BAD_REQUEST, ) - status_param = request.GET.get("status") - if status_param is not None and status_param not in STATUS_QUERY_CHOICES: - return Response( - {"detail": "Invalid status parameter"}, - status=status_codes.HTTP_400_BAD_REQUEST, - ) - - valid_group_ids = set( - Group.objects.filter( - id__in=group_ids, - project__organization=organization, - ).values_list("id", flat=True) + group_qs = Group.objects.filter( + id__in=group_ids, + project__organization=organization, ) + + status_param = request.GET.get("status") + if status_param is not None: + if status_param not in STATUS_QUERY_CHOICES: + return Response( + {"detail": "Invalid status parameter"}, + status=status_codes.HTTP_400_BAD_REQUEST, + ) + group_qs = group_qs.filter(status=STATUS_QUERY_CHOICES[status_param]) + + valid_group_ids = set(group_qs.values_list("id", flat=True)) group_ids = [gid for gid in group_ids if gid in valid_group_ids] if not group_ids: @@ -89,31 +90,4 @@ def get(self, request: Request, organization: Organization) -> Response: status=response.status, ) - data: SupergroupsByGroupIdsResponse = orjson.loads(response.data) - - if not status_param: - return Response(data) - - # Seer returns all group_ids per supergroup regardless of status. - # We can't filter before the Seer call because Seer expands group_ids - # to include the full supergroup membership, not just the requested IDs. - # Instead, collect every group_id from the response, check status in - # bulk, and strip out non-matching ones. - all_response_group_ids: list[int] = [] - for sg in data["data"]: - all_response_group_ids.extend(sg["group_ids"]) - - matching_ids = set( - Group.objects.filter( - id__in=all_response_group_ids, - project__organization=organization, - status=STATUS_QUERY_CHOICES[status_param], - ).values_list("id", flat=True) - ) - - for sg in data["data"]: - sg["group_ids"] = [gid for gid in sg["group_ids"] if gid in matching_ids] - # Drop supergroups that have no matching groups after filtering - data["data"] = [sg for sg in data["data"] if sg["group_ids"]] - - return Response(data) + return Response(orjson.loads(response.data)) diff --git a/src/sentry/seer/supergroups/explorer_lightweight_rca.py b/src/sentry/seer/supergroups/lightweight_rca.py similarity index 91% rename from src/sentry/seer/supergroups/explorer_lightweight_rca.py rename to src/sentry/seer/supergroups/lightweight_rca.py index b7459db4394d..26cf86b9715a 100644 --- a/src/sentry/seer/supergroups/explorer_lightweight_rca.py +++ b/src/sentry/seer/supergroups/lightweight_rca.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) -def trigger_explorer_lightweight_rca(group: Group) -> int | None: +def trigger_lightweight_rca(group: Group) -> int | None: """ Trigger a lightweight Explorer RCA run for the given group. @@ -26,7 +26,7 @@ def trigger_explorer_lightweight_rca(group: Group) -> int | None: """ has_feature = features.has("projects:supergroup-lightweight-rca", group.project) logger.info( - "explorer_lightweight_rca.feature_flag_check", + "lightweight_rca.feature_flag_check", extra={ "group_id": group.id, "project_id": group.project.id, @@ -66,7 +66,7 @@ def trigger_explorer_lightweight_rca(group: Group) -> int | None: ) logger.info( - "explorer_lightweight_rca.starting_run", + "lightweight_rca.starting_run", extra={ "group_id": group.id, "project_id": group.project.id, @@ -83,7 +83,7 @@ def trigger_explorer_lightweight_rca(group: Group) -> int | None: ) logger.info( - "explorer_lightweight_rca.run_started", + "lightweight_rca.run_started", extra={ "group_id": group.id, "project_id": group.project.id, @@ -94,7 +94,7 @@ def trigger_explorer_lightweight_rca(group: Group) -> int | None: return run_id except Exception: logger.exception( - "explorer_lightweight_rca.trigger_failed", + "lightweight_rca.trigger_failed", extra={ "group_id": group.id, "organization_id": group.organization.id, diff --git a/src/sentry/seer/supergroups/lightweight_rca_cluster.py b/src/sentry/seer/supergroups/lightweight_rca_cluster.py deleted file mode 100644 index e8920463d998..000000000000 --- a/src/sentry/seer/supergroups/lightweight_rca_cluster.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -import logging - -from sentry.api.serializers import EventSerializer, serialize -from sentry.eventstore import backend as eventstore -from sentry.models.group import Group -from sentry.seer.models import SeerApiError -from sentry.seer.signed_seer_api import ( - LightweightRCAClusterRequest, - SeerViewerContext, - make_lightweight_rca_cluster_request, -) - -logger = logging.getLogger(__name__) - - -def trigger_lightweight_rca_cluster(group: Group) -> None: - """ - Call Seer's lightweight RCA clustering endpoint for the given group. - - Sends issue event data to Seer, which generates a lightweight root cause analysis - and clusters the issue into supergroups based on embedding similarity. - """ - event = group.get_latest_event() - if not event: - logger.info( - "lightweight_rca_cluster.no_event", - extra={"group_id": group.id}, - ) - return - - ready_event = eventstore.get_event_by_id(group.project.id, event.event_id, group_id=group.id) - if not ready_event: - logger.info( - "lightweight_rca_cluster.event_not_ready", - extra={"group_id": group.id, "event_id": event.event_id}, - ) - return - - serialized_event = serialize(ready_event, None, EventSerializer()) - - body = LightweightRCAClusterRequest( - group_id=group.id, - issue={ - "id": group.id, - "title": group.title, - "short_id": group.qualified_short_id, - "events": [serialized_event], - }, - organization_slug=group.organization.slug, - organization_id=group.organization.id, - project_id=group.project.id, - ) - viewer_context = SeerViewerContext(organization_id=group.organization.id) - - response = make_lightweight_rca_cluster_request(body, timeout=30, viewer_context=viewer_context) - if response.status >= 400: - raise SeerApiError("Lightweight RCA cluster request failed", response.status) - - logger.info( - "lightweight_rca_cluster.success", - extra={ - "group_id": group.id, - "project_id": group.project.id, - "organization_id": group.organization.id, - }, - ) diff --git a/src/sentry/snuba/snuba_query_validator.py b/src/sentry/snuba/snuba_query_validator.py index bb906fc01d23..6f151d6ccfe3 100644 --- a/src/sentry/snuba/snuba_query_validator.py +++ b/src/sentry/snuba/snuba_query_validator.py @@ -24,7 +24,6 @@ ) from sentry.incidents.utils.constants import INCIDENTS_SNUBA_SUBSCRIPTION_TYPE from sentry.models.project import Project -from sentry.search.eap.constants import VALID_GRANULARITIES from sentry.search.eap.trace_metrics.validator import validate_trace_metrics_aggregate from sentry.snuba.dataset import Dataset from sentry.snuba.entity_subscription import ( @@ -52,13 +51,6 @@ # Allowed time windows (in seconds) for crash rate alerts CRASH_RATE_ALERTS_ALLOWED_TIME_WINDOWS = [1800, 3600, 7200, 14400, 43200, 86400] -MIN_EAP_ALERT_TIME_WINDOW_SECONDS = 300 - -# Valid time windows for EAP alerts: valid Snuba granularities at or above the alert minimum. -EAP_ALERTS_ALLOWED_TIME_WINDOWS = sorted( - g for g in VALID_GRANULARITIES if g >= MIN_EAP_ALERT_TIME_WINDOW_SECONDS -) - QUERY_TYPE_VALID_DATASETS = { SnubaQuery.Type.ERROR: {Dataset.Events}, @@ -338,8 +330,6 @@ def _validate_query(self, data: dict[str, Any]) -> None: # TODO(edward): Bypass snql query validation for EAP queries. Do we need validation for rpc requests? if dataset != Dataset.EventsAnalyticsPlatform: self._validate_snql_query(data, entity_subscription, projects) - else: - self._validate_time_window(data["time_window"], dataset) def _validate_snql_query( self, @@ -401,10 +391,9 @@ def _validate_time_window(self, value: int, dataset: Dataset) -> int: "30min, 1h, 2h, 4h, 12h and 24h" ) if dataset == Dataset.EventsAnalyticsPlatform: - if time_window_seconds not in EAP_ALERTS_ALLOWED_TIME_WINDOWS: + if time_window_seconds < 300: raise serializers.ValidationError( - f"Invalid Time Window: Allowed time windows (in seconds) for this alert type are: " - f"{EAP_ALERTS_ALLOWED_TIME_WINDOWS}" + "Invalid Time Window: Time window for this alert type must be at least 5 minutes." ) return time_window_seconds diff --git a/src/sentry/spans/consumers/process_segments/factory.py b/src/sentry/spans/consumers/process_segments/factory.py index a4132c15f4ed..808ad9441035 100644 --- a/src/sentry/spans/consumers/process_segments/factory.py +++ b/src/sentry/spans/consumers/process_segments/factory.py @@ -115,10 +115,7 @@ def _process_message( try: value = message.payload.value segment = orjson.loads(value) - skip_enrichment = segment.get("skip_enrichment", False) - processed = process_segment( - segment["spans"], skip_produce=skip_produce, skip_enrichment=skip_enrichment - ) + processed = process_segment(segment["spans"], skip_produce=skip_produce) return [_serialize_payload(span, message.timestamp) for span in processed] except Exception: logger.exception("segments.invalid-message") diff --git a/src/sentry/spans/consumers/process_segments/message.py b/src/sentry/spans/consumers/process_segments/message.py index 84e7a7b37590..dc86f59face9 100644 --- a/src/sentry/spans/consumers/process_segments/message.py +++ b/src/sentry/spans/consumers/process_segments/message.py @@ -50,7 +50,7 @@ @metrics.wraps("spans.consumers.process_segments.process_segment") def process_segment( - unprocessed_spans: list[SpanEvent], skip_produce: bool = False, skip_enrichment: bool = False + unprocessed_spans: list[SpanEvent], skip_produce: bool = False ) -> list[CompatibleSpan]: sample_rate = ( settings.SENTRY_PROCESS_SEGMENTS_TRANSACTIONS_SAMPLE_RATE @@ -62,17 +62,14 @@ def process_segment( "sample_rate": sample_rate, }, ): - return _process_segment(unprocessed_spans, skip_produce, skip_enrichment) + return _process_segment(unprocessed_spans, skip_produce) def _process_segment( - unprocessed_spans: list[SpanEvent], skip_produce: bool, skip_enrichment: bool + unprocessed_spans: list[SpanEvent], skip_produce: bool ) -> list[CompatibleSpan]: _verify_compatibility(unprocessed_spans) - if skip_enrichment: - return [make_compatible(span) for span in unprocessed_spans] - if unprocessed_spans: project_id = unprocessed_spans[0].get("project_id") if project_id in options.get("spans.process-segments.skip-enrichment-projects"): diff --git a/src/sentry/synapse/endpoints/org_cell_mappings.py b/src/sentry/synapse/endpoints/org_cell_mappings.py index c713117440a1..41ca5ff65623 100644 --- a/src/sentry/synapse/endpoints/org_cell_mappings.py +++ b/src/sentry/synapse/endpoints/org_cell_mappings.py @@ -3,7 +3,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import Endpoint, internal_control_silo_endpoint +from sentry.api.base import Endpoint, control_silo_endpoint from sentry.models.organizationmapping import OrganizationMapping from sentry.synapse.endpoints.authentication import ( SynapseAuthPermission, @@ -13,7 +13,7 @@ from sentry.types.cell import get_global_directory -@internal_control_silo_endpoint +@control_silo_endpoint class OrgCellMappingsEndpoint(Endpoint): """ Returns the organization-to-cell mappings for all orgs in pages. diff --git a/src/sentry/synapse/endpoints/project_key_cell_mappings.py b/src/sentry/synapse/endpoints/project_key_cell_mappings.py index ab90438737ba..207c32bd7ad3 100644 --- a/src/sentry/synapse/endpoints/project_key_cell_mappings.py +++ b/src/sentry/synapse/endpoints/project_key_cell_mappings.py @@ -3,7 +3,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import Endpoint, internal_control_silo_endpoint +from sentry.api.base import Endpoint, control_silo_endpoint from sentry.models.projectkeymapping import ProjectKeyMapping from sentry.synapse.endpoints.authentication import ( SynapseAuthPermission, @@ -13,7 +13,7 @@ from sentry.types.cell import get_global_directory -@internal_control_silo_endpoint +@control_silo_endpoint class ProjectKeyCellMappingsEndpoint(Endpoint): """ Returns the project-key-to-cell mappings for all project keys in pages. diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index a7d5ac582ad2..f4d8390e65c6 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -1584,22 +1584,6 @@ def kick_off_seer_automation(job: PostProcessJob) -> None: ) -def kick_off_lightweight_rca_cluster(job: PostProcessJob) -> None: - from sentry.tasks.seer.lightweight_rca_cluster import trigger_lightweight_rca_cluster_task - - if not job["group_state"]["is_new"]: - return - - event = job["event"] - group = event.group - - enabled_orgs: list[int] = options.get("supergroups.lightweight-enabled-orgs") - if group.organization.id not in enabled_orgs: - return - - trigger_lightweight_rca_cluster_task.delay(group.id) - - GROUP_CATEGORY_POST_PROCESS_PIPELINE: dict[ GroupCategory, list[Callable[[PostProcessJob], None]] ] = { @@ -1612,7 +1596,6 @@ def kick_off_lightweight_rca_cluster(job: PostProcessJob) -> None: handle_owner_assignment, handle_auto_assignment, kick_off_seer_automation, - kick_off_lightweight_rca_cluster, process_workflow_engine_issue_alerts, process_resource_change_bounds, process_data_forwarding, diff --git a/src/sentry/tasks/seer/lightweight_rca_cluster.py b/src/sentry/tasks/seer/lightweight_rca_cluster.py deleted file mode 100644 index 9d8abcbb4dc7..000000000000 --- a/src/sentry/tasks/seer/lightweight_rca_cluster.py +++ /dev/null @@ -1,32 +0,0 @@ -import logging - -from sentry.models.group import Group -from sentry.seer.supergroups.lightweight_rca_cluster import trigger_lightweight_rca_cluster -from sentry.tasks.base import instrumented_task -from sentry.taskworker.namespaces import ingest_errors_tasks - -logger = logging.getLogger(__name__) - - -@instrumented_task( - name="sentry.tasks.seer.lightweight_rca_cluster.trigger_lightweight_rca_cluster_task", - namespace=ingest_errors_tasks, -) -def trigger_lightweight_rca_cluster_task(group_id: int, **kwargs) -> None: - try: - group = Group.objects.get(id=group_id) - except Group.DoesNotExist: - logger.info( - "lightweight_rca_cluster_task.group_not_found", - extra={"group_id": group_id}, - ) - return - - try: - trigger_lightweight_rca_cluster(group) - except Exception: - logger.exception( - "lightweight_rca_cluster_task.failed", - extra={"group_id": group_id}, - ) - raise diff --git a/src/sentry/tasks/seer/night_shift.py b/src/sentry/tasks/seer/night_shift.py deleted file mode 100644 index 74407c0b4ba2..000000000000 --- a/src/sentry/tasks/seer/night_shift.py +++ /dev/null @@ -1,116 +0,0 @@ -from __future__ import annotations - -import logging -from collections.abc import Sequence -from datetime import timedelta - -import sentry_sdk - -from sentry import features, options -from sentry.models.organization import Organization, OrganizationStatus -from sentry.tasks.base import instrumented_task -from sentry.taskworker.namespaces import seer_tasks -from sentry.utils.iterators import chunked -from sentry.utils.query import RangeQuerySetWrapper - -logger = logging.getLogger("sentry.tasks.seer.night_shift") - -NIGHT_SHIFT_DISPATCH_STEP_SECONDS = 37 -NIGHT_SHIFT_SPREAD_DURATION = timedelta(hours=4) - -FEATURE_NAMES = [ - "organizations:seer-night-shift", - "organizations:gen-ai-features", -] - - -@instrumented_task( - name="sentry.tasks.seer.night_shift.schedule_night_shift", - namespace=seer_tasks, - processing_deadline_duration=15 * 60, -) -def schedule_night_shift() -> None: - """ - Nightly scheduler: iterates active orgs in batches, checks feature flags - in bulk, and dispatches per-org worker tasks with jitter. - """ - if not options.get("seer.night_shift.enable"): - return - - spread_seconds = int(NIGHT_SHIFT_SPREAD_DURATION.total_seconds()) - batch_index = 0 - - for org_batch in chunked( - RangeQuerySetWrapper[Organization]( - Organization.objects.filter(status=OrganizationStatus.ACTIVE), - step=1000, - ), - 100, - ): - for org in _get_eligible_orgs_from_batch(org_batch): - if bool(org.get_option("sentry:hide_ai_features")): - continue - - delay = (batch_index * NIGHT_SHIFT_DISPATCH_STEP_SECONDS) % spread_seconds - - run_night_shift_for_org.apply_async( - args=[org.id], - countdown=delay, - ) - batch_index += 1 - - logger.info( - "night_shift.schedule_complete", - extra={"orgs_dispatched": batch_index}, - ) - - -@instrumented_task( - name="sentry.tasks.seer.night_shift.run_night_shift_for_org", - namespace=seer_tasks, - processing_deadline_duration=5 * 60, -) -def run_night_shift_for_org(organization_id: int) -> None: - try: - organization = Organization.objects.get( - id=organization_id, status=OrganizationStatus.ACTIVE - ) - except Organization.DoesNotExist: - return - - sentry_sdk.set_tags( - { - "organization_id": organization.id, - "organization_slug": organization.slug, - } - ) - - logger.info( - "night_shift.org_dispatched", - extra={ - "organization_id": organization_id, - "organization_slug": organization.slug, - }, - ) - - -def _get_eligible_orgs_from_batch( - orgs: Sequence[Organization], -) -> list[Organization]: - """ - Check feature flags for a batch of orgs using batch_has_for_organizations. - Returns orgs that have all required feature flags enabled. - """ - eligible = list(orgs) - - for feature_name in FEATURE_NAMES: - batch_result = features.batch_has_for_organizations(feature_name, eligible) - if batch_result is None: - raise RuntimeError(f"batch_has_for_organizations returned None for {feature_name}") - - eligible = [org for org in eligible if batch_result.get(f"organization:{org.id}", False)] - - if not eligible: - return [] - - return eligible diff --git a/src/sentry/viewer_context.py b/src/sentry/viewer_context.py index 0e1926a67c06..b3c28788efc0 100644 --- a/src/sentry/viewer_context.py +++ b/src/sentry/viewer_context.py @@ -62,19 +62,6 @@ def serialize(self) -> dict[str, Any]: result["token"] = {"kind": self.token.kind, "scopes": list(self.token.get_scopes())} return result - @classmethod - def deserialize(cls, data: dict[str, Any]) -> ViewerContext: - """Reconstruct from a serialized dict. Token is not deserialized.""" - try: - actor_type = ActorType(data.get("actor_type", "unknown")) - except ValueError: - actor_type = ActorType.UNKNOWN - return cls( - organization_id=data.get("organization_id"), - user_id=data.get("user_id"), - actor_type=actor_type, - ) - @contextlib.contextmanager def viewer_context_scope(ctx: ViewerContext) -> Generator[None]: diff --git a/src/sentry/workflow_engine/defaults/workflows.py b/src/sentry/workflow_engine/defaults/workflows.py deleted file mode 100644 index 058cec2654c3..000000000000 --- a/src/sentry/workflow_engine/defaults/workflows.py +++ /dev/null @@ -1,116 +0,0 @@ -from typing import Sequence - -from django.db import router, transaction - -from sentry.models.organization import Organization -from sentry.models.project import Project -from sentry.notifications.models.notificationaction import ActionTarget -from sentry.notifications.types import FallthroughChoiceType -from sentry.workflow_engine.defaults.detectors import _ensure_detector -from sentry.workflow_engine.models import ( - Action, - DataCondition, - DataConditionGroup, - DataConditionGroupAction, - DetectorWorkflow, - Workflow, - WorkflowDataConditionGroup, -) -from sentry.workflow_engine.models.data_condition import Condition -from sentry.workflow_engine.typings.grouptype import IssueStreamGroupType - -DEFAULT_WORKFLOW_LABEL = "Send a notification for high priority issues" - - -def connect_workflows_to_issue_stream( - project: Project, - workflows: list[Workflow], -) -> Sequence[DetectorWorkflow]: - # Because we don't know if this signal is handled already or not... - issue_stream_detector = _ensure_detector(project, IssueStreamGroupType.slug) - - connections = [ - DetectorWorkflow( - workflow=workflow, - detector=issue_stream_detector, - ) - for workflow in workflows - ] - return DetectorWorkflow.objects.bulk_create( - connections, - ignore_conflicts=True, - ) - - -def create_priority_workflow(org: Organization) -> Workflow: - existing = Workflow.objects.filter(organization=org, name=DEFAULT_WORKFLOW_LABEL).first() - if existing: - return existing - - with transaction.atomic(router.db_for_write(Workflow)): - when_condition_group = DataConditionGroup.objects.create( - logic_type=DataConditionGroup.Type.ANY_SHORT_CIRCUIT, - organization=org, - ) - - workflow = Workflow.objects.create( - organization=org, - name=DEFAULT_WORKFLOW_LABEL, - when_condition_group=when_condition_group, - config={"frequency": 0}, - ) - - # Create the workflow trigger conditions - conditions: list[DataCondition] = [] - conditions.append( - DataCondition( - type=Condition.NEW_HIGH_PRIORITY_ISSUE, - condition_group=workflow.when_condition_group, - comparison=True, - condition_result=True, - ) - ) - conditions.append( - DataCondition( - type=Condition.EXISTING_HIGH_PRIORITY_ISSUE, - condition_group=workflow.when_condition_group, - comparison=True, - condition_result=True, - ) - ) - DataCondition.objects.bulk_create(conditions) - - # Create the Action - action_filter = DataConditionGroup.objects.create( - logic_type=DataConditionGroup.Type.ANY_SHORT_CIRCUIT, - organization=org, - ) - - action = Action.objects.create( - type=Action.Type.EMAIL, - config={ - "target_type": ActionTarget.ISSUE_OWNERS, - "target_identifier": None, - }, - data={ - "fallthrough_type": FallthroughChoiceType.ACTIVE_MEMBERS.value, - }, - ) - DataConditionGroupAction.objects.create( - action=action, - condition_group=action_filter, - ) - - WorkflowDataConditionGroup.objects.create( - workflow=workflow, - condition_group=action_filter, - ) - - return workflow - - -def ensure_default_workflows(project: Project) -> list[Workflow]: - workflows = [create_priority_workflow(project.organization)] - connect_workflows_to_issue_stream(project, workflows) - - return workflows diff --git a/src/sentry/workflow_engine/migration_helpers/issue_alert_migration.py b/src/sentry/workflow_engine/migration_helpers/issue_alert_migration.py index 88966534a528..29a4edf3354c 100644 --- a/src/sentry/workflow_engine/migration_helpers/issue_alert_migration.py +++ b/src/sentry/workflow_engine/migration_helpers/issue_alert_migration.py @@ -77,7 +77,7 @@ def run(self) -> Workflow: return workflow - def _create_detector_lookups(self) -> list[Detector]: + def _create_detector_lookups(self) -> list[Detector | None]: if self.rule.source == RuleSource.CRON_MONITOR: # Find the cron detector that was created before the rule monitor_slug = None @@ -87,7 +87,7 @@ def _create_detector_lookups(self) -> list[Detector]: break if not monitor_slug: - return [] + return [None] try: with in_test_hide_transaction_boundary(): @@ -105,7 +105,7 @@ def _create_detector_lookups(self) -> list[Detector]: except (Monitor.DoesNotExist, Detector.DoesNotExist): pass - return [] + return [None] if self.is_dry_run: error_detector = Detector.objects.filter( @@ -135,30 +135,13 @@ def _create_detector_lookups(self) -> list[Detector]: defaults={"config": {}, "name": ISSUE_STREAM_DETECTOR_NAME}, ) - # We are not returning the error_detector here to simplify - # _connect_default_detectors - return [issue_stream_detector] + return [error_detector, issue_stream_detector] def _connect_default_detectors(self, workflow: Workflow) -> None: default_detectors = self._create_detector_lookups() - - # do not add references to both issue stream and error group types - # it seems like other types might be relying on this as well, - # so this just says not to link the error groups. - # TODO - provide helpers to more easily create these classes - # and references in code, so we can remove the reliance on this code - references_to_create = [ - DetectorWorkflow( - detector=detector, - workflow=workflow, - ) - for detector in default_detectors - ] - - DetectorWorkflow.objects.bulk_create( - references_to_create, - ignore_conflicts=True, - ) + for detector in default_detectors: + if detector: + DetectorWorkflow.objects.get_or_create(detector=detector, workflow=workflow) def _bulk_create_data_conditions( self, diff --git a/src/sentry/workflow_engine/receivers/__init__.py b/src/sentry/workflow_engine/receivers/__init__.py index f5aadccdd370..0434ce88dd83 100644 --- a/src/sentry/workflow_engine/receivers/__init__.py +++ b/src/sentry/workflow_engine/receivers/__init__.py @@ -6,6 +6,5 @@ from .detector import * # NOQA from .detector_workflow import * # NOQA from .project_detectors import * # noqa: F401,F403 -from .project_workflows import * # noqa: F401,F403 from .workflow import * # NOQA from .workflow_data_condition_group import * # NOQA diff --git a/static/app/actionCreators/organizations.tsx b/static/app/actionCreators/organizations.tsx index 427732153421..738deb780471 100644 --- a/static/app/actionCreators/organizations.tsx +++ b/static/app/actionCreators/organizations.tsx @@ -234,7 +234,7 @@ export async function fetchOrganizations(api: Client, query?: Record - api.requestPromise('/organizations/', { + api.requestPromise(`/organizations/`, { host: region.url, query, // Authentication errors can happen as we span regions. diff --git a/static/app/bootstrap/initializeSdk.spec.tsx b/static/app/bootstrap/initializeSdk.spec.tsx index 4b4c48eb784a..b4bb71e5a1db 100644 --- a/static/app/bootstrap/initializeSdk.spec.tsx +++ b/static/app/bootstrap/initializeSdk.spec.tsx @@ -162,7 +162,7 @@ describe('isFilteredRequestErrorEvent', () => { }); describe('non-matching error type, non-matching message', () => { - it('rejects other errors', () => { + it(`rejects other errors`, () => { const event = { exception: { values: [{type: 'UncaughtSquirrelError', value: 'Squirrel was not caught'}], @@ -172,7 +172,7 @@ describe('isFilteredRequestErrorEvent', () => { expect(isFilteredRequestErrorEvent(event)).toBeFalsy(); }); - it('rejects other errors as causes', () => { + it(`rejects other errors as causes`, () => { const event = { exception: { values: [ diff --git a/static/app/components/aiPrivacyTooltip.tsx b/static/app/components/aiPrivacyTooltip.tsx index 64eabddb91bb..0d1b1a902c99 100644 --- a/static/app/components/aiPrivacyTooltip.tsx +++ b/static/app/components/aiPrivacyTooltip.tsx @@ -33,7 +33,7 @@ export function AiPrivacyNotice({linkProps = {}}: AiPrivacyNoticeProps) { * A shortened version of the privacy noice, useful for tooltips or places where space is limited. */ function AiPrivacyNoticeShort({linkProps = {}}: AiPrivacyNoticeProps) { - return tct('Powered by genAI. [link:Learn more.]', { + return tct(`Powered by genAI. [link:Learn more.]`, { link: , }); } diff --git a/static/app/components/alerts/unsupportedAlert.tsx b/static/app/components/alerts/unsupportedAlert.tsx index a578b7cc1068..7bc8084d9949 100644 --- a/static/app/components/alerts/unsupportedAlert.tsx +++ b/static/app/components/alerts/unsupportedAlert.tsx @@ -13,10 +13,10 @@ export function UnsupportedAlert({featureName, projectSlug}: Props) { }> {projectSlug ? ( - {t("%s isn't available for %s.", featureName, projectSlug)} + {t(`%s isn't available for %s.`, featureName, projectSlug)} ) : ( - {t("%s isn't available for the selected projects.", featureName)} + {t(`%s isn't available for the selected projects.`, featureName)} )} diff --git a/static/app/components/animatedSentryLogo.tsx b/static/app/components/animatedSentryLogo.tsx deleted file mode 100644 index afc089e5b5f1..000000000000 --- a/static/app/components/animatedSentryLogo.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import {useId} from 'react'; -import {useTheme} from '@emotion/react'; - -interface AnimatedSentryLogoProps { - /** - * Draw progress from 0 (no stroke) to 1 (fully drawn). - * Uses the stroke-dashoffset technique from the app loading screen. - */ - progress: number; - className?: string; - size?: number; -} - -const DASH_TOTAL = 1150; - -// Filled glyph (same path as index.ejs base + mask) -const SENTRY_GLYPH = - 'M223.687 85.56a28.02 28.02 0 0 0-24-13.559 28.02 28.02 0 0 0-24 13.559l-39.48 67.62a193.26 193.26 0 0 1 106.5 159.96h-27.72a166.07 166.07 0 0 0-92.76-136.32L85.687 240a95.52 95.52 0 0 1 55.38 73.02h-63.66a4.55 4.55 0 0 1-3.659-2.325 4.56 4.56 0 0 1-.06-4.335l17.64-30a64.4 64.4 0 0 0-20.16-11.4l-17.46 30a27.24 27.24 0 0 0 2.023 30.435 27.2 27.2 0 0 0 8.116 7.005 27.96 27.96 0 0 0 13.56 3.6h87.18a116.41 116.41 0 0 0-48-103.86l13.86-24a143.22 143.22 0 0 1 61.8 127.86h73.86a215.28 215.28 0 0 0-98.46-190.8l28.02-48a4.62 4.62 0 0 1 6.3-1.62c3.18 1.74 121.74 208.62 123.96 211.02a4.562 4.562 0 0 1-4.08 6.78h-28.56q.54 11.46 0 22.86h28.68a27.54 27.54 0 0 0 27.72-27.66 26.94 26.94 0 0 0-3.72-13.68z'; - -// Stroke paths that trace the logo outline (one group from index.ejs) -const STROKE_PATHS = [ - 'm82.8 253.5-12.7 22-12.5 21.2-.2.3v.1l-.2.2v.3a24 24 0 00.3 22.4l.3.6A24 24 0 0076.4 332h87l-3.2-21.8A115 115 0 00112 233l17.6-30.4a147 147 0 0166.8 112l1.3 17.5h65.6l-1.3-20.2a212 212 0 00-99.3-165.8l29.6-50.8a9 9 0 013.3-3.1 9 9 0 0112.1 3.1l122.2 209.2h.1a8 8 0 011 4v.2a8.5 8.5 0 01-8.6 8.5h-38.9', - 'M92.5 268.7 88 276.6 71.4 305v.1a8 8 0 000 7.2l.2.4a8 8 0 006 3.4h67.3l-.6-3.4A99 99 0 0097.2 242.5l-6.8-4 33.4-57.8 6.9 4a163 163 0 0181.6 128.8l.2 2.7h33.7L246 313A196 196 0 00147.8 155.7l-7-4L178.6 87v-.1a25 25 0 0133-9.1l.8.3a25 25 0 019 8.9h.1v.2L343.7 296.4a24 24 0 013.3 12v.4a24.5 24.5 0 01-24.6 24.4H291.5', - 'M85.7 264.5l-8.7 15-12.5 21.3-.1.2-.1.3A15.6 15.6 0 0076.9 324h77.2l-1.8-12.6a107 107 0 00-51-75.8l25.5-44.1A155 155 0 01204.3 314l.8 10.1h49.7l-.8-11.7A204 204 0 00151.8 148.9l33.6-57.7a17 17 0 0129.2 0L336.8 300.4a16 16 0 012.2 8.2 16.5 16.5 0 01-16.6 16.6H291.5', -]; - -/** - * Sentry brand mark with stroke-draw progress, adapted from the app - * loading animation in static/index.ejs. Single color, progress-driven - * instead of the looping multicolor original. - */ -export function AnimatedSentryLogo({ - progress, - size = 72, - className, -}: AnimatedSentryLogoProps) { - const theme = useTheme(); - const id = useId(); - const gooId = `goo-layer-${id}`; - const maskId = `mask-layer-${id}`; - - const dashOffset = DASH_TOTAL * (1 - progress); - - return ( - - - - - - - - - - - - - - {/* Muted base shape */} - - - {/* Stroke-draw overlay, masked to the logo shape */} - - {STROKE_PATHS.map((d, i) => ( - - ))} - - - ); -} diff --git a/static/app/components/assistant/getGuidesContent.tsx b/static/app/components/assistant/getGuidesContent.tsx index 39b7e0ff26af..5d03a5aefc89 100644 --- a/static/app/components/assistant/getGuidesContent.tsx +++ b/static/app/components/assistant/getGuidesContent.tsx @@ -26,7 +26,7 @@ export function getGuidesContent(): GuidesContent { title: t('Find problematic releases'), target: 'issue_sidebar_releases', description: t( - 'See which release introduced the issue and which release it last appeared in.' + `See which release introduced the issue and which release it last appeared in.` ), }, { @@ -42,7 +42,7 @@ export function getGuidesContent(): GuidesContent { title: t('Pinpoint hotspots'), target: 'issue_sidebar_tags', description: t( - 'Tags are key/value string pairs that are automatically indexed and searchable in Sentry.' + `Tags are key/value string pairs that are automatically indexed and searchable in Sentry.` ), }, { @@ -98,14 +98,14 @@ export function getGuidesContent(): GuidesContent { title: t('Event Breakdown'), target: 'trace_view_guide_breakdown', description: t( - 'The event breakdown shows you the breakdown of event types within a trace.' + `The event breakdown shows you the breakdown of event types within a trace.` ), }, { title: t('Events'), target: 'trace_view_guide_row', description: t( - 'You can quickly see errors and transactions in a trace alongside the project, transaction duration and any errors or performance issues related to the transaction.' + `You can quickly see errors and transactions in a trace alongside the project, transaction duration and any errors or performance issues related to the transaction.` ), }, { diff --git a/static/app/components/carousel.tsx b/static/app/components/carousel.tsx index eaf62146f2b6..ffff09b23639 100644 --- a/static/app/components/carousel.tsx +++ b/static/app/components/carousel.tsx @@ -97,7 +97,7 @@ const CarouselItems = styled('div')` const StyledArrowButton = styled(Button)<{direction: string}>` position: absolute; - ${p => (p.direction === 'left' ? 'left: 0;' : 'right: 0;')} + ${p => (p.direction === 'left' ? `left: 0;` : `right: 0;`)} top: 0; bottom: 0; height: 36px; diff --git a/static/app/components/charts/chartWidgetLoader.stories.tsx b/static/app/components/charts/chartWidgetLoader.stories.tsx index 6d2417a9c210..14a362dbe60d 100644 --- a/static/app/components/charts/chartWidgetLoader.stories.tsx +++ b/static/app/components/charts/chartWidgetLoader.stories.tsx @@ -31,7 +31,7 @@ export default Storybook.story('ChartWidgetLoader', story => { render these chart widgets).

- {''} + {``} ); }); diff --git a/static/app/components/charts/components/xAxis.tsx b/static/app/components/charts/components/xAxis.tsx index d0cdb8e72e4c..c70815e87507 100644 --- a/static/app/components/charts/components/xAxis.tsx +++ b/static/app/components/charts/components/xAxis.tsx @@ -37,7 +37,7 @@ export function XAxis({ const showDate = firstItem ? true : !computeShortInterval({start, end, period}); if (isGroupedByDate) { - const dateFormat = useShortDate ? 'MMM Do' : 'MMM D'; + const dateFormat = useShortDate ? 'MMM Do' : `MMM D`; const dateString = getFormattedDate(value, dateFormat, {local: !utc}); const timeFormat = getTimeFormat({seconds: addSecondsToTimeFormat}); diff --git a/static/app/components/commitRow.tsx b/static/app/components/commitRow.tsx index bff7479364cb..77dc1c3552b5 100644 --- a/static/app/components/commitRow.tsx +++ b/static/app/components/commitRow.tsx @@ -50,7 +50,7 @@ function CommitRow({ const handleInviteClick = useCallback(() => { if (!commit.author?.email) { Sentry.captureException( - new Error('Commit author has no email or id, invite flow is broken.') + new Error(`Commit author has no email or id, invite flow is broken.`) ); return; } diff --git a/static/app/components/contextPickerModal.spec.tsx b/static/app/components/contextPickerModal.spec.tsx index eb1a128063db..db044f9d2fe5 100644 --- a/static/app/components/contextPickerModal.spec.tsx +++ b/static/app/components/contextPickerModal.spec.tsx @@ -252,7 +252,7 @@ describe('ContextPickerModal', () => { const provider = {slug: 'github'}; const configQueryKey = [ - getApiUrl('/organizations/$organizationIdOrSlug/integrations/', { + getApiUrl(`/organizations/$organizationIdOrSlug/integrations/`, { path: {organizationIdOrSlug: org.slug}, }), {query: {provider_key: provider.slug, includeConfig: 0}}, @@ -299,7 +299,7 @@ describe('ContextPickerModal', () => { const provider = {slug: 'github'}; const configQueryKey = [ - getApiUrl('/organizations/$organizationIdOrSlug/integrations/', { + getApiUrl(`/organizations/$organizationIdOrSlug/integrations/`, { path: {organizationIdOrSlug: org.slug}, }), {query: {provider_key: provider.slug, includeConfig: 0}}, @@ -337,7 +337,7 @@ describe('ContextPickerModal', () => { const provider = {slug: 'github'}; const configQueryKey = [ - getApiUrl('/organizations/$organizationIdOrSlug/integrations/', { + getApiUrl(`/organizations/$organizationIdOrSlug/integrations/`, { path: {organizationIdOrSlug: org.slug}, }), {query: {provider_key: provider.slug, includeConfig: 0}}, diff --git a/static/app/components/core/code/__stories__/components.tsx b/static/app/components/core/code/__stories__/components.tsx index 2dd47d9303ae..03928f58235c 100644 --- a/static/app/components/core/code/__stories__/components.tsx +++ b/static/app/components/core/code/__stories__/components.tsx @@ -21,7 +21,7 @@ export function OnTabClickExample() { onTabClick={handleTabClick} language="bash" > - {tab === 'npm' ? 'npm install --save @sentry/browser' : 'yarn add @sentry/browser'} + {tab === 'npm' ? `npm install --save @sentry/browser` : 'yarn add @sentry/browser'} ); } diff --git a/static/app/components/core/code/codeBlock.mdx b/static/app/components/core/code/codeBlock.mdx index b9e8ef50f7d2..d2e52b368d00 100644 --- a/static/app/components/core/code/codeBlock.mdx +++ b/static/app/components/core/code/codeBlock.mdx @@ -84,7 +84,7 @@ Display a filename at the top of the code block using the `filename` prop. - {"const myVariable = 'testing';"} + {`const myVariable = 'testing';`} ```jsx @@ -97,7 +97,7 @@ Display a filename at the top of the code block using the `filename` prop. - {"const myVariable = 'testing';"} + {`const myVariable = 'testing';`} ```jsx @@ -122,7 +122,7 @@ export function TabExample() { onTabClick={t => setTab(t)} language="bash" > - {tab === 'npm' ? 'npm install --save @sentry/browser' : 'yarn add @sentry/browser'} + {tab === 'npm' ? `npm install --save @sentry/browser` : 'yarn add @sentry/browser'} ); } diff --git a/static/app/components/core/compactSelect/compactSelect.spec.tsx b/static/app/components/core/compactSelect/compactSelect.spec.tsx index 995a13dd51e6..f64ea44fb9e2 100644 --- a/static/app/components/core/compactSelect/compactSelect.spec.tsx +++ b/static/app/components/core/compactSelect/compactSelect.spec.tsx @@ -881,11 +881,11 @@ describe('CompactSelect', () => { expect(screen.getByPlaceholderText('Search…')).toHaveFocus(); }); // Option Three is not reachable via keyboard, focus wraps back to Option One - await userEvent.keyboard('{ArrowDown}'); + await userEvent.keyboard(`{ArrowDown}`); await waitFor(() => { expect(screen.getByRole('option', {name: 'Option One'})).toHaveFocus(); }); - await userEvent.keyboard('{ArrowDown>2}'); + await userEvent.keyboard(`{ArrowDown>2}`); await waitFor(() => { expect(screen.getByRole('option', {name: 'Option One'})).toHaveFocus(); }); @@ -1318,9 +1318,9 @@ describe('CompactSelect', () => { expect(screen.getByPlaceholderText('Search…')).toHaveFocus(); }); // Option Three is not reachable via keyboard, focus wraps back to Option One - await userEvent.keyboard('{ArrowDown}'); + await userEvent.keyboard(`{ArrowDown}`); expect(screen.getByRole('row', {name: 'Option One'})).toHaveFocus(); - await userEvent.keyboard('{ArrowDown>2}'); + await userEvent.keyboard(`{ArrowDown>2}`); expect(screen.getByRole('row', {name: 'Option One'})).toHaveFocus(); // Option Three is still available via search diff --git a/static/app/components/core/compactSelect/control.tsx b/static/app/components/core/compactSelect/control.tsx index e3032495eb7e..bc99eecc9b20 100644 --- a/static/app/components/core/compactSelect/control.tsx +++ b/static/app/components/core/compactSelect/control.tsx @@ -683,7 +683,7 @@ const StyledOverlay = styled(Overlay, { ${p => p.width && `width: ${withUnits(p.width)};`} ${p => p.height && `height: ${withUnits(p.height)};`} ${p => p.minWidth && `min-width: ${withUnits(p.minWidth)};`} - max-width: ${p => (p.maxWidth ? `min(${withUnits(p.maxWidth)}, 100%)` : '100%')}; + max-width: ${p => (p.maxWidth ? `min(${withUnits(p.maxWidth)}, 100%)` : `100%`)}; max-height: ${p => p.maxHeight ? `min(${withUnits(p.maxHeight)}, ${withUnits(p.maxHeightProp)})` diff --git a/static/app/components/core/input/inputGroup.tsx b/static/app/components/core/input/inputGroup.tsx index 90ffa88078ba..cd4bdaa8d1ba 100644 --- a/static/app/components/core/input/inputGroup.tsx +++ b/static/app/components/core/input/inputGroup.tsx @@ -80,7 +80,7 @@ const StyledLeadingItemsWrap = styled(InputItemsWrap)<{ disablePointerEvents?: boolean; }>` left: ${p => p.theme.form[p.size].paddingLeft + 1}px; - ${p => p.disablePointerEvents && 'pointer-events: none;'} + ${p => p.disablePointerEvents && `pointer-events: none;`} `; const StyledTrailingItemsWrap = styled(InputItemsWrap)<{ @@ -88,7 +88,7 @@ const StyledTrailingItemsWrap = styled(InputItemsWrap)<{ disablePointerEvents?: boolean; }>` right: ${p => p.theme.form[p.size].paddingRight + 1}px; - ${p => p.disablePointerEvents && 'pointer-events: none;'} + ${p => p.disablePointerEvents && `pointer-events: none;`} `; interface InputContext { diff --git a/static/app/components/core/loader/indeterminateLoader.tsx b/static/app/components/core/loader/indeterminateLoader.tsx index 337a8b0f34e3..232594ffce01 100644 --- a/static/app/components/core/loader/indeterminateLoader.tsx +++ b/static/app/components/core/loader/indeterminateLoader.tsx @@ -16,8 +16,7 @@ interface IndeterminateLoaderProps extends React.HTMLAttributes variant?: 'vibrant' | 'monochrome'; } -const SQUIGGLE_TILE = - "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='1 0 16 8'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M17 6c-4 0-4-4-8-4S5 6 1 6'/%3E%3C/svg%3E\")"; +const SQUIGGLE_TILE = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='1 0 16 8'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M17 6c-4 0-4-4-8-4S5 6 1 6'/%3E%3C/svg%3E")`; const indeterminateSlow = keyframes` 0% { left: -35%; right: 100%; } diff --git a/static/app/components/core/menuListItem/menuListItem.tsx b/static/app/components/core/menuListItem/menuListItem.tsx index 4e3bbc029893..2644c12c2531 100644 --- a/static/app/components/core/menuListItem/menuListItem.tsx +++ b/static/app/components/core/menuListItem/menuListItem.tsx @@ -87,7 +87,7 @@ const StyledInnerWrap = styled('div', { box-shadow: none; outline: none; } - ${p => p.disabled && 'cursor: default;'} + ${p => p.disabled && `cursor: default;`} &::before { content: ''; @@ -134,7 +134,7 @@ const StyledLeadingItems = styled('div')<{ flex-shrink: 0; align-items: flex-start; - ${p => p.disabled && 'opacity: 0.5;'} + ${p => p.disabled && `opacity: 0.5;`} `; const StyledLabel = styled('div')` @@ -431,7 +431,7 @@ const TrailingItems = styled('div')<{disabled: boolean}>` gap: ${p => p.theme.space.md}; margin-right: ${p => p.theme.space.xs}; - ${p => p.disabled && 'opacity: 0.5;'} + ${p => p.disabled && `opacity: 0.5;`} `; export const LeadingItems = StyledLeadingItems; diff --git a/static/app/components/core/slot/slot.tsx b/static/app/components/core/slot/slot.tsx index 8ecd3cf31aa0..7c0aab1589a2 100644 --- a/static/app/components/core/slot/slot.tsx +++ b/static/app/components/core/slot/slot.tsx @@ -226,7 +226,7 @@ function makeSlotProvider( return {children}; } - SlotProvider.displayName = 'Slot.Provider'; + SlotProvider.displayName = `Slot.Provider`; return SlotProvider as (props: SlotProviderProps) => React.ReactNode; } diff --git a/static/app/components/createAlertButton.spec.tsx b/static/app/components/createAlertButton.spec.tsx index 199781b4ccfc..1ca0a78e05c5 100644 --- a/static/app/components/createAlertButton.spec.tsx +++ b/static/app/components/createAlertButton.spec.tsx @@ -151,14 +151,14 @@ describe('CreateAlertFromViewButton', () => { location: { pathname: '/organizations/org-slug/issues/alerts/wizard/', }, - route: '/organizations/:orgId/issues/alerts/wizard/', + route: `/organizations/:orgId/issues/alerts/wizard/`, }, } ); await userEvent.click(screen.getByRole('button')); expect(router.location).toEqual( expect.objectContaining({ - pathname: '/organizations/org-slug/issues/alerts/wizard/', + pathname: `/organizations/org-slug/issues/alerts/wizard/`, query: {}, }) ); @@ -202,7 +202,7 @@ describe('CreateAlertFromViewButton', () => { await userEvent.click(screen.getByRole('button')); expect(router.location).toEqual( expect.objectContaining({ - pathname: '/organizations/org-slug/issues/alerts/new/metric/', + pathname: `/organizations/org-slug/issues/alerts/new/metric/`, query: expect.objectContaining({ query: 'event.type:error ', project: 'project-slug', diff --git a/static/app/components/discover/transactionsTable.tsx b/static/app/components/discover/transactionsTable.tsx index cc8f1d4e0d81..967bf22fb576 100644 --- a/static/app/components/discover/transactionsTable.tsx +++ b/static/app/components/discover/transactionsTable.tsx @@ -92,7 +92,7 @@ export function TransactionsTable(props: Props) { size="xs" position="top" title={t( - 'Span durations are summed over the course of an entire transaction. Any overlapping spans are only counted once.' + `Span durations are summed over the course of an entire transaction. Any overlapping spans are only counted once.` )} /> diff --git a/static/app/components/events/autofix/preferences/hooks/useBulkAutofixAutomationSettings.ts b/static/app/components/events/autofix/preferences/hooks/useBulkAutofixAutomationSettings.ts index 54983017d123..95874e90c17f 100644 --- a/static/app/components/events/autofix/preferences/hooks/useBulkAutofixAutomationSettings.ts +++ b/static/app/components/events/autofix/preferences/hooks/useBulkAutofixAutomationSettings.ts @@ -114,7 +114,7 @@ export function useUpdateBulkAutofixAutomationSettings( return fetchMutation({ method: 'POST', url: getApiUrl( - '/organizations/$organizationIdOrSlug/autofix/automation-settings/', + `/organizations/$organizationIdOrSlug/autofix/automation-settings/`, { path: {organizationIdOrSlug: organization.slug}, } @@ -141,7 +141,7 @@ export function useUpdateBulkAutofixAutomationSettings( // Invalidate the query for ProjectOptions to Settings>Project>Seer details page queryClient.invalidateQueries({ queryKey: [ - getApiUrl('/projects/$organizationIdOrSlug/$projectIdOrSlug/', { + getApiUrl(`/projects/$organizationIdOrSlug/$projectIdOrSlug/`, { path: { organizationIdOrSlug: organization.slug, projectIdOrSlug: project.slug, @@ -153,7 +153,7 @@ export function useUpdateBulkAutofixAutomationSettings( queryClient.invalidateQueries({ queryKey: [ getApiUrl( - '/projects/$organizationIdOrSlug/$projectIdOrSlug/seer/preferences/', + `/projects/$organizationIdOrSlug/$projectIdOrSlug/seer/preferences/`, { path: { organizationIdOrSlug: organization.slug, diff --git a/static/app/components/events/autofix/useAutofixSetup.tsx b/static/app/components/events/autofix/useAutofixSetup.tsx index 6fa22f9388ea..9604abff5ebf 100644 --- a/static/app/components/events/autofix/useAutofixSetup.tsx +++ b/static/app/components/events/autofix/useAutofixSetup.tsx @@ -34,7 +34,7 @@ function makeAutofixSetupQueryKey( checkWriteAccess?: boolean ): ApiQueryKey { return [ - getApiUrl('/organizations/$organizationIdOrSlug/issues/$issueId/autofix/setup/', { + getApiUrl(`/organizations/$organizationIdOrSlug/issues/$issueId/autofix/setup/`, { path: {organizationIdOrSlug: orgSlug, issueId: groupId}, }), { diff --git a/static/app/components/events/autofix/v2/artifactCards.tsx b/static/app/components/events/autofix/v2/artifactCards.tsx index a2093b6f582d..dbf2d8aa5ae8 100644 --- a/static/app/components/events/autofix/v2/artifactCards.tsx +++ b/static/app/components/events/autofix/v2/artifactCards.tsx @@ -432,7 +432,7 @@ const TreeKeyTrunk = styled('div')<{spacerCount: number}>` display: grid; height: 100%; align-items: center; - grid-template-columns: ${p => (p.spacerCount > 0 ? 'auto 1rem 1fr' : '1fr')}; + grid-template-columns: ${p => (p.spacerCount > 0 ? `auto 1rem 1fr` : '1fr')}; `; const TreeValue = styled('div')` diff --git a/static/app/components/events/autofix/v3/autofixCards.tsx b/static/app/components/events/autofix/v3/autofixCards.tsx index 1b1f73ba0db5..8e1d8a377018 100644 --- a/static/app/components/events/autofix/v3/autofixCards.tsx +++ b/static/app/components/events/autofix/v3/autofixCards.tsx @@ -280,12 +280,7 @@ export function PullRequestsCard({section}: AutofixCardProps) { ) { return ( - } - > + {t('View %s#%s', pullRequest.repo_name, pullRequest.pr_number)} - ) : isWaitingForCallback ? ( - - ) : ( - - )} - - ); -} - -export const bitbucketIntegrationPipeline = { - type: 'integration', - provider: 'bitbucket', - actionTitle: t('Installing Bitbucket Integration'), - getCompletionData: pipelineComplete, - steps: [ - { - stepId: 'authorize', - shortDescription: t('Authorizing Bitbucket'), - component: BitbucketAuthorizeStep, - }, - ], -} as const satisfies PipelineDefinition; diff --git a/static/app/components/pipeline/pipelineIntegrationGitLab.spec.tsx b/static/app/components/pipeline/pipelineIntegrationGitLab.spec.tsx deleted file mode 100644 index fbf423de03f9..000000000000 --- a/static/app/components/pipeline/pipelineIntegrationGitLab.spec.tsx +++ /dev/null @@ -1,366 +0,0 @@ -import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; - -import {gitlabIntegrationPipeline} from './pipelineIntegrationGitLab'; -import type {PipelineStepProps} from './types'; - -const InstallationConfigStep = gitlabIntegrationPipeline.steps[0].component; -const GitLabOAuthLoginStep = gitlabIntegrationPipeline.steps[1].component; - -function makeStepProps( - overrides: Partial> & {stepData: D} -): PipelineStepProps { - return { - advance: jest.fn(), - advanceError: null, - isAdvancing: false, - stepIndex: 0, - totalSteps: 2, - ...overrides, - }; -} - -let mockPopup: Window; - -function dispatchPipelineMessage({ - data, - origin = document.location.origin, - source = mockPopup, -}: { - data: Record; - origin?: string; - source?: Window | MessageEventSource | null; -}) { - act(() => { - const event = new MessageEvent('message', {data, origin}); - Object.defineProperty(event, 'source', {value: source}); - window.dispatchEvent(event); - }); -} - -beforeEach(() => { - mockPopup = { - closed: false, - close: jest.fn(), - focus: jest.fn(), - } as unknown as Window; - jest.spyOn(window, 'open').mockReturnValue(mockPopup); -}); - -afterEach(() => { - jest.restoreAllMocks(); -}); - -describe('InstallationConfigStep', () => { - it('renders the guided steps and config form', () => { - render( - - ); - - expect( - screen.getByText( - 'To connect Sentry with your GitLab instance, you need to create an OAuth application in GitLab.' - ) - ).toBeInTheDocument(); - - expect(screen.getByText('Open GitLab application settings')).toBeInTheDocument(); - expect(screen.getByText('Create a new application')).toBeInTheDocument(); - expect(screen.getByText('Configure the integration')).toBeInTheDocument(); - }); - - it('renders setup values in the create step', async () => { - render( - - ); - - // Navigate to the create step - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - - expect(screen.getByText('Redirect URI')).toBeInTheDocument(); - expect( - screen.getByText('https://sentry.io/extensions/gitlab/setup/') - ).toBeInTheDocument(); - expect(screen.getByText('Scopes')).toBeInTheDocument(); - expect(screen.getByText('api')).toBeInTheDocument(); - }); - - it('submits config with required fields and calls advance', async () => { - const advance = jest.fn(); - render( - - ); - - // Navigate through guided steps to the configure step - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - - await userEvent.type( - screen.getByRole('textbox', {name: 'GitLab Application ID'}), - 'my-app-id' - ); - await userEvent.type(screen.getByLabelText('GitLab Application Secret'), 'my-secret'); - - await userEvent.click(screen.getByRole('button', {name: 'Continue'})); - - await waitFor(() => { - expect(advance).toHaveBeenCalledWith({ - url: undefined, - verify_ssl: undefined, - group: '', - include_subgroups: undefined, - client_id: 'my-app-id', - client_secret: 'my-secret', - }); - }); - }); - - it('submits with group and include_subgroups when group is set', async () => { - const advance = jest.fn(); - render( - - ); - - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - - await userEvent.type( - screen.getByRole('textbox', {name: 'GitLab Application ID'}), - 'my-app-id' - ); - await userEvent.type(screen.getByLabelText('GitLab Application Secret'), 'my-secret'); - await userEvent.type( - screen.getByRole('textbox', {name: 'GitLab Group Path'}), - 'my-group/sub' - ); - - // Include Subgroups toggle should now be visible - await userEvent.click(screen.getByRole('checkbox', {name: 'Include Subgroups'})); - - await userEvent.click(screen.getByRole('button', {name: 'Continue'})); - - await waitFor(() => { - expect(advance).toHaveBeenCalledWith( - expect.objectContaining({ - group: 'my-group/sub', - include_subgroups: true, - client_id: 'my-app-id', - client_secret: 'my-secret', - }) - ); - }); - }); - - it('does not show include_subgroups toggle when group is empty', async () => { - render(); - - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - - expect( - screen.queryByRole('checkbox', {name: 'Include Subgroups'}) - ).not.toBeInTheDocument(); - }); - - it('shows self-hosted fields when self-hosted toggle is enabled', async () => { - render(); - - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - - // Self-hosted fields should not be visible initially - expect(screen.queryByRole('textbox', {name: 'GitLab URL'})).not.toBeInTheDocument(); - - await userEvent.click(screen.getByRole('checkbox', {name: 'Self-Hosted Instance'})); - - expect(screen.getByRole('textbox', {name: 'GitLab URL'})).toBeInTheDocument(); - expect(screen.getByRole('checkbox', {name: 'Verify SSL'})).toBeInTheDocument(); - }); - - it('submits self-hosted config with URL and verify_ssl', async () => { - const advance = jest.fn(); - render( - - ); - - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - - await userEvent.type( - screen.getByRole('textbox', {name: 'GitLab Application ID'}), - 'my-app-id' - ); - await userEvent.type(screen.getByLabelText('GitLab Application Secret'), 'my-secret'); - - await userEvent.click(screen.getByRole('checkbox', {name: 'Self-Hosted Instance'})); - - await userEvent.type( - screen.getByRole('textbox', {name: 'GitLab URL'}), - 'https://gitlab.example.com/' - ); - - // Verify SSL is on by default, turn it off - await userEvent.click(screen.getByRole('checkbox', {name: 'Verify SSL'})); - - await userEvent.click(screen.getByRole('button', {name: 'Continue'})); - - await waitFor(() => { - expect(advance).toHaveBeenCalledWith( - expect.objectContaining({ - url: 'https://gitlab.example.com', - verify_ssl: false, - client_id: 'my-app-id', - client_secret: 'my-secret', - }) - ); - }); - }); - - it('strips trailing slashes from self-hosted URL', async () => { - const advance = jest.fn(); - render( - - ); - - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - - await userEvent.type( - screen.getByRole('textbox', {name: 'GitLab Application ID'}), - 'id' - ); - await userEvent.type(screen.getByLabelText('GitLab Application Secret'), 'secret'); - - await userEvent.click(screen.getByRole('checkbox', {name: 'Self-Hosted Instance'})); - - await userEvent.type( - screen.getByRole('textbox', {name: 'GitLab URL'}), - 'https://gitlab.example.com///' - ); - - await userEvent.click(screen.getByRole('button', {name: 'Continue'})); - - await waitFor(() => { - expect(advance).toHaveBeenCalledWith( - expect.objectContaining({ - url: 'https://gitlab.example.com', - }) - ); - }); - }); - - it('shows submitting state when isAdvancing is true', async () => { - render( - - ); - - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - await userEvent.click(screen.getByRole('button', {name: 'Next'})); - - expect(screen.getByRole('button', {name: 'Submitting...'})).toBeDisabled(); - }); -}); - -describe('GitLabOAuthLoginStep', () => { - it('renders the OAuth login step for GitLab', () => { - render( - - ); - - expect(screen.getByRole('button', {name: 'Authorize GitLab'})).toBeInTheDocument(); - }); - - it('calls advance with code and state on OAuth callback', async () => { - const advance = jest.fn(); - render( - - ); - - await userEvent.click(screen.getByRole('button', {name: 'Authorize GitLab'})); - - dispatchPipelineMessage({ - data: { - _pipeline_source: 'sentry-pipeline', - code: 'auth-code-123', - state: 'state-xyz', - }, - }); - - expect(advance).toHaveBeenCalledWith({ - code: 'auth-code-123', - state: 'state-xyz', - }); - }); - - it('shows loading state when isAdvancing is true', () => { - render( - - ); - - expect(screen.getByRole('button', {name: 'Authorizing...'})).toBeDisabled(); - }); - - it('disables authorize button when oauthUrl is not provided', () => { - render(); - - expect(screen.getByRole('button', {name: 'Authorize GitLab'})).toBeDisabled(); - }); -}); diff --git a/static/app/components/pipeline/pipelineIntegrationGitLab.tsx b/static/app/components/pipeline/pipelineIntegrationGitLab.tsx deleted file mode 100644 index 2a34cdbf4122..000000000000 --- a/static/app/components/pipeline/pipelineIntegrationGitLab.tsx +++ /dev/null @@ -1,294 +0,0 @@ -import {useCallback} from 'react'; -import {z} from 'zod'; - -import {CodeBlock} from '@sentry/scraps/code'; -import {defaultFormOptions, useScrapsForm} from '@sentry/scraps/form'; -import {Flex, Stack} from '@sentry/scraps/layout'; -import {Text} from '@sentry/scraps/text'; - -import {GuidedSteps} from 'sentry/components/guidedSteps/guidedSteps'; -import {t, tct} from 'sentry/locale'; -import type {IntegrationWithConfig} from 'sentry/types/integrations'; - -import type {OAuthCallbackData} from './shared/oauthLoginStep'; -import {OAuthLoginStep} from './shared/oauthLoginStep'; -import type {PipelineDefinition, PipelineStepProps} from './types'; -import {pipelineComplete} from './types'; - -const installationConfigSchema = z - .object({ - selfHosted: z.boolean(), - url: z.string(), - group: z.string(), - includeSubgroups: z.boolean(), - verifySsl: z.boolean(), - clientId: z.string().min(1, t('Application ID is required')), - clientSecret: z.string().min(1, t('Application Secret is required')), - }) - .refine(data => !data.selfHosted || z.httpUrl().safeParse(data.url).success, { - path: ['url'], - message: t('A valid GitLab URL is required for self-hosted instances'), - }); - -interface InstallationConfigStepData { - defaults?: { - includeSubgroups?: boolean; - verifySsl?: boolean; - }; - setupValues?: Array<{label: string; value: string}>; -} - -interface InstallationConfigAdvanceData { - client_id: string; - client_secret: string; - group: string; - include_subgroups?: boolean; - url?: string; - verify_ssl?: boolean; -} - -function InstallationConfigStep({ - stepData, - advance, - isAdvancing, -}: PipelineStepProps) { - const defaults = stepData.defaults ?? {}; - const setupValues = stepData.setupValues ?? []; - - const form = useScrapsForm({ - ...defaultFormOptions, - defaultValues: { - selfHosted: false, - url: '', - group: '', - includeSubgroups: defaults.includeSubgroups ?? false, - verifySsl: defaults.verifySsl ?? true, - clientId: '', - clientSecret: '', - }, - validators: {onDynamic: installationConfigSchema}, - onSubmit: ({value}) => { - advance({ - url: value.selfHosted ? value.url.replace(/\/+$/, '') : undefined, - verify_ssl: value.selfHosted ? value.verifySsl : undefined, - group: value.group, - include_subgroups: value.group ? value.includeSubgroups : undefined, - client_id: value.clientId, - client_secret: value.clientSecret, - }); - }, - }); - - const configForm = ( - - - - {field => ( - - - - )} - - - {field => ( - - - - )} - - - {field => ( - - - - )} - - state.values.group}> - {group => - group ? ( - - {field => ( - - - - )} - - ) : null - } - - - {field => ( - - - - )} - - state.values.selfHosted}> - {isSelfHosted => - isSelfHosted ? ( - - - {field => ( - - - - )} - - - {field => ( - - - - )} - - - ) : null - } - - - - {isAdvancing ? t('Submitting...') : t('Continue')} - - - - - ); - - return ( - - - {t( - 'To connect Sentry with your GitLab instance, you need to create an OAuth application in GitLab.' - )} - - - - - - {tct( - 'Navigate to [bold:User Settings \u203A Access \u203A Applications] in GitLab.', - { - bold: , - } - )} - - - {tct( - 'For self-managed instances, use [bold:Admin Area \u203A Applications] instead.', - {bold: } - )} - - - - - - - {setupValues.map(({label, value}) => ( - - - {label} - - {value} - - ))} - - - - - {configForm} - - - - ); -} - -function GitLabOAuthLoginStep({ - stepData, - advance, - isAdvancing, -}: PipelineStepProps<{oauthUrl?: string}, {code: string; state: string}>) { - const handleOAuthCallback = useCallback( - (data: OAuthCallbackData) => { - advance({code: data.code, state: data.state}); - }, - [advance] - ); - - return ( - - ); -} - -export const gitlabIntegrationPipeline = { - type: 'integration', - provider: 'gitlab', - actionTitle: t('Installing GitLab Integration'), - getCompletionData: pipelineComplete, - steps: [ - { - stepId: 'installation_config', - shortDescription: t('Configuring GitLab connection'), - component: InstallationConfigStep, - }, - { - stepId: 'oauth_login', - shortDescription: t('Authorizing via GitLab OAuth flow'), - component: GitLabOAuthLoginStep, - }, - ], -} as const satisfies PipelineDefinition; diff --git a/static/app/components/pipeline/pipelineIntegrationSlack.spec.tsx b/static/app/components/pipeline/pipelineIntegrationSlack.spec.tsx deleted file mode 100644 index 740343d5d25a..000000000000 --- a/static/app/components/pipeline/pipelineIntegrationSlack.spec.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import {act, render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; - -import {slackIntegrationPipeline} from './pipelineIntegrationSlack'; -import type {PipelineStepProps} from './types'; - -const SlackOAuthLoginStep = slackIntegrationPipeline.steps[0].component; - -function makeStepProps( - overrides: Partial> & {stepData: D} -): PipelineStepProps { - return { - advance: jest.fn(), - advanceError: null, - isAdvancing: false, - stepIndex: 0, - totalSteps: 1, - ...overrides, - }; -} - -let mockPopup: Window; - -function dispatchPipelineMessage({ - data, - origin = document.location.origin, - source = mockPopup, -}: { - data: Record; - origin?: string; - source?: Window | MessageEventSource | null; -}) { - act(() => { - const event = new MessageEvent('message', {data, origin}); - Object.defineProperty(event, 'source', {value: source}); - window.dispatchEvent(event); - }); -} - -beforeEach(() => { - mockPopup = { - closed: false, - close: jest.fn(), - focus: jest.fn(), - } as unknown as Window; - jest.spyOn(window, 'open').mockReturnValue(mockPopup); -}); - -afterEach(() => { - jest.restoreAllMocks(); -}); - -describe('SlackOAuthLoginStep', () => { - it('renders the OAuth login step for Slack', () => { - render( - - ); - - expect(screen.getByRole('button', {name: 'Authorize Slack'})).toBeInTheDocument(); - }); - - it('calls advance with code and state on OAuth callback', async () => { - const advance = jest.fn(); - render( - - ); - - await userEvent.click(screen.getByRole('button', {name: 'Authorize Slack'})); - - dispatchPipelineMessage({ - data: { - _pipeline_source: 'sentry-pipeline', - code: 'auth-code-123', - state: 'state-xyz', - }, - }); - - expect(advance).toHaveBeenCalledWith({ - code: 'auth-code-123', - state: 'state-xyz', - }); - }); - - it('shows loading state when isAdvancing is true', () => { - render( - - ); - - expect(screen.getByRole('button', {name: 'Authorizing...'})).toBeDisabled(); - }); - - it('disables authorize button when oauthUrl is not provided', () => { - render(); - - expect(screen.getByRole('button', {name: 'Authorize Slack'})).toBeDisabled(); - }); -}); diff --git a/static/app/components/pipeline/pipelineIntegrationSlack.tsx b/static/app/components/pipeline/pipelineIntegrationSlack.tsx deleted file mode 100644 index ecd4433cb99e..000000000000 --- a/static/app/components/pipeline/pipelineIntegrationSlack.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import {useCallback} from 'react'; - -import {t} from 'sentry/locale'; -import type {IntegrationWithConfig} from 'sentry/types/integrations'; - -import type {OAuthCallbackData} from './shared/oauthLoginStep'; -import {OAuthLoginStep} from './shared/oauthLoginStep'; -import type {PipelineDefinition, PipelineStepProps} from './types'; -import {pipelineComplete} from './types'; - -function SlackOAuthLoginStep({ - stepData, - advance, - isAdvancing, -}: PipelineStepProps<{oauthUrl?: string}, {code: string; state: string}>) { - const handleOAuthCallback = useCallback( - (data: OAuthCallbackData) => { - advance({code: data.code, state: data.state}); - }, - [advance] - ); - - return ( - - ); -} - -export const slackIntegrationPipeline = { - type: 'integration', - provider: 'slack', - actionTitle: t('Installing Slack Integration'), - getCompletionData: pipelineComplete, - steps: [ - { - stepId: 'oauth_login', - shortDescription: t('Authorizing via Slack OAuth'), - component: SlackOAuthLoginStep, - }, - ], -} as const satisfies PipelineDefinition; diff --git a/static/app/components/pipeline/registry.tsx b/static/app/components/pipeline/registry.tsx index d15ce8325232..c88fad5a4016 100644 --- a/static/app/components/pipeline/registry.tsx +++ b/static/app/components/pipeline/registry.tsx @@ -1,18 +1,12 @@ import {dummyIntegrationPipeline} from './pipelineDummyProvider'; -import {bitbucketIntegrationPipeline} from './pipelineIntegrationBitbucket'; import {githubIntegrationPipeline} from './pipelineIntegrationGitHub'; -import {gitlabIntegrationPipeline} from './pipelineIntegrationGitLab'; -import {slackIntegrationPipeline} from './pipelineIntegrationSlack'; /** * All registered pipeline definitions. */ export const PIPELINE_REGISTRY = [ - bitbucketIntegrationPipeline, dummyIntegrationPipeline, githubIntegrationPipeline, - gitlabIntegrationPipeline, - slackIntegrationPipeline, ] as const; type AllPipelines = (typeof PIPELINE_REGISTRY)[number]; diff --git a/static/app/components/platformPicker.spec.tsx b/static/app/components/platformPicker.spec.tsx index 54ea74373620..4d197802b355 100644 --- a/static/app/components/platformPicker.spec.tsx +++ b/static/app/components/platformPicker.spec.tsx @@ -130,6 +130,6 @@ describe('PlatformPicker', () => { await userEvent.type(screen.getByPlaceholderText('Filter Platforms'), 'play'); - expect(screen.getByTestId('platform-playstation')).toBeInTheDocument(); + expect(screen.getByTestId(`platform-playstation`)).toBeInTheDocument(); }); }); diff --git a/static/app/components/platformPicker.tsx b/static/app/components/platformPicker.tsx index cc4f6d7988ff..eb232b95213c 100644 --- a/static/app/components/platformPicker.tsx +++ b/static/app/components/platformPicker.tsx @@ -266,7 +266,7 @@ export function PlatformPicker({ title={t("We don't have an SDK for that yet!")} > {tct( - "Sure you haven't misspelled? If you're using a lesser-known platform, consider choosing a more generic SDK like Browser JavaScript, Python, Node, .NET & Java or create a generic project, by selecting [linkOther:“Other”].", + `Sure you haven't misspelled? If you're using a lesser-known platform, consider choosing a more generic SDK like Browser JavaScript, Python, Node, .NET & Java or create a generic project, by selecting [linkOther:“Other”].`, { linkOther: ( - - + + {tct( + 'Alerts configured for all Issues in [projectName] will also apply to this Monitor.', + { + projectName: {project.slug}, + } + )} + + } /> diff --git a/static/app/views/detectors/components/forms/common/projectField.tsx b/static/app/views/detectors/components/forms/common/projectField.tsx index 8d401a4f3bdc..23b24d7dd832 100644 --- a/static/app/views/detectors/components/forms/common/projectField.tsx +++ b/static/app/views/detectors/components/forms/common/projectField.tsx @@ -3,12 +3,14 @@ import styled from '@emotion/styled'; import {SentryProjectSelectorField} from 'sentry/components/forms/fields/sentryProjectSelectorField'; import {t} from 'sentry/locale'; import {useProjects} from 'sentry/utils/useProjects'; +import {useDetectorFormProject} from 'sentry/views/detectors/components/forms/common/useDetectorFormProject'; import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context'; import {useCanEditDetector} from 'sentry/views/detectors/utils/useCanEditDetector'; export function ProjectField() { const {projects, fetching} = useProjects(); - const {project, detectorType} = useDetectorFormContext(); + const {detectorType} = useDetectorFormContext(); + const project = useDetectorFormProject(); const canEditDetector = useCanEditDetector({projectId: project.id, detectorType}); return ( diff --git a/static/app/views/detectors/components/forms/common/useDetectorFormProject.tsx b/static/app/views/detectors/components/forms/common/useDetectorFormProject.tsx new file mode 100644 index 000000000000..b38d576277ab --- /dev/null +++ b/static/app/views/detectors/components/forms/common/useDetectorFormProject.tsx @@ -0,0 +1,22 @@ +import {useMemo} from 'react'; + +import {useFormField} from 'sentry/components/workflowEngine/form/useFormField'; +import type {Project} from 'sentry/types/project'; +import {useProjects} from 'sentry/utils/useProjects'; + +export function useDetectorFormProject(): Project { + const projectId = useFormField('projectId'); + const {projects} = useProjects(); + const project = useMemo( + () => projects.find(p => p.id === projectId), + [projects, projectId] + ); + // There's a top-level spinner when projects are loading, and you can't select a + // project that's not in the ProjectStore, so this should never happen. + if (!project) { + throw new Error( + `useDetectorFormProject: no project found for projectId "${projectId}"` + ); + } + return project; +} diff --git a/static/app/views/detectors/components/forms/common/useSetAutomaticName.spec.tsx b/static/app/views/detectors/components/forms/common/useSetAutomaticName.spec.tsx index 4fe59334e85e..b1982f75cd6a 100644 --- a/static/app/views/detectors/components/forms/common/useSetAutomaticName.spec.tsx +++ b/static/app/views/detectors/components/forms/common/useSetAutomaticName.spec.tsx @@ -34,7 +34,7 @@ describe('useSetAutomaticName', () => { const renderDetectorForm = (detector?: Detector, initialFormData = {}) => { return render( - + data as any} diff --git a/static/app/views/detectors/components/forms/context.tsx b/static/app/views/detectors/components/forms/context.tsx index de83ccab0c5a..ae5ab92e4d23 100644 --- a/static/app/views/detectors/components/forms/context.tsx +++ b/static/app/views/detectors/components/forms/context.tsx @@ -1,6 +1,5 @@ import {createContext, useContext, useState} from 'react'; -import type {Project} from 'sentry/types/project'; import type {Detector, DetectorType} from 'sentry/types/workflowEngine/detectors'; type DetectorFormContextType = { @@ -10,7 +9,6 @@ type DetectorFormContextType = { * Used by useSetAutomaticName to disable automatic name generation. */ hasSetDetectorName: boolean; - project: Project; setHasSetDetectorName: (value: boolean) => void; detector?: Detector; }; @@ -19,20 +17,18 @@ const DetectorFormContext = createContext(null); export function DetectorFormProvider({ detectorType, - project, detector, children, }: { children: React.ReactNode; detectorType: DetectorType; - project: Project; detector?: Detector; }) { const [hasSetDetectorName, setHasSetDetectorName] = useState(false); return ( {children} diff --git a/static/app/views/detectors/components/forms/cron/cronIssuePreview.tsx b/static/app/views/detectors/components/forms/cron/cronIssuePreview.tsx index 6269b074e045..9952ebc73c56 100644 --- a/static/app/views/detectors/components/forms/cron/cronIssuePreview.tsx +++ b/static/app/views/detectors/components/forms/cron/cronIssuePreview.tsx @@ -2,7 +2,7 @@ import {t} from 'sentry/locale'; import {DetectorIssuePreview} from 'sentry/views/detectors/components/forms/common/detectorIssuePreview'; import {IssuePreviewSection} from 'sentry/views/detectors/components/forms/common/issuePreviewSection'; import {ownerToActor} from 'sentry/views/detectors/components/forms/common/ownerToActor'; -import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context'; +import {useDetectorFormProject} from 'sentry/views/detectors/components/forms/common/useDetectorFormProject'; import {useCronDetectorFormField} from 'sentry/views/detectors/components/forms/cron/fields'; const FALLBACK_ISSUE_TITLE = t('Cron failure: …'); @@ -22,7 +22,7 @@ export function CronIssuePreview({step}: {step?: number}) { const owner = useCronDetectorFormField('owner'); const issueTitle = useCronIssueTitle(); const assignee = ownerToActor(owner); - const {project} = useDetectorFormContext(); + const project = useDetectorFormProject(); return ( diff --git a/static/app/views/detectors/components/forms/cron/index.spec.tsx b/static/app/views/detectors/components/forms/cron/index.spec.tsx index 8ccf62848d5b..a4cba9dc59dd 100644 --- a/static/app/views/detectors/components/forms/cron/index.spec.tsx +++ b/static/app/views/detectors/components/forms/cron/index.spec.tsx @@ -43,7 +43,7 @@ describe('NewCronDetectorForm', () => { const renderForm = (routerConfig?: any) => { return render( - + , { diff --git a/static/app/views/detectors/components/forms/error/index.tsx b/static/app/views/detectors/components/forms/error/index.tsx index 4888b9082caa..720d0c0b9535 100644 --- a/static/app/views/detectors/components/forms/error/index.tsx +++ b/static/app/views/detectors/components/forms/error/index.tsx @@ -141,6 +141,7 @@ export function EditExistingErrorDetectorForm({detector}: {detector: ErrorDetect { MockApiClient.clearMockResponses(); OrganizationStore.reset(); OrganizationStore.onUpdate(organization); + ProjectsStore.loadInitialData([project]); MemberListStore.init(); MemberListStore.loadInitialData([UserFixture()]); @@ -80,7 +82,7 @@ describe('NewMetricDetectorForm', () => { it('shows default issue preview and updates subtitle when threshold changes', async () => { render( - + , {organization} @@ -143,7 +145,7 @@ describe('NewMetricDetectorForm', () => { it('removes is filters when switching away from the errors dataset', async () => { render( - + , { diff --git a/static/app/views/detectors/components/forms/metric/metric.tsx b/static/app/views/detectors/components/forms/metric/metric.tsx index 626c59191095..c163300632b9 100644 --- a/static/app/views/detectors/components/forms/metric/metric.tsx +++ b/static/app/views/detectors/components/forms/metric/metric.tsx @@ -710,7 +710,7 @@ const DatasetRow = styled('div')` `; const FilterRow = styled('div')<{disabled: boolean}>` - ${p => (p.disabled ? 'opacity: 0.6;' : '')} + ${p => (p.disabled ? `opacity: 0.6;` : '')} `; const StyledSelectField = styled(SelectField)` @@ -794,7 +794,7 @@ const IntervalField = styled(SelectField)` `; const DisabledSection = styled('div')<{disabled: boolean}>` - ${p => (p.disabled ? 'opacity: 0.6;' : '')} + ${p => (p.disabled ? `opacity: 0.6;` : '')} `; const PriorityLabel = styled('span')` diff --git a/static/app/views/detectors/components/forms/metric/metricIssuePreview.tsx b/static/app/views/detectors/components/forms/metric/metricIssuePreview.tsx index f7ca68d5cbc2..d6e331a4ea8c 100644 --- a/static/app/views/detectors/components/forms/metric/metricIssuePreview.tsx +++ b/static/app/views/detectors/components/forms/metric/metricIssuePreview.tsx @@ -4,7 +4,7 @@ import {getDuration} from 'sentry/utils/duration/getDuration'; import {DetectorIssuePreview} from 'sentry/views/detectors/components/forms/common/detectorIssuePreview'; import {IssuePreviewSection} from 'sentry/views/detectors/components/forms/common/issuePreviewSection'; import {ownerToActor} from 'sentry/views/detectors/components/forms/common/ownerToActor'; -import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context'; +import {useDetectorFormProject} from 'sentry/views/detectors/components/forms/common/useDetectorFormProject'; import { METRIC_DETECTOR_FORM_FIELDS, useMetricDetectorFormField, @@ -81,7 +81,7 @@ export function MetricIssuePreview({step}: {step?: number}) { const owner = useMetricDetectorFormField(METRIC_DETECTOR_FORM_FIELDS.owner); const subtitle = useMetricIssuePreviewSubtitle(); const assignee = ownerToActor(owner); - const {project} = useDetectorFormContext(); + const project = useDetectorFormProject(); return ( diff --git a/static/app/views/detectors/components/forms/newDetectorLayout.tsx b/static/app/views/detectors/components/forms/newDetectorLayout.tsx index d3d685acd795..cb9c115cbfb1 100644 --- a/static/app/views/detectors/components/forms/newDetectorLayout.tsx +++ b/static/app/views/detectors/components/forms/newDetectorLayout.tsx @@ -1,5 +1,6 @@ import {useMemo, useState} from 'react'; import {useTheme} from '@emotion/react'; +import orderBy from 'lodash/orderBy'; import type {FormProps} from 'sentry/components/forms/form'; import {FormModel} from 'sentry/components/forms/model'; @@ -11,10 +12,10 @@ import type { DetectorType, } from 'sentry/types/workflowEngine/detectors'; import {useLocation} from 'sentry/utils/useLocation'; +import {useProjects} from 'sentry/utils/useProjects'; import {NewDetectorBreadcrumbs} from 'sentry/views/detectors/components/forms/common/breadcrumbs'; import {DetectorNameField} from 'sentry/views/detectors/components/forms/common/detectorNameField'; import {NewDetectorFooter} from 'sentry/views/detectors/components/forms/common/footer'; -import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context'; import {MonitorFeedbackButton} from 'sentry/views/detectors/components/monitorFeedbackButton'; import {useCreateDetectorFormSubmit} from 'sentry/views/detectors/hooks/useCreateDetectorFormSubmit'; @@ -45,7 +46,19 @@ export function NewDetectorLayout< const location = useLocation(); const theme = useTheme(); const maxWidth = theme.breakpoints.xl; - const formContext = useDetectorFormContext(); + const {projects} = useProjects(); + + const initialProjectId = useMemo(() => { + const queryProjectId = location.query.project as string | undefined; + if (queryProjectId) { + const match = projects.find(p => p.id === queryProjectId); + if (match) { + return match.id; + } + } + const sorted = orderBy(projects, ['isMember', 'isBookmarked'], ['desc', 'desc']); + return sorted[0]?.id ?? ''; + }, [location.query.project, projects]); const formSubmitHandler = useCreateDetectorFormSubmit({ detectorType, @@ -57,7 +70,7 @@ export function NewDetectorLayout< const initialData = useMemo(() => { return { - projectId: formContext.project.id, + projectId: initialProjectId, environment: (location.query.environment as string | undefined) || '', name: (location.query.name as string | undefined) || '', owner: (location.query.owner as string | undefined) || '', @@ -65,7 +78,7 @@ export function NewDetectorLayout< ...initialFormData, }; }, [ - formContext.project.id, + initialProjectId, initialFormData, location.query.environment, location.query.name, diff --git a/static/app/views/detectors/components/forms/uptime/uptimeIssuePreview.tsx b/static/app/views/detectors/components/forms/uptime/uptimeIssuePreview.tsx index 414aaabeccee..a387b1e07bfa 100644 --- a/static/app/views/detectors/components/forms/uptime/uptimeIssuePreview.tsx +++ b/static/app/views/detectors/components/forms/uptime/uptimeIssuePreview.tsx @@ -2,7 +2,7 @@ import {t} from 'sentry/locale'; import {DetectorIssuePreview} from 'sentry/views/detectors/components/forms/common/detectorIssuePreview'; import {IssuePreviewSection} from 'sentry/views/detectors/components/forms/common/issuePreviewSection'; import {ownerToActor} from 'sentry/views/detectors/components/forms/common/ownerToActor'; -import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context'; +import {useDetectorFormProject} from 'sentry/views/detectors/components/forms/common/useDetectorFormProject'; import {useUptimeDetectorFormField} from 'sentry/views/detectors/components/forms/uptime/fields'; import {formatUptimeUrl} from 'sentry/views/detectors/components/forms/uptime/formatUptimeUrl'; @@ -28,7 +28,7 @@ export function UptimeIssuePreview({step}: {step?: number}) { const owner = useUptimeDetectorFormField('owner'); const issueTitle = useUptimeIssueTitle(); const assignee = ownerToActor(owner); - const {project} = useDetectorFormContext(); + const project = useDetectorFormProject(); return ( diff --git a/static/app/views/detectors/detail.spec.tsx b/static/app/views/detectors/detail.spec.tsx index a25fe4203239..f45895db0365 100644 --- a/static/app/views/detectors/detail.spec.tsx +++ b/static/app/views/detectors/detail.spec.tsx @@ -97,11 +97,11 @@ describe('DetectorDetails', () => { body: [], }); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/issues/1/', + url: `/organizations/org-slug/issues/1/`, body: GroupFixture(), }); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/detectors/', + url: `/organizations/org-slug/detectors/`, body: [issueStreamDetector], match: [ MockApiClient.matchQuery({ @@ -111,7 +111,7 @@ describe('DetectorDetails', () => { ], }); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/workflows/', + url: `/organizations/org-slug/workflows/`, body: [], match: [(_url, options) => options.query?.detector === undefined], }); diff --git a/static/app/views/detectors/edit.tsx b/static/app/views/detectors/edit.tsx index e3b470629360..663698fadbb2 100644 --- a/static/app/views/detectors/edit.tsx +++ b/static/app/views/detectors/edit.tsx @@ -10,6 +10,7 @@ import {useDetectorQuery} from 'sentry/views/detectors/hooks'; export default function DetectorEdit() { const params = useParams<{detectorId: string}>(); + const {fetching: isFetchingProjects} = useProjects(); useWorkflowEngineFeatureGate({redirect: true}); const { @@ -20,9 +21,6 @@ export default function DetectorEdit() { refetch, } = useDetectorQuery(params.detectorId); - const {projects, fetching: isFetchingProjects} = useProjects(); - const project = projects.find(p => p.id === detector?.projectId); - if (isPending || isFetchingProjects) { return ; } @@ -36,16 +34,8 @@ export default function DetectorEdit() { ); } - if (!project) { - return ; - } - return ( - + ); diff --git a/static/app/views/detectors/hooks/useMonitorsScheduleSampleBuckets.tsx b/static/app/views/detectors/hooks/useMonitorsScheduleSampleBuckets.tsx index ec2db7074d37..4714a1977fd7 100644 --- a/static/app/views/detectors/hooks/useMonitorsScheduleSampleBuckets.tsx +++ b/static/app/views/detectors/hooks/useMonitorsScheduleSampleBuckets.tsx @@ -58,7 +58,7 @@ export function useMonitorsScheduleSampleBuckets({ return useApiQuery>>( [ - getApiUrl('/organizations/$organizationIdOrSlug/monitors-schedule-buckets/', { + getApiUrl(`/organizations/$organizationIdOrSlug/monitors-schedule-buckets/`, { path: {organizationIdOrSlug: organization.slug}, }), {query}, diff --git a/static/app/views/detectors/hooks/useMonitorsScheduleSampleWindow.tsx b/static/app/views/detectors/hooks/useMonitorsScheduleSampleWindow.tsx index 90d4bc7ed786..f241ca2bb197 100644 --- a/static/app/views/detectors/hooks/useMonitorsScheduleSampleWindow.tsx +++ b/static/app/views/detectors/hooks/useMonitorsScheduleSampleWindow.tsx @@ -34,7 +34,7 @@ export function useMonitorsScheduleSampleWindow({ return useApiQuery( [ - getApiUrl('/organizations/$organizationIdOrSlug/monitors-schedule-window/', { + getApiUrl(`/organizations/$organizationIdOrSlug/monitors-schedule-window/`, { path: {organizationIdOrSlug: organization.slug}, }), {query}, diff --git a/static/app/views/detectors/new-settings.tsx b/static/app/views/detectors/new-settings.tsx index e05ad2dc1df0..9f9356c6fe74 100644 --- a/static/app/views/detectors/new-settings.tsx +++ b/static/app/views/detectors/new-settings.tsx @@ -1,9 +1,3 @@ -import orderBy from 'lodash/orderBy'; -import {parseAsString, useQueryState} from 'nuqs'; - -import {Stack} from '@sentry/scraps/layout'; - -import * as Layout from 'sentry/components/layouts/thirds'; import {LoadingError} from 'sentry/components/loadingError'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle'; @@ -19,41 +13,20 @@ import { } from 'sentry/views/detectors/utils/detectorTypeConfig'; export default function DetectorNewSettings() { - const {projects, fetching: isFetchingProjects} = useProjects(); + const {fetching: isFetchingProjects} = useProjects(); const [detectorType] = useDetectorTypeQueryState(); - const [projectId] = useQueryState('project', parseAsString); useWorkflowEngineFeatureGate({redirect: true}); - if (!detectorType || !isValidDetectorType(detectorType)) { - return ; - } - if (isFetchingProjects) { - return ( - - - - - - - - ); + return ; } - const sortedProjects = orderBy( - projects, - ['isMember', 'isBookmarked'], - ['desc', 'desc'] - ); - const project = - (projectId ? projects.find(p => p.id === projectId) : null) ?? sortedProjects[0]; - - if (!project) { - return ; + if (!detectorType || !isValidDetectorType(detectorType)) { + return ; } return ( - + diff --git a/static/app/views/detectors/new.spec.tsx b/static/app/views/detectors/new.spec.tsx index 61750dfa3345..ac20cba7e5a8 100644 --- a/static/app/views/detectors/new.spec.tsx +++ b/static/app/views/detectors/new.spec.tsx @@ -38,7 +38,7 @@ describe('DetectorNew', () => { expect(router.location).toEqual( expect.objectContaining({ - pathname: '/organizations/org-slug/monitors/new/settings/', + pathname: `/organizations/org-slug/monitors/new/settings/`, query: expect.objectContaining({ detectorType: 'uptime_domain_failure', }), @@ -66,7 +66,7 @@ describe('DetectorNew', () => { expect(router.location).toEqual( expect.objectContaining({ - pathname: '/organizations/org-slug/monitors/new/settings/', + pathname: `/organizations/org-slug/monitors/new/settings/`, query: { detectorType: 'uptime_domain_failure', project: '2', diff --git a/static/app/views/discover/breadcrumb.tsx b/static/app/views/discover/breadcrumb.tsx index 821fdd52709e..bf243b895f1f 100644 --- a/static/app/views/discover/breadcrumb.tsx +++ b/static/app/views/discover/breadcrumb.tsx @@ -50,7 +50,7 @@ export function DiscoverBreadcrumb({ if (defined(eventView.id)) { crumbs.push({ to: makeDiscoverPathname({ - path: '/queries/', + path: `/queries/`, organization, }), label: t('Saved Queries'), diff --git a/static/app/views/discover/homepage.spec.tsx b/static/app/views/discover/homepage.spec.tsx index f54f9d04c58e..85c9609614c1 100644 --- a/static/app/views/discover/homepage.spec.tsx +++ b/static/app/views/discover/homepage.spec.tsx @@ -98,7 +98,7 @@ describe('Discover > Homepage', () => { location: { pathname: `/organizations/${organization.slug}/explore/discover/homepage/`, }, - route: '/organizations/:orgId/explore/discover/homepage/', + route: `/organizations/:orgId/explore/discover/homepage/`, }, organization, }); @@ -123,7 +123,7 @@ describe('Discover > Homepage', () => { field: ['project'], }, }, - route: '/organizations/:orgId/explore/discover/homepage/', + route: `/organizations/:orgId/explore/discover/homepage/`, }, organization, }); @@ -141,7 +141,7 @@ describe('Discover > Homepage', () => { location: { pathname: `/organizations/${organization.slug}/explore/discover/homepage/`, }, - route: '/organizations/:orgId/explore/discover/homepage/', + route: `/organizations/:orgId/explore/discover/homepage/`, }, organization, }); @@ -173,7 +173,7 @@ describe('Discover > Homepage', () => { location: { pathname: `/organizations/${organization.slug}/explore/discover/homepage/`, }, - route: '/organizations/:orgId/explore/discover/homepage/', + route: `/organizations/:orgId/explore/discover/homepage/`, }, organization, }); @@ -220,7 +220,7 @@ describe('Discover > Homepage', () => { location: { pathname: `/organizations/${organization.slug}/explore/discover/homepage/`, }, - route: '/organizations/:orgId/explore/discover/homepage/', + route: `/organizations/:orgId/explore/discover/homepage/`, }, organization, }); @@ -245,7 +245,7 @@ describe('Discover > Homepage', () => { field: ['title'], }, }, - route: '/organizations/:orgId/explore/discover/homepage/', + route: `/organizations/:orgId/explore/discover/homepage/`, }, organization, }); @@ -273,7 +273,7 @@ describe('Discover > Homepage', () => { field: ['title'], }, }, - route: '/organizations/:orgId/explore/discover/homepage/', + route: `/organizations/:orgId/explore/discover/homepage/`, }, organization, }); @@ -302,7 +302,7 @@ describe('Discover > Homepage', () => { field: ['title'], }, }, - route: '/organizations/:orgId/explore/discover/homepage/', + route: `/organizations/:orgId/explore/discover/homepage/`, }, organization, }); @@ -336,7 +336,7 @@ describe('Discover > Homepage', () => { field: ['title'], }, }, - route: '/organizations/:orgId/explore/discover/homepage/', + route: `/organizations/:orgId/explore/discover/homepage/`, }, organization, }); @@ -385,7 +385,7 @@ describe('Discover > Homepage', () => { location: { pathname: `/organizations/${organization.slug}/explore/discover/homepage/`, }, - route: '/organizations/:orgId/explore/discover/homepage/', + route: `/organizations/:orgId/explore/discover/homepage/`, }, organization, }); @@ -412,7 +412,7 @@ describe('Discover > Homepage', () => { field: ['title'], }, }, - route: '/organizations/:orgId/explore/discover/homepage/', + route: `/organizations/:orgId/explore/discover/homepage/`, }, organization, }); @@ -467,7 +467,7 @@ describe('Discover > Homepage', () => { location: { pathname: `/organizations/${organization.slug}/explore/discover/homepage/`, }, - route: '/organizations/:orgId/explore/discover/homepage/', + route: `/organizations/:orgId/explore/discover/homepage/`, }, organization, }); diff --git a/static/app/views/discover/landing.tsx b/static/app/views/discover/landing.tsx index b216603d865c..2e79098e6474 100644 --- a/static/app/views/discover/landing.tsx +++ b/static/app/views/discover/landing.tsx @@ -152,7 +152,7 @@ function DiscoverLanding() { const savedQueriesPageLinks = savedQueriesResponse?.headers.Link; const to = makeDiscoverPathname({ - path: '/homepage/', + path: `/homepage/`, organization, }); diff --git a/static/app/views/discover/results.spec.tsx b/static/app/views/discover/results.spec.tsx index ef1a9ce62a3b..b3bc4effbd38 100644 --- a/static/app/views/discover/results.spec.tsx +++ b/static/app/views/discover/results.spec.tsx @@ -252,7 +252,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {query: 'tag:value'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -289,7 +289,7 @@ describe('Results', () => { cursor: '0%3A50%3A0', }, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -346,7 +346,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {...generateFields(), yAxis: 'count()'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -373,7 +373,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {...generateFields(), display: 'default', yAxis: 'count'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -400,7 +400,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {...generateFields(), display: 'previous'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -426,7 +426,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {...generateFields(), statsPeriod: '60d', project: '-1'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -456,7 +456,7 @@ describe('Results', () => { project: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map(String), }, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -482,7 +482,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {...generateFields(), statsPeriod: '30d', project: '-1'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -512,7 +512,7 @@ describe('Results', () => { project: [1, 2, 3, 4].map(String), }, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -539,7 +539,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {id: '1', statsPeriod: '24h'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -594,7 +594,7 @@ describe('Results', () => { environment: ['production'], }, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -626,7 +626,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {...generateFields(), yAxis: 'count()'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -686,7 +686,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {...generateFields(), display: 'default', yAxis: 'count()'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -747,7 +747,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {...generateFields(), display: 'default', yAxis: 'count()'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -808,7 +808,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {...generateFields(), display: 'default', yAxis: 'count'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -862,7 +862,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {...generateFields(), display: 'default', yAxis: 'count'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -898,7 +898,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {...generateFields(), yAxis: 'count()'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -923,7 +923,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {fromMetric: 'true', id: '1'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -950,7 +950,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {showUnparameterizedBanner: 'true', id: '1'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -981,7 +981,7 @@ describe('Results', () => { // These fields take priority and should be sent in the request query: {field: ['title', 'user'], id: '1'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -1040,7 +1040,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {id: '1'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -1090,7 +1090,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: initialQuery as Record, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -1132,7 +1132,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {id: '1'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -1163,7 +1163,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {id: '1'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -1199,7 +1199,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: initialQuery as Record, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -1230,7 +1230,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: initialQuery as Record, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -1281,7 +1281,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {id: '1'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -1391,7 +1391,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {id: '1'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); @@ -1500,7 +1500,7 @@ describe('Results', () => { pathname: `/organizations/${organization.slug}/explore/discover/results/`, query: {id: '1'}, }, - route: '/organizations/:orgId/explore/discover/results/', + route: `/organizations/:orgId/explore/discover/results/`, }, organization, }); diff --git a/static/app/views/discover/results/resultsSearchQueryBuilder.spec.tsx b/static/app/views/discover/results/resultsSearchQueryBuilder.spec.tsx index 474d5d28e8c0..d0b43b657998 100644 --- a/static/app/views/discover/results/resultsSearchQueryBuilder.spec.tsx +++ b/static/app/views/discover/results/resultsSearchQueryBuilder.spec.tsx @@ -13,16 +13,16 @@ describe('ResultsSearchQueryBuilder', () => { beforeEach(() => { organization = OrganizationFixture(); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/recent-searches/', + url: `/organizations/org-slug/recent-searches/`, body: [], }); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/recent-searches/', + url: `/organizations/org-slug/recent-searches/`, body: [], method: 'POST', }); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/tags/', + url: `/organizations/org-slug/tags/`, body: [{key: 'transaction', name: 'transaction', kind: FieldKind.FIELD}], }); }); diff --git a/static/app/views/discover/table/columnEditCollection.tsx b/static/app/views/discover/table/columnEditCollection.tsx index d1184e36434e..124a6168616e 100644 --- a/static/app/views/discover/table/columnEditCollection.tsx +++ b/static/app/views/discover/table/columnEditCollection.tsx @@ -562,7 +562,7 @@ class ColumnEditCollection extends Component { const title = canAdd ? undefined : t( - "Sorry, you've reached the maximum number of columns (%d). Delete columns to add more.", + `Sorry, you've reached the maximum number of columns (%d). Delete columns to add more.`, MAX_COL_COUNT ); @@ -671,7 +671,7 @@ function OnDemandEquationsWarning() { @@ -704,7 +704,7 @@ const RowContainer = styled('div')<{ css` align-items: flex-start; grid-template-columns: ${p.singleColumn - ? '1fr' + ? `1fr` : `${p.theme.space['2xl']} 1fr 40px 40px`}; @media (min-width: ${p.theme.breakpoints.sm}) { diff --git a/static/app/views/discover/table/queryField.tsx b/static/app/views/discover/table/queryField.tsx index 7c431279136a..c467e8ac80f7 100644 --- a/static/app/views/discover/table/queryField.tsx +++ b/static/app/views/discover/table/queryField.tsx @@ -742,7 +742,7 @@ const Container = styled('div')<{ display: grid; ${p => p.tripleLayout - ? 'grid-template-columns: 1fr 2fr;' + ? `grid-template-columns: 1fr 2fr;` : `grid-template-columns: repeat(${p.gridColumns}, 1fr) ${p.error ? 'auto' : ''};`} gap: ${p => p.theme.space.md}; align-items: center; diff --git a/static/app/views/discover/table/tableView.tsx b/static/app/views/discover/table/tableView.tsx index 3c39f68ebdac..3b579ccaac2f 100644 --- a/static/app/views/discover/table/tableView.tsx +++ b/static/app/views/discover/table/tableView.tsx @@ -1,5 +1,4 @@ import {Fragment} from 'react'; -import {useMatches} from 'react-router-dom'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import * as Sentry from '@sentry/react'; @@ -49,6 +48,7 @@ import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useNavigate} from 'sentry/utils/useNavigate'; import {useProjects} from 'sentry/utils/useProjects'; +import {useRoutes} from 'sentry/utils/useRoutes'; import {appendQueryDatasetParam, hasDatasetSelector} from 'sentry/views/dashboards/utils'; import { getExpandedResults, @@ -107,8 +107,8 @@ export function TableView(props: TableViewProps) { const theme = useTheme(); const navigate = useNavigate(); const {projects} = useProjects(); - const matches = useMatches(); - const replayLinkGenerator = generateReplayLink(matches); + const routes = useRoutes(); + const replayLinkGenerator = generateReplayLink(routes); function _renderPrependColumns( isHeader: boolean, diff --git a/static/app/views/explore/components/attributeBreakdowns/utils.tsx b/static/app/views/explore/components/attributeBreakdowns/utils.tsx index 65088727cd1f..5bc81a82a51b 100644 --- a/static/app/views/explore/components/attributeBreakdowns/utils.tsx +++ b/static/app/views/explore/components/attributeBreakdowns/utils.tsx @@ -69,7 +69,7 @@ function createActionButton( ): string { return [ ' ` display: grid; height: 100%; align-items: center; - grid-template-columns: ${p => (p.spacerCount > 0 ? 'auto 1rem 1fr' : '1fr')}; + grid-template-columns: ${p => (p.spacerCount > 0 ? `auto 1rem 1fr` : '1fr')}; `; const TreeValueTrunk = styled('div')` diff --git a/static/app/views/explore/hooks/useMetricOptions.tsx b/static/app/views/explore/hooks/useMetricOptions.tsx index e305ddade723..91161057012c 100644 --- a/static/app/views/explore/hooks/useMetricOptions.tsx +++ b/static/app/views/explore/hooks/useMetricOptions.tsx @@ -71,7 +71,7 @@ function metricOptionsQueryKey({ } return [ - getApiUrl('/organizations/$organizationIdOrSlug/events/', { + getApiUrl(`/organizations/$organizationIdOrSlug/events/`, { path: {organizationIdOrSlug: orgSlug!}, }), {query}, diff --git a/static/app/views/explore/hooks/useSortByFields.spec.tsx b/static/app/views/explore/hooks/useSortByFields.spec.tsx index 9b7b62d641a2..3ca6ac0429d4 100644 --- a/static/app/views/explore/hooks/useSortByFields.spec.tsx +++ b/static/app/views/explore/hooks/useSortByFields.spec.tsx @@ -37,7 +37,7 @@ describe('useSortByFields', () => { MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/trace-items/attributes/', + url: `/organizations/org-slug/trace-items/attributes/`, body: [], }); diff --git a/static/app/views/explore/hooks/useTab.spec.tsx b/static/app/views/explore/hooks/useTab.spec.tsx index eb5aab65f20b..e85b257b1bdb 100644 --- a/static/app/views/explore/hooks/useTab.spec.tsx +++ b/static/app/views/explore/hooks/useTab.spec.tsx @@ -12,7 +12,7 @@ function Wrapper({children}: {children: ReactNode}) { function makeInitialRouterConfig({table}: {table: string}) { return { location: { - pathname: '/organizations/org-slug/explore/traces/', + pathname: `/organizations/org-slug/explore/traces/`, query: {table}, }, }; diff --git a/static/app/views/explore/hooks/useVisualizeFields.spec.tsx b/static/app/views/explore/hooks/useVisualizeFields.spec.tsx index 6e1cc8845dbc..c38d32e605f8 100644 --- a/static/app/views/explore/hooks/useVisualizeFields.spec.tsx +++ b/static/app/views/explore/hooks/useVisualizeFields.spec.tsx @@ -46,7 +46,7 @@ describe('useVisualizeFields', () => { MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/trace-items/attributes/', + url: `/organizations/org-slug/trace-items/attributes/`, body: [], }); diff --git a/static/app/views/explore/logs/fieldRenderers.tsx b/static/app/views/explore/logs/fieldRenderers.tsx index ae920f2666ac..88b9d4874497 100644 --- a/static/app/views/explore/logs/fieldRenderers.tsx +++ b/static/app/views/explore/logs/fieldRenderers.tsx @@ -353,7 +353,7 @@ function FilteredTooltip({ return ( (null); const tableBodyRef = useRef(null); - const {width: tableWidth} = useDimensions({elementRef: tableRef}); const [expandedLogRows, setExpandedLogRows] = useState>( new Set(embeddedOptions?.openWithExpandedIds ?? []) ); @@ -558,7 +556,7 @@ export function LogsInfiniteTable({ {!embeddedOptions?.replay && ( ) : null} - + {embeddedOptions?.replay && showJumpDownButton ? ( ) : null} diff --git a/static/app/views/explore/logs/tables/logsTableRow.spec.tsx b/static/app/views/explore/logs/tables/logsTableRow.spec.tsx index a1596d08096c..90953517162d 100644 --- a/static/app/views/explore/logs/tables/logsTableRow.spec.tsx +++ b/static/app/views/explore/logs/tables/logsTableRow.spec.tsx @@ -99,7 +99,7 @@ describe('logsTableRow', () => { [LOGS_SORT_BYS_KEY]: '-timestamp', }, }, - route: '/organizations/:orgId/explore/logs/', + route: `/organizations/:orgId/explore/logs/`, }; const initialRouterConfigWithCodeFilePath = { diff --git a/static/app/views/explore/logs/useLogsQuery.tsx b/static/app/views/explore/logs/useLogsQuery.tsx index 8c9614ef3685..eae61961782f 100644 --- a/static/app/views/explore/logs/useLogsQuery.tsx +++ b/static/app/views/explore/logs/useLogsQuery.tsx @@ -252,7 +252,7 @@ function getPageParam( firstTimestamp = BigInt(firstRow[OurLogKnownFieldKey.TIMESTAMP_PRECISE]); lastTimestamp = BigInt(lastRow[OurLogKnownFieldKey.TIMESTAMP_PRECISE]); } catch { - logger.warn('No timestamp precise found for log row, using timestamp instead', { + logger.warn(`No timestamp precise found for log row, using timestamp instead`, { logId: firstRow[OurLogKnownFieldKey.ID], timestamp: firstRow[OurLogKnownFieldKey.TIMESTAMP], timestampPrecise: firstRow[OurLogKnownFieldKey.TIMESTAMP_PRECISE], diff --git a/static/app/views/explore/logs/useVirtualStreaming.tsx b/static/app/views/explore/logs/useVirtualStreaming.tsx index 91feb5a16163..18ebff9225c5 100644 --- a/static/app/views/explore/logs/useVirtualStreaming.tsx +++ b/static/app/views/explore/logs/useVirtualStreaming.tsx @@ -220,7 +220,7 @@ export function useVirtualStreaming({ hasWarnedRef.current = true; logger.warn( - 'No most recent page data timestamp found, skipping virtual streaming update', + `No most recent page data timestamp found, skipping virtual streaming update`, { logId: data?.pages?.[data.pages.length - 1]?.[0]?.data?.[0]?.[OurLogKnownFieldKey.ID], @@ -321,7 +321,7 @@ function getApproximateTimestamp(row: { } if (!timestampHasWarned) { timestampHasWarned = true; - logger.warn('No timestamp precise found for log row, using timestamp instead', { + logger.warn(`No timestamp precise found for log row, using timestamp instead`, { logId: row[OurLogKnownFieldKey.ID], traceId: row[OurLogKnownFieldKey.TRACE_ID], timestamp: row[OurLogKnownFieldKey.TIMESTAMP], diff --git a/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTable.spec.tsx b/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTable.spec.tsx index 8173884a1099..d4eea76a27ab 100644 --- a/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTable.spec.tsx +++ b/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTable.spec.tsx @@ -50,7 +50,7 @@ describe('useMultiQueryTable', () => { ['aggregate', useMultiQueryTableAggregateMode], ['sample', useMultiQueryTableSampleMode], ])( - 'triggers the high accuracy request when there is no data and a partial scan for %s mode', + `triggers the high accuracy request when there is no data and a partial scan for %s mode`, async (_mode, hook) => { jest.mocked(useReadQueriesFromLocation).mockReturnValue([ { diff --git a/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTimeseries.tsx b/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTimeseries.tsx index dc5dd96fc086..ed0fa88c38ae 100644 --- a/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTimeseries.tsx +++ b/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTimeseries.tsx @@ -107,7 +107,7 @@ function useMultiQueryTimeseriesImpl({ const timeseriesResult = useSortedTimeSeries( options, - 'api.explore.spans-timeseries', + `api.explore.spans-timeseries`, DiscoverDatasets.SPANS ); diff --git a/static/app/views/explore/spans/content.spec.tsx b/static/app/views/explore/spans/content.spec.tsx index 44b143b85704..294693c42005 100644 --- a/static/app/views/explore/spans/content.spec.tsx +++ b/static/app/views/explore/spans/content.spec.tsx @@ -75,7 +75,7 @@ describe('ExploreContent', () => { body: [project], }); MockApiClient.addMockResponse({ - url: '/assistant/', + url: `/assistant/`, method: 'GET', body: [], }); diff --git a/static/app/views/explore/spans/droppedFieldsAlert.tsx b/static/app/views/explore/spans/droppedFieldsAlert.tsx index 58a49d045969..52e7ade6fa5b 100644 --- a/static/app/views/explore/spans/droppedFieldsAlert.tsx +++ b/static/app/views/explore/spans/droppedFieldsAlert.tsx @@ -32,7 +32,7 @@ export function DroppedFieldsAlert(): React.JSX.Element | null { const changedReason = savedQuery.changedReason; if (changedReason.columns.length > 0) { columnsWarning.push( - tct('[columns] is no longer supported', { + tct(`[columns] is no longer supported`, { columns: changedReason.columns.join(', '), }) ); @@ -40,7 +40,7 @@ export function DroppedFieldsAlert(): React.JSX.Element | null { if (changedReason.equations) { equationsWarning.push( ...changedReason.equations.map(equation => - tct('[equation] is no longer supported because [reason] is unsupported', { + tct(`[equation] is no longer supported because [reason] is unsupported`, { equation: stripEquationPrefix(equation.equation), reason: typeof equation.reason === 'string' @@ -61,10 +61,10 @@ export function DroppedFieldsAlert(): React.JSX.Element | null { // make sure that the reason and the orderby aren't the same and word it correctly return typeof orderby.reason === 'string' || orderbyWithoutPrefix === reasonText - ? tct('sorting by [orderby] is no longer supported', { + ? tct(`sorting by [orderby] is no longer supported`, { orderby: orderbyWithoutPrefix, }) - : tct('sorting by [orderby] is not supported because [reason] is unsupported', { + : tct(`sorting by [orderby] is not supported because [reason] is unsupported`, { orderby: orderby.orderby, reason: reasonText, }); diff --git a/static/app/views/explore/tables/fieldRenderer.spec.tsx b/static/app/views/explore/tables/fieldRenderer.spec.tsx index 129f76ef9519..6ca21d0d7df7 100644 --- a/static/app/views/explore/tables/fieldRenderer.spec.tsx +++ b/static/app/views/explore/tables/fieldRenderer.spec.tsx @@ -101,7 +101,7 @@ describe('FieldRenderer tests', () => { expect(screen.getByText('spanId')).toBeInTheDocument(); expect(screen.getByRole('link')).toHaveAttribute( 'href', - '/organizations/org-slug/explore/traces/trace/traceId/?node=span-spanId&node=txn-transactionSpanId&source=traces&statsPeriod=14d&targetId=transactionSpanId×tamp=1727964900' + `/organizations/org-slug/explore/traces/trace/traceId/?node=span-spanId&node=txn-transactionSpanId&source=traces&statsPeriod=14d&targetId=transactionSpanId×tamp=1727964900` ); }); }); @@ -159,7 +159,7 @@ describe('FieldRenderer tests', () => { expect(screen.getByText('transactionId')).toBeInTheDocument(); expect(screen.getByRole('link')).toHaveAttribute( 'href', - '/organizations/org-slug/explore/traces/trace/traceId/?source=traces&statsPeriod=14d&targetId=transactionSpanId×tamp=1727964900' + `/organizations/org-slug/explore/traces/trace/traceId/?source=traces&statsPeriod=14d&targetId=transactionSpanId×tamp=1727964900` ); }); }); @@ -217,7 +217,7 @@ describe('FieldRenderer tests', () => { expect(screen.getByText('traceId')).toBeInTheDocument(); expect(screen.getByRole('link')).toHaveAttribute( 'href', - '/organizations/org-slug/explore/traces/trace/traceId/?source=traces&statsPeriod=14d×tamp=1727964900' + `/organizations/org-slug/explore/traces/trace/traceId/?source=traces&statsPeriod=14d×tamp=1727964900` ); }); }); diff --git a/static/app/views/feedback/feedbackEmptyState.tsx b/static/app/views/feedback/feedbackEmptyState.tsx index b2ac6532ff87..d1ac318bca84 100644 --- a/static/app/views/feedback/feedbackEmptyState.tsx +++ b/static/app/views/feedback/feedbackEmptyState.tsx @@ -101,7 +101,7 @@ export function FeedbackEmptyState({projectIds, issueTab = false}: Props) {

{t('What do users think?')}

{t( - "You can't read minds. At least we hope not. Ask users for feedback on the impact of their crashes or bugs and you shall receive." + `You can't read minds. At least we hope not. Ask users for feedback on the impact of their crashes or bugs and you shall receive.` )}

diff --git a/static/app/views/insights/browser/resources/components/sampleImages.spec.tsx b/static/app/views/insights/browser/resources/components/sampleImages.spec.tsx index 3522596f5f43..eb805ac829c3 100644 --- a/static/app/views/insights/browser/resources/components/sampleImages.spec.tsx +++ b/static/app/views/insights/browser/resources/components/sampleImages.spec.tsx @@ -90,7 +90,7 @@ const setupMockRequests = ( }, }); MockApiClient.addMockResponse({ - url: '/projects/org-slug/project-slug/performance/configure/', + url: `/projects/org-slug/project-slug/performance/configure/`, method: 'GET', body: { enable_images: enableImages, diff --git a/static/app/views/insights/browser/resources/components/tables/resourceTable.tsx b/static/app/views/insights/browser/resources/components/tables/resourceTable.tsx index bdaabcb0ca00..48aa7c02c1ed 100644 --- a/static/app/views/insights/browser/resources/components/tables/resourceTable.tsx +++ b/static/app/views/insights/browser/resources/components/tables/resourceTable.tsx @@ -55,7 +55,7 @@ const { const {EPM} = SpanFunction; const RESOURCE_SIZE_ALERT = t( - "If you're noticing unusually large resource sizes, try updating to SDK version 7.82.0 or higher." + `If you're noticing unusually large resource sizes, try updating to SDK version 7.82.0 or higher.` ); type Row = Pick< @@ -169,7 +169,7 @@ export function ResourceTable({sort, defaultResourceTypes}: Props) { if (key === 'avg(http.response_content_length)') { return ; } - if (key === 'avg(span.self_time)') { + if (key === `avg(span.self_time)`) { return ; } if (key === SPAN_OP) { diff --git a/static/app/views/insights/browser/resources/queries/useResourcePageQuery.ts b/static/app/views/insights/browser/resources/queries/useResourcePageQuery.ts index 6ba4dae7d05a..ca4fe7520a40 100644 --- a/static/app/views/insights/browser/resources/queries/useResourcePageQuery.ts +++ b/static/app/views/insights/browser/resources/queries/useResourcePageQuery.ts @@ -61,7 +61,7 @@ export const useResourcePagesQuery = ( fields: [ 'transaction', 'epm()', - 'avg(span.self_time)', + `avg(span.self_time)`, `avg(${HTTP_RESPONSE_CONTENT_LENGTH})`, RESOURCE_RENDER_BLOCKING_STATUS, ], diff --git a/static/app/views/insights/browser/resources/views/resourceSummaryPage.tsx b/static/app/views/insights/browser/resources/views/resourceSummaryPage.tsx index 90a5a0e55a72..6a0ad3bf19b3 100644 --- a/static/app/views/insights/browser/resources/views/resourceSummaryPage.tsx +++ b/static/app/views/insights/browser/resources/views/resourceSummaryPage.tsx @@ -146,7 +146,7 @@ function ResourceSummary() { spanMetrics?.[0]?.[`avg(${HTTP_RESPONSE_TRANSFER_SIZE})`] ?? 0 } avgDuration={spanMetrics?.[0]?.[`avg(${SPAN_SELF_TIME})`] ?? 0} - throughput={spanMetrics?.[0]?.['epm()'] ?? 0} + throughput={spanMetrics?.[0]?.[`epm()`] ?? 0} timeSpentTotal={spanMetrics?.[0]?.[`sum(${SPAN_SELF_TIME})`] ?? 0} /> diff --git a/static/app/views/insights/browser/resources/views/resourcesLandingPage.spec.tsx b/static/app/views/insights/browser/resources/views/resourcesLandingPage.spec.tsx index 269e8aad17cf..f5f5f9fa81cd 100644 --- a/static/app/views/insights/browser/resources/views/resourcesLandingPage.spec.tsx +++ b/static/app/views/insights/browser/resources/views/resourcesLandingPage.spec.tsx @@ -37,7 +37,7 @@ describe('ResourcesLandingPage', () => { pathname: `/organizations/${organization.slug}/insights/frontend/assets/`, query: {statsPeriod: '10d'}, }, - route: '/organizations/:orgId/insights/frontend/assets/', + route: `/organizations/:orgId/insights/frontend/assets/`, }; beforeEach(() => { @@ -266,7 +266,7 @@ const setupMockRequests = (organization: Organization) => { }); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events/', + url: `/organizations/org-slug/events/`, method: 'GET', match: [ MockApiClient.matchQuery({ diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.spec.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.spec.tsx index 9345f99b2b98..9a20f2492356 100644 --- a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.spec.tsx +++ b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.spec.tsx @@ -18,7 +18,7 @@ describe('PageOverviewWebVitalsDetailPanel', () => { pathname: `/organizations/${organization.slug}/insights/frontend/pageloads/overview/`, query: {}, }, - route: '/organizations/:orgId/insights/frontend/pageloads/overview/', + route: `/organizations/:orgId/insights/frontend/pageloads/overview/`, }; beforeEach(() => { diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx index 23d6181383d7..f16eabe6deb4 100644 --- a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx +++ b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx @@ -1,5 +1,4 @@ import {useMemo} from 'react'; -import {useMatches} from 'react-router-dom'; import styled from '@emotion/styled'; import {Link} from '@sentry/scraps/link'; @@ -24,6 +23,7 @@ import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useProjects} from 'sentry/utils/useProjects'; +import {useRoutes} from 'sentry/utils/useRoutes'; import type {DashboardFilters} from 'sentry/views/dashboards/types'; import {WebVitalStatusLineChart} from 'sentry/views/insights/browser/webVitals/components/charts/webVitalStatusLineChart'; import {PerformanceBadge} from 'sentry/views/insights/browser/webVitals/components/performanceBadge'; @@ -85,7 +85,7 @@ export function PageOverviewWebVitalsDetailPanel({ const location = useLocation(); const {projects} = useProjects(); const organization = useOrganization(); - const matches = useMatches(); + const routes = useRoutes(); const {replayExists} = useReplayExists(); const domainViewFilters = useDomainViewFilters(); @@ -93,7 +93,7 @@ export function PageOverviewWebVitalsDetailPanel({ const subregions = location.query[SpanFields.USER_GEO_SUBREGION] as SubregionCode[]; const isSpansWebVital = defined(webVital) && ['inp', 'cls', 'lcp'].includes(webVital); - const replayLinkGenerator = generateReplayLink(matches); + const replayLinkGenerator = generateReplayLink(routes); const project = useMemo( () => projects.find(p => p.id === String(location.query.project)), diff --git a/static/app/views/insights/browser/webVitals/components/tables/pagePerformanceTable.spec.tsx b/static/app/views/insights/browser/webVitals/components/tables/pagePerformanceTable.spec.tsx index 9f01d7167ec8..113d805b0aae 100644 --- a/static/app/views/insights/browser/webVitals/components/tables/pagePerformanceTable.spec.tsx +++ b/static/app/views/insights/browser/webVitals/components/tables/pagePerformanceTable.spec.tsx @@ -19,7 +19,7 @@ describe('PagePerformanceTable', () => { pathname: `/organizations/${organization.slug}/insights/frontend/pageloads/overview/`, query: {}, }, - route: '/organizations/:orgId/insights/frontend/pageloads/overview/', + route: `/organizations/:orgId/insights/frontend/pageloads/overview/`, }; let eventsMock: jest.Mock; diff --git a/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx b/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx index 68de04da3628..692833ced330 100644 --- a/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx +++ b/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx @@ -1,5 +1,4 @@ import {useCallback, useMemo} from 'react'; -import {useMatches} from 'react-router-dom'; import styled from '@emotion/styled'; import {ProjectAvatar} from '@sentry/scraps/avatar'; @@ -35,6 +34,7 @@ import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useProjects} from 'sentry/utils/useProjects'; +import {useRoutes} from 'sentry/utils/useRoutes'; import {PerformanceBadge} from 'sentry/views/insights/browser/webVitals/components/performanceBadge'; import {useTransactionSamplesWebVitalsScoresQuery} from 'sentry/views/insights/browser/webVitals/queries/storedScoreQueries/useTransactionSamplesWebVitalsScoresQuery'; import { @@ -153,7 +153,7 @@ export function PageSamplePerformanceTable({transaction, search, limit = 9}: Pro const {projects} = useProjects(); const organization = useOrganization(); const {replayExists} = useReplayExists(); - const matches = useMatches(); + const routes = useRoutes(); const navigate = useNavigate(); const domainViewFilters = useDomainViewFilters(); @@ -184,7 +184,7 @@ export function PageSamplePerformanceTable({transaction, search, limit = 9}: Pro sortableFields: sortableFields as unknown as string[], }); - const replayLinkGenerator = generateReplayLink(matches); + const replayLinkGenerator = generateReplayLink(routes); const project = useMemo( () => projects.find(p => p.id === String(location.query.project)), diff --git a/static/app/views/insights/browser/webVitals/components/webVitalDescription.tsx b/static/app/views/insights/browser/webVitals/components/webVitalDescription.tsx index 243de300d723..bf751c5bb317 100644 --- a/static/app/views/insights/browser/webVitals/components/webVitalDescription.tsx +++ b/static/app/views/insights/browser/webVitals/components/webVitalDescription.tsx @@ -137,7 +137,7 @@ export function WebVitalDescription({score, value, webVital}: Props) { {longDescription} - {tct(' [webVital] is available for the following browsers:', { + {tct(` [webVital] is available for the following browsers:`, { webVital: webVital.toUpperCase(), })} diff --git a/static/app/views/insights/browser/webVitals/components/webVitalsDetailPanel.spec.tsx b/static/app/views/insights/browser/webVitals/components/webVitalsDetailPanel.spec.tsx index 648fda81ded2..76f3b8a24814 100644 --- a/static/app/views/insights/browser/webVitals/components/webVitalsDetailPanel.spec.tsx +++ b/static/app/views/insights/browser/webVitals/components/webVitalsDetailPanel.spec.tsx @@ -68,13 +68,13 @@ describe('WebVitalsDetailPanel', () => { 'performance_score(measurements.score.lcp)', 'performance_score(measurements.score.fcp)', 'performance_score(measurements.score.cls)', - 'performance_score(measurements.score.inp)', + `performance_score(measurements.score.inp)`, 'performance_score(measurements.score.ttfb)', 'performance_score(measurements.score.total)', 'avg(measurements.score.weight.lcp)', 'avg(measurements.score.weight.fcp)', 'avg(measurements.score.weight.cls)', - 'avg(measurements.score.weight.inp)', + `avg(measurements.score.weight.inp)`, 'avg(measurements.score.weight.ttfb)', 'count()', 'count_scores(measurements.score.total)', diff --git a/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/getWebVitalScoresFromTableDataRow.tsx b/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/getWebVitalScoresFromTableDataRow.tsx index cfa56e62a318..a5f96b318934 100644 --- a/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/getWebVitalScoresFromTableDataRow.tsx +++ b/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/getWebVitalScoresFromTableDataRow.tsx @@ -32,7 +32,7 @@ function getWebVitalScore(data: PerformanceScores, webVital: WebVitals): number } function getTotalScore(data: TotalPerformanceScore): number { - return data['performance_score(measurements.score.total)'] * 100; + return data[`performance_score(measurements.score.total)`] * 100; } function getWebVitalScoreCount(data: CountScores, webVital: WebVitals | 'total'): number { diff --git a/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/useProjectWebVitalsScoresQuery.tsx b/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/useProjectWebVitalsScoresQuery.tsx index b6e8df12a3bc..bced2eb5796f 100644 --- a/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/useProjectWebVitalsScoresQuery.tsx +++ b/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/useProjectWebVitalsScoresQuery.tsx @@ -47,13 +47,13 @@ export const useProjectWebVitalsScoresQuery = ({ 'performance_score(measurements.score.lcp)', 'performance_score(measurements.score.fcp)', 'performance_score(measurements.score.cls)', - 'performance_score(measurements.score.inp)', + `performance_score(measurements.score.inp)`, 'performance_score(measurements.score.ttfb)', 'performance_score(measurements.score.total)', 'avg(measurements.score.weight.lcp)', 'avg(measurements.score.weight.fcp)', 'avg(measurements.score.weight.cls)', - 'avg(measurements.score.weight.inp)', + `avg(measurements.score.weight.inp)`, 'avg(measurements.score.weight.ttfb)', 'count()', 'count_scores(measurements.score.total)', @@ -61,7 +61,7 @@ export const useProjectWebVitalsScoresQuery = ({ 'count_scores(measurements.score.fcp)', 'count_scores(measurements.score.cls)', 'count_scores(measurements.score.ttfb)', - 'count_scores(measurements.score.inp)', + `count_scores(measurements.score.inp)`, ...(weightWebVital === 'total' ? [] : [`sum(measurements.score.weight.${weightWebVital})` as const]), diff --git a/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/useTransactionWebVitalsScoresQuery.tsx b/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/useTransactionWebVitalsScoresQuery.tsx index eef4d5959bfc..3fdf71a179e1 100644 --- a/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/useTransactionWebVitalsScoresQuery.tsx +++ b/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/useTransactionWebVitalsScoresQuery.tsx @@ -88,12 +88,12 @@ export const useTransactionWebVitalsScoresQuery = ({ `opportunity_score(measurements.score.${webVital})`, 'performance_score(measurements.score.total)', 'count()', - 'count_scores(measurements.score.lcp)', - 'count_scores(measurements.score.fcp)', - 'count_scores(measurements.score.cls)', - 'count_scores(measurements.score.inp)', - 'count_scores(measurements.score.ttfb)', - 'count_scores(measurements.score.total)', + `count_scores(measurements.score.lcp)`, + `count_scores(measurements.score.fcp)`, + `count_scores(measurements.score.cls)`, + `count_scores(measurements.score.inp)`, + `count_scores(measurements.score.ttfb)`, + `count_scores(measurements.score.total)`, totalOpportunityScoreField, ], }, diff --git a/static/app/views/insights/browser/webVitals/queries/useSpanSamplesWebVitalsQuery.tsx b/static/app/views/insights/browser/webVitals/queries/useSpanSamplesWebVitalsQuery.tsx index e2e31bb1663c..c79744e2a0dd 100644 --- a/static/app/views/insights/browser/webVitals/queries/useSpanSamplesWebVitalsQuery.tsx +++ b/static/app/views/insights/browser/webVitals/queries/useSpanSamplesWebVitalsQuery.tsx @@ -137,7 +137,7 @@ export function useSpanSamplesWebVitalsQuery({ row[SpanFields.USER_IP], replayId: row[SpanFields.REPLAY_ID] ?? row[SpanFields.REPLAYID], 'profile.id': row[SpanFields.PROFILEID] ?? row[SpanFields.PROFILE_ID], - totalScore: Math.round((row['measurements.score.total'] ?? 0) * 100), + totalScore: Math.round((row[`measurements.score.total`] ?? 0) * 100), inpScore: Math.round((row[SpanFields.INP_SCORE_RATIO] ?? 0) * 100), lcpScore: Math.round((row[SpanFields.LCP_SCORE_RATIO] ?? 0) * 100), clsScore: Math.round((row[SpanFields.CLS_SCORE_RATIO] ?? 0) * 100), diff --git a/static/app/views/insights/browser/webVitals/views/pageOverview.spec.tsx b/static/app/views/insights/browser/webVitals/views/pageOverview.spec.tsx index 7ee53a1c7c98..28f820623664 100644 --- a/static/app/views/insights/browser/webVitals/views/pageOverview.spec.tsx +++ b/static/app/views/insights/browser/webVitals/views/pageOverview.spec.tsx @@ -19,7 +19,7 @@ describe('PageOverview', () => { pathname: `/organizations/${organization.slug}/insights/frontend/pageloads/overview/`, query: {}, }, - route: '/organizations/:orgId/insights/frontend/pageloads/overview/', + route: `/organizations/:orgId/insights/frontend/pageloads/overview/`, }; let eventsMock: jest.Mock; @@ -116,13 +116,13 @@ describe('PageOverview', () => { 'performance_score(measurements.score.lcp)', 'performance_score(measurements.score.fcp)', 'performance_score(measurements.score.cls)', - 'performance_score(measurements.score.inp)', + `performance_score(measurements.score.inp)`, 'performance_score(measurements.score.ttfb)', 'performance_score(measurements.score.total)', 'avg(measurements.score.weight.lcp)', 'avg(measurements.score.weight.fcp)', 'avg(measurements.score.weight.cls)', - 'avg(measurements.score.weight.inp)', + `avg(measurements.score.weight.inp)`, 'avg(measurements.score.weight.ttfb)', 'count()', 'count_scores(measurements.score.total)', @@ -130,7 +130,7 @@ describe('PageOverview', () => { 'count_scores(measurements.score.fcp)', 'count_scores(measurements.score.cls)', 'count_scores(measurements.score.ttfb)', - 'count_scores(measurements.score.inp)', + `count_scores(measurements.score.inp)`, ], query: 'span.op:[ui.interaction.click,ui.interaction.hover,ui.interaction.drag,ui.interaction.press,ui.webvital.cls,ui.webvital.lcp,pageload,""]', diff --git a/static/app/views/insights/browser/webVitals/views/webVitalsLandingPage.spec.tsx b/static/app/views/insights/browser/webVitals/views/webVitalsLandingPage.spec.tsx index eb33b9d336c2..5e762fc1b916 100644 --- a/static/app/views/insights/browser/webVitals/views/webVitalsLandingPage.spec.tsx +++ b/static/app/views/insights/browser/webVitals/views/webVitalsLandingPage.spec.tsx @@ -18,7 +18,7 @@ describe('WebVitalsLandingPage', () => { pathname: `/organizations/${organization.slug}/insights/frontend/pageloads/`, query: {}, }, - route: '/organizations/:orgId/insights/frontend/pageloads/', + route: `/organizations/:orgId/insights/frontend/pageloads/`, }; let eventsMock: jest.Mock; @@ -158,13 +158,13 @@ describe('WebVitalsLandingPage', () => { 'performance_score(measurements.score.lcp)', 'performance_score(measurements.score.fcp)', 'performance_score(measurements.score.cls)', - 'performance_score(measurements.score.inp)', + `performance_score(measurements.score.inp)`, 'performance_score(measurements.score.ttfb)', 'performance_score(measurements.score.total)', 'avg(measurements.score.weight.lcp)', 'avg(measurements.score.weight.fcp)', 'avg(measurements.score.weight.cls)', - 'avg(measurements.score.weight.inp)', + `avg(measurements.score.weight.inp)`, 'avg(measurements.score.weight.ttfb)', 'count()', 'count_scores(measurements.score.total)', @@ -172,7 +172,7 @@ describe('WebVitalsLandingPage', () => { 'count_scores(measurements.score.fcp)', 'count_scores(measurements.score.cls)', 'count_scores(measurements.score.ttfb)', - 'count_scores(measurements.score.inp)', + `count_scores(measurements.score.inp)`, ], query: 'span.op:[ui.interaction.click,ui.interaction.hover,ui.interaction.drag,ui.interaction.press,ui.webvital.cls,ui.webvital.lcp,pageload,""]', diff --git a/static/app/views/insights/cache/components/charts/hitMissChart.tsx b/static/app/views/insights/cache/components/charts/hitMissChart.tsx index 26412eb5b508..60335b52081a 100644 --- a/static/app/views/insights/cache/components/charts/hitMissChart.tsx +++ b/static/app/views/insights/cache/components/charts/hitMissChart.tsx @@ -39,7 +39,7 @@ export function CacheHitMissChart({search}: Props) { return ( { body: [], }); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events-timeseries/', + url: `/organizations/org-slug/events-timeseries/`, method: 'GET', body: [], }); diff --git a/static/app/views/insights/cache/components/tables/transactionsTable.tsx b/static/app/views/insights/cache/components/tables/transactionsTable.tsx index fbdf30cef6a4..69741738be75 100644 --- a/static/app/views/insights/cache/components/tables/transactionsTable.tsx +++ b/static/app/views/insights/cache/components/tables/transactionsTable.tsx @@ -87,7 +87,7 @@ const COLUMN_ORDER: Column[] = [ width: COL_WIDTH_UNDEFINED, }, { - key: 'sum(span.self_time)', + key: `sum(span.self_time)`, name: DataTitles.timeSpent, width: COL_WIDTH_UNDEFINED, }, @@ -96,7 +96,7 @@ const COLUMN_ORDER: Column[] = [ const SORTABLE_FIELDS = [ `${EPM}()`, `${CACHE_MISS_RATE}()`, - 'sum(span.self_time)', + `sum(span.self_time)`, `avg(${CACHE_ITEM_SIZE})`, ] as const; diff --git a/static/app/views/insights/cache/views/cacheLandingPage.spec.tsx b/static/app/views/insights/cache/views/cacheLandingPage.spec.tsx index 506627187aad..03adbde6a5e4 100644 --- a/static/app/views/insights/cache/views/cacheLandingPage.spec.tsx +++ b/static/app/views/insights/cache/views/cacheLandingPage.spec.tsx @@ -36,7 +36,7 @@ describe('CacheLandingPage', () => { pathname: `/organizations/${organization.slug}/insights/backend/caches/`, query: {statsPeriod: '10d', project: '1'}, }, - route: '/organizations/:orgId/insights/backend/caches/', + route: `/organizations/:orgId/insights/backend/caches/`, }; ProjectsStore.loadInitialData([ diff --git a/static/app/views/insights/cache/views/cacheLandingPage.tsx b/static/app/views/insights/cache/views/cacheLandingPage.tsx index b92763050b1f..e8583cdf423d 100644 --- a/static/app/views/insights/cache/views/cacheLandingPage.tsx +++ b/static/app/views/insights/cache/views/cacheLandingPage.tsx @@ -165,12 +165,12 @@ export default PageWithProviders; const addCustomMeta = (meta?: EventsMetaType) => { if (meta?.fields) { meta.fields[`avg(${CACHE_ITEM_SIZE})`] = 'size'; - meta.fields['avg(span.duration)'] = 'duration'; + meta.fields[`avg(span.duration)`] = 'duration'; meta.units[`avg(${CACHE_ITEM_SIZE})`] = 'byte'; } if (meta?.units) { - meta.units['avg(span.duration)'] = 'millisecond'; + meta.units[`avg(span.duration)`] = 'millisecond'; } }; diff --git a/static/app/views/insights/common/components/fullSpanDescription.spec.tsx b/static/app/views/insights/common/components/fullSpanDescription.spec.tsx index 7db0b058c692..98cde8022e80 100644 --- a/static/app/views/insights/common/components/fullSpanDescription.spec.tsx +++ b/static/app/views/insights/common/components/fullSpanDescription.spec.tsx @@ -64,7 +64,7 @@ describe('FullSpanDescription', () => { { project: project.slug, span_id: spanId, - 'span.description': '{"insert": "my_cool_collection😎", "a": {}}', + 'span.description': `{"insert": "my_cool_collection😎", "a": {}}`, 'db.system': 'mongodb', }, ], @@ -92,8 +92,7 @@ describe('FullSpanDescription', () => { { project: project.slug, span_id: spanId, - 'span.description': - '{"insert": "my_cool_collection😎", "a": {}, "uh_oh":"the_query_is_truncated", "ohno*', + 'span.description': `{"insert": "my_cool_collection😎", "a": {}, "uh_oh":"the_query_is_truncated", "ohno*`, 'db.system': 'mongodb', }, ], diff --git a/static/app/views/insights/common/components/fullSpanDescription.tsx b/static/app/views/insights/common/components/fullSpanDescription.tsx index cabc16b45488..84c25a2de4c3 100644 --- a/static/app/views/insights/common/components/fullSpanDescription.tsx +++ b/static/app/views/insights/common/components/fullSpanDescription.tsx @@ -7,7 +7,7 @@ import {ClippedBox} from 'sentry/components/clippedBox'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {IconOpen} from 'sentry/icons'; import {t} from 'sentry/locale'; -import {SQLishFormatter} from 'sentry/utils/sqlish'; +import {SQLishFormatter} from 'sentry/utils/sqlish/SQLishFormatter'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; diff --git a/static/app/views/insights/common/components/moduleFeature.spec.tsx b/static/app/views/insights/common/components/moduleFeature.spec.tsx index cfe30d9a90ac..7dcaf891b5cb 100644 --- a/static/app/views/insights/common/components/moduleFeature.spec.tsx +++ b/static/app/views/insights/common/components/moduleFeature.spec.tsx @@ -24,7 +24,7 @@ describe('ModulePageProviders', () => { } ); - await screen.findByText("You don't have access to this feature"); + await screen.findByText(`You don't have access to this feature`); }); it('renders module content if module is enabled', async () => { @@ -39,6 +39,6 @@ describe('ModulePageProviders', () => { } ); - await screen.findByText('Module Content'); + await screen.findByText(`Module Content`); }); }); diff --git a/static/app/views/insights/common/components/modulesOnboarding.tsx b/static/app/views/insights/common/components/modulesOnboarding.tsx index fc4c07082c4d..7a7327ad529f 100644 --- a/static/app/views/insights/common/components/modulesOnboarding.tsx +++ b/static/app/views/insights/common/components/modulesOnboarding.tsx @@ -260,7 +260,7 @@ type EmptyStateContent = { const EMPTY_STATE_CONTENT: Record = { [ModuleName.APP_START]: { - heading: t("Don't lose your user's attention before your app loads"), + heading: t(`Don't lose your user's attention before your app loads`), description: tct( 'Monitor cold and warm [dataTypePlural] and track down the operations and releases contributing to regressions.', { @@ -268,7 +268,7 @@ const EMPTY_STATE_CONTENT: Record = { MODULE_DATA_TYPES_PLURAL[ModuleName.APP_START].toLocaleLowerCase(), } ), - valuePropDescription: tct('Mobile [dataType] insights give you visibility into:', { + valuePropDescription: tct(`Mobile [dataType] insights give you visibility into:`, { dataType: MODULE_DATA_TYPES[ModuleName.APP_START], }), valuePropPoints: [ @@ -423,7 +423,7 @@ const EMPTY_STATE_CONTENT: Record = { supportedSdks: ['python', 'javascript', 'php', 'java', 'ruby', 'dotnet'], }, [ModuleName.SCREEN_LOAD]: { - heading: t("Don’t lose your user's attention once your app loads"), + heading: t(`Don’t lose your user's attention once your app loads`), description: tct( 'View the most active [dataTypePlural] in your mobile application and monitor your releases for screen load performance.', { @@ -463,7 +463,7 @@ const EMPTY_STATE_CONTENT: Record = { supportedSdks: ['android', 'flutter', 'apple-ios', 'react-native'], }, [ModuleName.SESSIONS]: { - heading: t("Get insights about your application's session health"), + heading: t(`Get insights about your application's session health`), description: tct( 'Understand the frequency of handled errors and crashes compared to healthy sessions.', { diff --git a/static/app/views/insights/common/components/spanDescription.spec.tsx b/static/app/views/insights/common/components/spanDescription.spec.tsx index 16d9e79b7f7c..8aac67db7496 100644 --- a/static/app/views/insights/common/components/spanDescription.spec.tsx +++ b/static/app/views/insights/common/components/spanDescription.spec.tsx @@ -110,7 +110,7 @@ describe('DatabaseSpanDescription', () => { }); it('correctly formats and displays MongoDB queries', async () => { - const sampleMongoDBQuery = '{"a": "?", "insert": "documents"}'; + const sampleMongoDBQuery = `{"a": "?", "insert": "documents"}`; MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, diff --git a/static/app/views/insights/common/components/spanDescription.tsx b/static/app/views/insights/common/components/spanDescription.tsx index afa063e21505..2212b9bac0a8 100644 --- a/static/app/views/insights/common/components/spanDescription.tsx +++ b/static/app/views/insights/common/components/spanDescription.tsx @@ -6,7 +6,7 @@ import {Flex} from '@sentry/scraps/layout'; import {ClippedBox} from 'sentry/components/clippedBox'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; -import {SQLishFormatter} from 'sentry/utils/sqlish'; +import {SQLishFormatter} from 'sentry/utils/sqlish/SQLishFormatter'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; diff --git a/static/app/views/insights/common/components/tableCells/renderHeadCell.tsx b/static/app/views/insights/common/components/tableCells/renderHeadCell.tsx index c3f4ddc7c298..2638ccbe6647 100644 --- a/static/app/views/insights/common/components/tableCells/renderHeadCell.tsx +++ b/static/app/views/insights/common/components/tableCells/renderHeadCell.tsx @@ -42,10 +42,10 @@ const SORTABLE_FIELDS = new Set([ `sum(${SPAN_DURATION})`, `sum(${SPAN_SELF_TIME})`, `p95(${SPAN_SELF_TIME})`, - 'p75(transaction.duration)', - 'transaction.duration', + `p75(transaction.duration)`, + `transaction.duration`, 'transaction', - 'count()', + `count()`, `${EPM}()`, `${TPM}()`, `${TIME_SPENT_PERCENTAGE}()`, diff --git a/static/app/views/insights/common/components/tableCells/spanDescriptionCell.spec.tsx b/static/app/views/insights/common/components/tableCells/spanDescriptionCell.spec.tsx deleted file mode 100644 index fac858111be9..000000000000 --- a/static/app/views/insights/common/components/tableCells/spanDescriptionCell.spec.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import {OrganizationFixture} from 'sentry-fixture/organization'; - -import {render, screen} from 'sentry-test/reactTestingLibrary'; - -import {ModuleName} from 'sentry/views/insights/types'; - -import {SpanDescriptionCell} from './spanDescriptionCell'; - -describe('SpanDescriptionCell', () => { - const organization = OrganizationFixture(); - - it('formats SQL descriptions with bold keywords via toSimpleMarkup', () => { - render( - , - {organization} - ); - - // SQLishFormatter.toSimpleMarkup uppercases keywords and wraps them in - const boldElements = document.querySelectorAll('b'); - const boldText = Array.from(boldElements).map(el => el.textContent); - expect(boldText).toEqual(['SELECT', 'FROM', 'WHERE']); - }); - - it('renders raw description for non-DB modules', () => { - render( - , - {organization} - ); - - expect(screen.getByText('https://example.com/api/resource')).toBeInTheDocument(); - }); -}); diff --git a/static/app/views/insights/common/components/tableCells/spanDescriptionCell.tsx b/static/app/views/insights/common/components/tableCells/spanDescriptionCell.tsx index cc7758bdd159..c7dec474f85a 100644 --- a/static/app/views/insights/common/components/tableCells/spanDescriptionCell.tsx +++ b/static/app/views/insights/common/components/tableCells/spanDescriptionCell.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import {Hovercard} from 'sentry/components/hovercard'; import {t} from 'sentry/locale'; -import {SQLishFormatter} from 'sentry/utils/sqlish'; +import {SQLishFormatter} from 'sentry/utils/sqlish/SQLishFormatter'; import {FullSpanDescription} from 'sentry/views/insights/common/components/fullSpanDescription'; import {SpanGroupDetailsLink} from 'sentry/views/insights/common/components/spanGroupDetailsLink'; import {SupportedDatabaseSystem} from 'sentry/views/insights/database/utils/constants'; diff --git a/static/app/views/insights/common/components/widgets/hooks/useResourceSummarySeries.tsx b/static/app/views/insights/common/components/widgets/hooks/useResourceSummarySeries.tsx index bba34262bf91..53caa178793d 100644 --- a/static/app/views/insights/common/components/widgets/hooks/useResourceSummarySeries.tsx +++ b/static/app/views/insights/common/components/widgets/hooks/useResourceSummarySeries.tsx @@ -47,7 +47,7 @@ export function useResourceSummarySeries(props: Props) { { query: search, yAxis: [ - 'epm()', + `epm()`, `avg(${SPAN_SELF_TIME})`, `avg(${HTTP_RESPONSE_CONTENT_LENGTH})`, `avg(${HTTP_DECODED_RESPONSE_CONTENT_LENGTH})`, diff --git a/static/app/views/insights/common/components/widgets/overviewCacheMissChartWidget.tsx b/static/app/views/insights/common/components/widgets/overviewCacheMissChartWidget.tsx index 5ab02a0b1c1c..10b87d1ed838 100644 --- a/static/app/views/insights/common/components/widgets/overviewCacheMissChartWidget.tsx +++ b/static/app/views/insights/common/components/widgets/overviewCacheMissChartWidget.tsx @@ -129,7 +129,7 @@ export default function OverviewCacheMissChartWidget(props: LoadableChartWidgetP to={ organization.features.includes('insight-modules') ? `/insights/backend/caches?project=${item['project.id']}&transaction=${item.transaction}` - : '/insights/backend/caches' + : `/insights/backend/caches` } > {item.transaction} diff --git a/static/app/views/insights/common/components/widgets/releaseNewIssuesChartWidget.tsx b/static/app/views/insights/common/components/widgets/releaseNewIssuesChartWidget.tsx index 1f1242137cc4..cfa967e80680 100644 --- a/static/app/views/insights/common/components/widgets/releaseNewIssuesChartWidget.tsx +++ b/static/app/views/insights/common/components/widgets/releaseNewIssuesChartWidget.tsx @@ -35,7 +35,7 @@ export default function ReleaseNewIssuesChartWidget(props: LoadableChartWidgetPr )} series={series} description={t( - "New issue counts over time, grouped by release. The 5 most recent releases are shown, and the rest are grouped into 'other'." + `New issue counts over time, grouped by release. The 5 most recent releases are shown, and the rest are grouped into 'other'.` )} isPending={isPending} error={error} diff --git a/static/app/views/insights/common/queries/useDiscover.spec.tsx b/static/app/views/insights/common/queries/useDiscover.spec.tsx index b1fa09006bde..43c2a6ab84c1 100644 --- a/static/app/views/insights/common/queries/useDiscover.spec.tsx +++ b/static/app/views/insights/common/queries/useDiscover.spec.tsx @@ -119,7 +119,7 @@ describe('useDiscover', () => { per_page: 10, project: [], sort: '-epm()', - query: 'span.group:221aa7ebd216 transaction:/api/details release:0.0.1', + query: `span.group:221aa7ebd216 transaction:/api/details release:0.0.1`, referrer: 'api-spec', sampling: SAMPLING_MODE.NORMAL, statsPeriod: '10d', @@ -258,8 +258,7 @@ describe('useDiscover', () => { per_page: 10, project: [], sort: '-span.group', - query: - 'span.group:221aa7ebd216 measurements.inp:<50 measurements.inp:>0 transaction:/api/details release:0.0.1', + query: `span.group:221aa7ebd216 measurements.inp:<50 measurements.inp:>0 transaction:/api/details release:0.0.1`, referrer: 'api-spec', statsPeriod: '10d', }, diff --git a/static/app/views/insights/common/queries/useHasTtfdConfigured.tsx b/static/app/views/insights/common/queries/useHasTtfdConfigured.tsx index ad9b2e12dc55..403e46b36939 100644 --- a/static/app/views/insights/common/queries/useHasTtfdConfigured.tsx +++ b/static/app/views/insights/common/queries/useHasTtfdConfigured.tsx @@ -20,8 +20,8 @@ export function useTTFDConfigured(additionalFilters?: string[]) { search: queryString, enabled: !isReleasesLoading, fields: [ - 'avg(measurements.time_to_initial_display)', - 'avg(measurements.time_to_full_display)', + `avg(measurements.time_to_initial_display)`, + `avg(measurements.time_to_full_display)`, 'count()', ], }, diff --git a/static/app/views/insights/common/queries/useSpanSamples.tsx b/static/app/views/insights/common/queries/useSpanSamples.tsx index dd3801d0db7b..b098c70d2a56 100644 --- a/static/app/views/insights/common/queries/useSpanSamples.tsx +++ b/static/app/views/insights/common/queries/useSpanSamples.tsx @@ -137,7 +137,7 @@ export const useSpanSamples = ( meta: EventsMetaType; }>( [ - getApiUrl('/organizations/$organizationIdOrSlug/spans-samples/', { + getApiUrl(`/organizations/$organizationIdOrSlug/spans-samples/`, { path: {organizationIdOrSlug: organization.slug}, }), { diff --git a/static/app/views/insights/common/utils/buildEventViewQuery.tsx b/static/app/views/insights/common/utils/buildEventViewQuery.tsx index 39bebed7f0ed..744721feedb1 100644 --- a/static/app/views/insights/common/utils/buildEventViewQuery.tsx +++ b/static/app/views/insights/common/utils/buildEventViewQuery.tsx @@ -58,7 +58,7 @@ export function buildEventViewQuery({ if (defined(spanCategory)) { if (spanCategory === NULL_SPAN_CATEGORY) { - result.push('!has:span.category'); + result.push(`!has:span.category`); } else if (spanCategory !== 'Other') { result.push(`span.category:${spanCategory}`); } diff --git a/static/app/views/insights/common/utils/getAlertsUrl.tsx b/static/app/views/insights/common/utils/getAlertsUrl.tsx index 64f720ace3e0..a0431f4ef386 100644 --- a/static/app/views/insights/common/utils/getAlertsUrl.tsx +++ b/static/app/views/insights/common/utils/getAlertsUrl.tsx @@ -63,7 +63,7 @@ export function getAlertsUrl({ return ( makeAlertsPathname({ - path: '/new/metric/', + path: `/new/metric/`, organization, }) + `?${qs.stringify(queryParams)}` ); diff --git a/static/app/views/insights/common/views/spanSummaryPage/sampleList/index.spec.tsx b/static/app/views/insights/common/views/spanSummaryPage/sampleList/index.spec.tsx index 6f43fb89c9fb..a50ebefd58cd 100644 --- a/static/app/views/insights/common/views/spanSummaryPage/sampleList/index.spec.tsx +++ b/static/app/views/insights/common/views/spanSummaryPage/sampleList/index.spec.tsx @@ -19,7 +19,7 @@ describe('SampleList', () => { body: [], }); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events-timeseries/', + url: `/organizations/org-slug/events-timeseries/`, method: 'GET', body: [], }); diff --git a/static/app/views/insights/common/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx b/static/app/views/insights/common/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx index 2e1e8c113757..8e39e6ce7ef5 100644 --- a/static/app/views/insights/common/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx +++ b/static/app/views/insights/common/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx @@ -160,7 +160,7 @@ describe('SampleTable', () => { const initializeMockRequests = () => { MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events/', + url: `/organizations/org-slug/events/`, body: { data: [ { @@ -178,7 +178,7 @@ const initializeMockRequests = () => { ], }); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events/', + url: `/organizations/org-slug/events/`, body: { data: [ { diff --git a/static/app/views/insights/crons/components/monitorForm.tsx b/static/app/views/insights/crons/components/monitorForm.tsx index b4032206ccb4..8137bd92bda6 100644 --- a/static/app/views/insights/crons/components/monitorForm.tsx +++ b/static/app/views/insights/crons/components/monitorForm.tsx @@ -590,7 +590,7 @@ const InputGroup = styled('div')<{noPadding?: boolean}>` gap: ${p => p.theme.space.md}; ${FieldWrapper} { - ${p => p.noPadding && 'padding: 0;'}; + ${p => p.noPadding && `padding: 0;`}; } `; diff --git a/static/app/views/insights/crons/components/monitorHeader.tsx b/static/app/views/insights/crons/components/monitorHeader.tsx index 5af89e2a441f..3d5ef19ed226 100644 --- a/static/app/views/insights/crons/components/monitorHeader.tsx +++ b/static/app/views/insights/crons/components/monitorHeader.tsx @@ -19,7 +19,7 @@ export function MonitorHeader({monitor, orgSlug, onUpdate}: Props) { const crumbs = [ { label: t('Alerts'), - to: makeAlertsPathname({path: '/rules/', organization}), + to: makeAlertsPathname({path: `/rules/`, organization}), preservePageFilters: true, }, { diff --git a/static/app/views/insights/crons/components/newMonitorButton.tsx b/static/app/views/insights/crons/components/newMonitorButton.tsx index ed21d316384d..2c3cc4c5552d 100644 --- a/static/app/views/insights/crons/components/newMonitorButton.tsx +++ b/static/app/views/insights/crons/components/newMonitorButton.tsx @@ -11,7 +11,7 @@ export function NewMonitorButton(props: Props) { return ( diff --git a/static/app/views/insights/crons/components/overviewTimeline/index.tsx b/static/app/views/insights/crons/components/overviewTimeline/index.tsx index df442cb979bb..f4eb07a61dd5 100644 --- a/static/app/views/insights/crons/components/overviewTimeline/index.tsx +++ b/static/app/views/insights/crons/components/overviewTimeline/index.tsx @@ -17,7 +17,7 @@ import {useDateNavigation} from 'sentry/components/checkInTimeline/hooks/useDate import {useTimeWindowConfig} from 'sentry/components/checkInTimeline/hooks/useTimeWindowConfig'; import {Panel} from 'sentry/components/panels/panel'; import {Sticky} from 'sentry/components/sticky'; -import {useQueryClient} from 'sentry/utils/queryClient'; +import {setApiQueryData, useQueryClient} from 'sentry/utils/queryClient'; import {useApi} from 'sentry/utils/useApi'; import {useDebouncedValue} from 'sentry/utils/useDebouncedValue'; import {useDimensions} from 'sentry/utils/useDimensions'; @@ -25,7 +25,7 @@ import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {CronServiceIncidents} from 'sentry/views/insights/crons/components/serviceIncidents'; import type {Monitor} from 'sentry/views/insights/crons/types'; -import {monitorListApiOptions} from 'sentry/views/insights/crons/utils'; +import {makeMonitorListQueryKey} from 'sentry/views/insights/crons/utils'; import {OverviewRow} from './overviewRow'; import {SortSelector} from './sortSelector'; @@ -53,16 +53,14 @@ export function OverviewTimeline({monitorList}: Props) { return; } - const monitorListOptions = monitorListApiOptions(organization, location.query); - - queryClient.setQueryData(monitorListOptions.queryKey, old => { - if (!old) { + const queryKey = makeMonitorListQueryKey(organization, location.query); + setApiQueryData(queryClient, queryKey, oldMonitorList => { + if (!oldMonitorList) { return undefined; } - const oldMonitorList = old.json; const oldMonitorIdx = oldMonitorList.findIndex(m => m.slug === monitor.slug); if (oldMonitorIdx < 0) { - return old; + return oldMonitorList; } const oldMonitor = oldMonitorList[oldMonitorIdx]!; @@ -76,10 +74,10 @@ export function OverviewTimeline({monitorList}: Props) { const right = oldMonitorList.slice(oldMonitorIdx + 1); if (newEnvList.length === 0) { - return {...old, json: [...left, ...right]}; + return [...left, ...right]; } - return {...old, json: [...left, updatedMonitor, ...right]}; + return [...left, updatedMonitor, ...right]; }); }; @@ -100,14 +98,11 @@ export function OverviewTimeline({monitorList}: Props) { return; } - const monitorListOptions = monitorListApiOptions(organization, location.query); - queryClient.setQueryData(monitorListOptions.queryKey, old => { - return old - ? { - ...old, - // TODO(davidenwang): in future only change the specifically modified environment for optimistic updates - json: old.json.map(m => (m.slug === monitor.slug ? resp : m)), - } + const queryKey = makeMonitorListQueryKey(organization, location.query); + setApiQueryData(queryClient, queryKey, oldMonitorList => { + return oldMonitorList + ? // TODO(davidenwang): in future only change the specifically modified environment for optimistic updates + oldMonitorList.map(m => (m.slug === monitor.slug ? resp : m)) : undefined; }); }; @@ -120,15 +115,12 @@ export function OverviewTimeline({monitorList}: Props) { return; } - const monitorListOptions = monitorListApiOptions(organization, location.query); - queryClient.setQueryData(monitorListOptions.queryKey, old => { - return old - ? { - ...old, - json: old.json.map(m => - m.slug === monitor.slug ? {...m, status: resp.status} : m - ), - } + const queryKey = makeMonitorListQueryKey(organization, location.query); + setApiQueryData(queryClient, queryKey, oldMonitorList => { + return oldMonitorList + ? oldMonitorList.map(m => + m.slug === monitor.slug ? {...m, status: resp.status} : m + ) : undefined; }); }; diff --git a/static/app/views/insights/crons/utils.tsx b/static/app/views/insights/crons/utils.tsx index ec11d6386f2e..d5da279e703e 100644 --- a/static/app/views/insights/crons/utils.tsx +++ b/static/app/views/insights/crons/utils.tsx @@ -2,36 +2,34 @@ import type {TickStyle} from 'sentry/components/checkInTimeline/types'; import {t, tn} from 'sentry/locale'; import type {SelectValue} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; -import {apiOptions} from 'sentry/utils/api/apiOptions'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; -import {CheckInStatus, type Monitor} from './types'; +import {CheckInStatus} from './types'; -export function monitorListApiOptions( +export function makeMonitorListQueryKey( organization: Organization, - queryParams: Partial< - Record< - 'asc' | 'cursor' | 'environment' | 'owner' | 'project' | 'query' | 'sort', - unknown - > - > + params: Record ) { - const {query, project, environment, owner, cursor, sort, asc} = queryParams; - return apiOptions.as()('/organizations/$organizationIdOrSlug/monitors/', { - path: {organizationIdOrSlug: organization.slug}, - query: { - cursor, - query, - project, - environment, - owner, - includeNew: true, - per_page: 20, - sort, - asc, + const {query, project, environment, owner, cursor, sort, asc} = params; + + return [ + getApiUrl('/organizations/$organizationIdOrSlug/monitors/', { + path: {organizationIdOrSlug: organization.slug}, + }), + { + query: { + cursor, + query, + project, + environment, + owner, + includeNew: true, + per_page: 20, + sort, + asc, + }, }, - staleTime: 0, - }); + ] as const; } export function makeMonitorDetailsQueryKey( diff --git a/static/app/views/insights/crons/views/overview.tsx b/static/app/views/insights/crons/views/overview.tsx index 356948e99497..d2247d63559d 100644 --- a/static/app/views/insights/crons/views/overview.tsx +++ b/static/app/views/insights/crons/views/overview.tsx @@ -1,6 +1,5 @@ import {Fragment} from 'react'; import styled from '@emotion/styled'; -import {useQuery} from '@tanstack/react-query'; import * as qs from 'query-string'; import {Alert} from '@sentry/scraps/alert'; @@ -29,7 +28,7 @@ import {SearchBar} from 'sentry/components/searchBar'; import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle'; import {IconAdd, IconList} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; -import {selectJsonWithHeaders} from 'sentry/utils/api/apiOptions'; +import {useApiQuery} from 'sentry/utils/queryClient'; import {decodeList, decodeScalar} from 'sentry/utils/queryString'; import {useRouteAnalyticsEventNames} from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames'; import {useRouteAnalyticsParams} from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams'; @@ -44,7 +43,8 @@ import {OwnerFilter} from 'sentry/views/insights/crons/components/ownerFilter'; import {GlobalMonitorProcessingErrors} from 'sentry/views/insights/crons/components/processingErrors/globalMonitorProcessingErrors'; import {useCronsUpsertGuideState} from 'sentry/views/insights/crons/components/useCronsUpsertGuideState'; import {MODULE_DESCRIPTION, MODULE_DOC_LINK} from 'sentry/views/insights/crons/settings'; -import {monitorListApiOptions} from 'sentry/views/insights/crons/utils'; +import type {Monitor} from 'sentry/views/insights/crons/types'; +import {makeMonitorListQueryKey} from 'sentry/views/insights/crons/utils'; const CronsListPageHeader = HookOrDefault({ hookName: 'component:crons-list-page-header', @@ -57,24 +57,21 @@ function CronsOverview() { const {guideVisible} = useCronsUpsertGuideState(); const project = decodeList(location.query?.project); - const {data, isPending, refetch} = useQuery({ - ...monitorListApiOptions(organization, { - cursor: location.query.cursor, - query: location.query.query, - project: location.query.project, - environment: location.query.environment, - owner: location.query.owner, - sort: location.query.sort, - asc: location.query.asc, - }), - select: selectJsonWithHeaders, + const queryKey = makeMonitorListQueryKey(organization, location.query); + + const { + data: monitorList, + getResponseHeader: monitorListHeaders, + isPending, + refetch, + } = useApiQuery(queryKey, { + staleTime: 0, }); - const monitorList = data?.json; useRouteAnalyticsEventNames('monitors.page_viewed', 'Monitors: Page Viewed'); useRouteAnalyticsParams({empty_state: !monitorList || monitorList.length === 0}); - const monitorListPageLinks = data?.headers.Link; + const monitorListPageLinks = monitorListHeaders?.('Link'); const handleSearch = (query: string) => { const currentQuery = {...location.query, cursor: undefined}; diff --git a/static/app/views/insights/database/components/tables/queriesTable.tsx b/static/app/views/insights/database/components/tables/queriesTable.tsx index 8ef2bc723237..de655ebed312 100644 --- a/static/app/views/insights/database/components/tables/queriesTable.tsx +++ b/static/app/views/insights/database/components/tables/queriesTable.tsx @@ -54,7 +54,7 @@ const COLUMN_ORDER: Column[] = [ width: COL_WIDTH_UNDEFINED, }, { - key: 'avg(span.self_time)', + key: `avg(span.self_time)`, name: DataTitles.avg, width: COL_WIDTH_UNDEFINED, }, diff --git a/static/app/views/insights/database/utils/formatMongoDBQuery.spec.tsx b/static/app/views/insights/database/utils/formatMongoDBQuery.spec.tsx index db34e81d3579..d4f597db12a6 100644 --- a/static/app/views/insights/database/utils/formatMongoDBQuery.spec.tsx +++ b/static/app/views/insights/database/utils/formatMongoDBQuery.spec.tsx @@ -88,8 +88,7 @@ describe('formatMongoDBQuery', () => { }); it('handles truncated MongoDB query strings by repairing the JSON', () => { - const query = - '{"_id":{},"test":"?","insert":"some_collection","address":"?","details":{"email":"?","nam*'; + const query = `{"_id":{},"test":"?","insert":"some_collection","address":"?","details":{"email":"?","nam*`; const tokenizedQuery = formatMongoDBQuery(query, 'insert'); render({tokenizedQuery}); @@ -104,8 +103,7 @@ describe('formatMongoDBQuery', () => { }); it('properly handles formatting MongoDB queries when the operation entry is the last entry', () => { - const query = - '{"first_key":"first_value","second_key":"second_value","findOne":"my_collection"}'; + const query = `{"first_key":"first_value","second_key":"second_value","findOne":"my_collection"}`; const tokenizedQuery = formatMongoDBQuery(query, 'findOne'); render({tokenizedQuery}); diff --git a/static/app/views/insights/database/views/databaseLandingPage.spec.tsx b/static/app/views/insights/database/views/databaseLandingPage.spec.tsx index 5bf961673ce0..23c718dfce33 100644 --- a/static/app/views/insights/database/views/databaseLandingPage.spec.tsx +++ b/static/app/views/insights/database/views/databaseLandingPage.spec.tsx @@ -19,7 +19,7 @@ describe('DatabaseLandingPage', () => { pathname: `/organizations/${organization.slug}/insights/backend/database/`, query: {statsPeriod: '10d'}, }, - route: '/organizations/:orgId/insights/backend/database/', + route: `/organizations/:orgId/insights/backend/database/`, }; let spanListRequestMock: jest.Mock; diff --git a/static/app/views/insights/database/views/databaseSpanSummaryPage.spec.tsx b/static/app/views/insights/database/views/databaseSpanSummaryPage.spec.tsx index 9de786f261f4..4a415f756f71 100644 --- a/static/app/views/insights/database/views/databaseSpanSummaryPage.spec.tsx +++ b/static/app/views/insights/database/views/databaseSpanSummaryPage.spec.tsx @@ -19,7 +19,7 @@ describe('DatabaseSpanSummaryPage', () => { const groupId = '1756baf8fd19c116'; const initialRouterConfig = { - route: '/organizations/:orgId/insights/backend/database/spans/span/:groupId/', + route: `/organizations/:orgId/insights/backend/database/spans/span/:groupId/`, location: { pathname: `/organizations/${organization.slug}/insights/backend/database/spans/span/${groupId}/`, query: {statsPeriod: '10d', transactionsCursor: '0:25:0'}, @@ -138,7 +138,7 @@ describe('DatabaseSpanSummaryPage', () => { }); MockApiClient.addMockResponse({ - url: '/projects/org-slug//releases/1.0.0/', + url: `/projects/org-slug//releases/1.0.0/`, method: 'GET', body: [], }); diff --git a/static/app/views/insights/http/components/httpSamplesPanel.spec.tsx b/static/app/views/insights/http/components/httpSamplesPanel.spec.tsx index f3e59edfc660..a1196d5aa8f8 100644 --- a/static/app/views/insights/http/components/httpSamplesPanel.spec.tsx +++ b/static/app/views/insights/http/components/httpSamplesPanel.spec.tsx @@ -20,7 +20,7 @@ describe('HTTPSamplesPanel', () => { let eventsRequestMock: jest.Mock; const basePath = `/organizations/${organization.slug}/insights/backend/http/domains/`; - const baseRoute = '/organizations/:orgId/insights/backend/http/domains/'; + const baseRoute = `/organizations/:orgId/insights/backend/http/domains/`; beforeEach(() => { jest.clearAllMocks(); @@ -120,7 +120,7 @@ describe('HTTPSamplesPanel', () => { body: { timeSeries: [ TimeSeriesFixture({ - yAxis: 'epm()', + yAxis: `epm()`, groupBy: [{key: SpanFields.SPAN_STATUS_CODE, value: '301'}], values: [ {timestamp: 1699907700000, value: 7810.2}, @@ -128,7 +128,7 @@ describe('HTTPSamplesPanel', () => { ], }), TimeSeriesFixture({ - yAxis: 'epm()', + yAxis: `epm()`, groupBy: [{key: SpanFields.SPAN_STATUS_CODE, value: '304'}], values: [ {timestamp: 1699907700000, value: 2701.5}, diff --git a/static/app/views/insights/http/components/httpSamplesPanel.tsx b/static/app/views/insights/http/components/httpSamplesPanel.tsx index 44af47157c63..0dbba6298c2b 100644 --- a/static/app/views/insights/http/components/httpSamplesPanel.tsx +++ b/static/app/views/insights/http/components/httpSamplesPanel.tsx @@ -214,7 +214,7 @@ export function HTTPSamplesPanel() { } = useFetchSpanTimeSeries( { query: search, - yAxis: ['avg(span.self_time)'], + yAxis: [`avg(span.self_time)`], enabled: isPanelOpen && query.panel === 'duration', }, Referrer.SAMPLES_PANEL_DURATION_CHART @@ -390,21 +390,21 @@ export function HTTPSamplesPanel() { diff --git a/static/app/views/insights/http/components/tables/domainTransactionsTable.tsx b/static/app/views/insights/http/components/tables/domainTransactionsTable.tsx index 4e720ba457bf..52de5bdfed59 100644 --- a/static/app/views/insights/http/components/tables/domainTransactionsTable.tsx +++ b/static/app/views/insights/http/components/tables/domainTransactionsTable.tsx @@ -59,22 +59,22 @@ const COLUMN_ORDER: Column[] = [ width: COL_WIDTH_UNDEFINED, }, { - key: 'http_response_rate(3)', + key: `http_response_rate(3)`, name: t('3XXs'), width: 50, }, { - key: 'http_response_rate(4)', + key: `http_response_rate(4)`, name: t('4XXs'), width: 50, }, { - key: 'http_response_rate(5)', + key: `http_response_rate(5)`, name: t('5XXs'), width: 50, }, { - key: 'avg(span.self_time)', + key: `avg(span.self_time)`, name: DataTitles.avg, width: COL_WIDTH_UNDEFINED, }, diff --git a/static/app/views/insights/http/components/tables/domainsTable.tsx b/static/app/views/insights/http/components/tables/domainsTable.tsx index ea28094548b3..5aaa8fbea9aa 100644 --- a/static/app/views/insights/http/components/tables/domainsTable.tsx +++ b/static/app/views/insights/http/components/tables/domainsTable.tsx @@ -64,22 +64,22 @@ const COLUMN_ORDER: Column[] = [ width: COL_WIDTH_UNDEFINED, }, { - key: 'http_response_rate(3)', + key: `http_response_rate(3)`, name: t('3XXs'), width: 50, }, { - key: 'http_response_rate(4)', + key: `http_response_rate(4)`, name: t('4XXs'), width: 50, }, { - key: 'http_response_rate(5)', + key: `http_response_rate(5)`, name: t('5XXs'), width: 50, }, { - key: 'avg(span.self_time)', + key: `avg(span.self_time)`, name: DataTitles.avg, width: COL_WIDTH_UNDEFINED, }, diff --git a/static/app/views/insights/http/queries/useSpanSamples.spec.tsx b/static/app/views/insights/http/queries/useSpanSamples.spec.tsx index a0ee7e8e72c4..d89d09993a5a 100644 --- a/static/app/views/insights/http/queries/useSpanSamples.spec.tsx +++ b/static/app/views/insights/http/queries/useSpanSamples.spec.tsx @@ -120,7 +120,7 @@ describe('useSpanSamples', () => { project: [], dataset: 'spans', sampling: 'NORMAL', - query: 'span.group:221aa7ebd216 release:0.0.1', + query: `span.group:221aa7ebd216 release:0.0.1`, referrer: 'api-spec', statsPeriod: '10d', environment: ['prod'], diff --git a/static/app/views/insights/http/queries/useSpanSamples.tsx b/static/app/views/insights/http/queries/useSpanSamples.tsx index abf93b9b4434..40b7e82de2d3 100644 --- a/static/app/views/insights/http/queries/useSpanSamples.tsx +++ b/static/app/views/insights/http/queries/useSpanSamples.tsx @@ -55,7 +55,7 @@ export const useSpanSamples = ( meta: EventsMetaType; }>( [ - getApiUrl('/organizations/$organizationIdOrSlug/spans-samples/', { + getApiUrl(`/organizations/$organizationIdOrSlug/spans-samples/`, { path: {organizationIdOrSlug: organization.slug}, }), { diff --git a/static/app/views/insights/http/views/httpDomainSummaryPage.spec.tsx b/static/app/views/insights/http/views/httpDomainSummaryPage.spec.tsx index b7479f9e019d..61bf04355b44 100644 --- a/static/app/views/insights/http/views/httpDomainSummaryPage.spec.tsx +++ b/static/app/views/insights/http/views/httpDomainSummaryPage.spec.tsx @@ -31,7 +31,7 @@ describe('HTTPDomainSummaryPage', () => { pathname: `/organizations/${organization.slug}/insights/backend/http/domains/`, query: {domain: '*.sentry.dev', statsPeriod: '10d', transactionsCursor: '0:20:0'}, }, - route: '/organizations/:orgId/insights/backend/http/domains/', + route: `/organizations/:orgId/insights/backend/http/domains/`, }; jest.mocked(useReleaseStats).mockReturnValue({ @@ -57,7 +57,7 @@ describe('HTTPDomainSummaryPage', () => { }); regionFilterRequestMock = MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events/', + url: `/organizations/org-slug/events/`, method: 'GET', match: [ MockApiClient.matchQuery({ diff --git a/static/app/views/insights/http/views/httpDomainSummaryPage.tsx b/static/app/views/insights/http/views/httpDomainSummaryPage.tsx index 982ea8096468..882afc032a4c 100644 --- a/static/app/views/insights/http/views/httpDomainSummaryPage.tsx +++ b/static/app/views/insights/http/views/httpDomainSummaryPage.tsx @@ -198,21 +198,21 @@ export function HTTPDomainSummaryPage() { diff --git a/static/app/views/insights/http/views/httpLandingPage.spec.tsx b/static/app/views/insights/http/views/httpLandingPage.spec.tsx index 788855b217fc..ea4966f3c58f 100644 --- a/static/app/views/insights/http/views/httpLandingPage.spec.tsx +++ b/static/app/views/insights/http/views/httpLandingPage.spec.tsx @@ -72,7 +72,7 @@ describe('HTTPLandingPage', () => { }); regionFilterRequestMock = MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events/', + url: `/organizations/org-slug/events/`, method: 'GET', match: [ MockApiClient.matchQuery({ diff --git a/static/app/views/insights/mobile/appStarts/components/eventSamples.spec.tsx b/static/app/views/insights/mobile/appStarts/components/eventSamples.spec.tsx index ea34be5f70bb..8233c9523cf6 100644 --- a/static/app/views/insights/mobile/appStarts/components/eventSamples.spec.tsx +++ b/static/app/views/insights/mobile/appStarts/components/eventSamples.spec.tsx @@ -35,7 +35,7 @@ describe('ScreenLoadEventSamples', () => { }) ); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events/', + url: `/organizations/org-slug/events/`, method: 'GET', match: [ MockApiClient.matchQuery({ diff --git a/static/app/views/insights/mobile/appStarts/components/spanOpSelector.tsx b/static/app/views/insights/mobile/appStarts/components/spanOpSelector.tsx index a33b554adefe..7e0a5c0164a4 100644 --- a/static/app/views/insights/mobile/appStarts/components/spanOpSelector.tsx +++ b/static/app/views/insights/mobile/appStarts/components/spanOpSelector.tsx @@ -51,7 +51,7 @@ export function SpanOpSelector({transaction, primaryRelease}: Props) { 'has:span.description', 'transaction.op:[ui.load,navigation]', `transaction:${transaction}`, - 'has:ttid', + `has:ttid`, `span.op:[${APP_START_SPANS.join(',')}]`, `app_start_type:${appStartType}`, ]); diff --git a/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.spec.tsx b/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.spec.tsx index aa0cf996cad0..c49f88a5c767 100644 --- a/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.spec.tsx +++ b/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.spec.tsx @@ -22,7 +22,7 @@ describe('Screen Summary', () => { [SpanFields.APP_START_TYPE]: 'cold', }, }, - route: '/organizations/:orgId/insights/screens/spans/', + route: `/organizations/:orgId/insights/screens/spans/`, }; describe('Native Project', () => { diff --git a/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.tsx b/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.tsx index d31cd9020b84..2b15022a4022 100644 --- a/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.tsx +++ b/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.tsx @@ -91,7 +91,7 @@ export function ScreenSummaryContentPage() { `count_if(release,equals,${primaryRelease})`, ]; } else { - fields = ['avg(span.duration)', 'count()']; + fields = [`avg(span.duration)`, `count()`]; } return ( diff --git a/static/app/views/insights/mobile/common/components/spanSamplesPanelContainer.spec.tsx b/static/app/views/insights/mobile/common/components/spanSamplesPanelContainer.spec.tsx index 3007b5c85778..94c2169ac816 100644 --- a/static/app/views/insights/mobile/common/components/spanSamplesPanelContainer.spec.tsx +++ b/static/app/views/insights/mobile/common/components/spanSamplesPanelContainer.spec.tsx @@ -19,7 +19,7 @@ describe('SpanSamplesContainer', () => { body: [], }); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events-timeseries/', + url: `/organizations/org-slug/events-timeseries/`, method: 'GET', body: [], }); diff --git a/static/app/views/insights/mobile/common/components/tables/samplesTables.spec.tsx b/static/app/views/insights/mobile/common/components/tables/samplesTables.spec.tsx index dffac852d343..df74f8361747 100644 --- a/static/app/views/insights/mobile/common/components/tables/samplesTables.spec.tsx +++ b/static/app/views/insights/mobile/common/components/tables/samplesTables.spec.tsx @@ -13,7 +13,7 @@ jest.mocked(useReleaseSelection).mockReturnValue({ describe('SamplesTables', () => { beforeEach(() => { MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events/', + url: `/organizations/org-slug/events/`, method: 'GET', match: [ MockApiClient.matchQuery({ @@ -29,7 +29,7 @@ describe('SamplesTables', () => { }); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events/', + url: `/organizations/org-slug/events/`, method: 'GET', match: [ MockApiClient.matchQuery({ diff --git a/static/app/views/insights/mobile/screenload/components/eventSamples.spec.tsx b/static/app/views/insights/mobile/screenload/components/eventSamples.spec.tsx index 6544493504f8..32926ecb652d 100644 --- a/static/app/views/insights/mobile/screenload/components/eventSamples.spec.tsx +++ b/static/app/views/insights/mobile/screenload/components/eventSamples.spec.tsx @@ -40,7 +40,7 @@ describe('ScreenLoadEventSamples', () => { isLoading: false, }); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events/', + url: `/organizations/org-slug/events/`, method: 'GET', match: [ MockApiClient.matchQuery({ diff --git a/static/app/views/insights/mobile/screenload/components/eventSamples.tsx b/static/app/views/insights/mobile/screenload/components/eventSamples.tsx index 6d29723df14e..f04a76b545c8 100644 --- a/static/app/views/insights/mobile/screenload/components/eventSamples.tsx +++ b/static/app/views/insights/mobile/screenload/components/eventSamples.tsx @@ -44,7 +44,7 @@ export function ScreenLoadEventSamples({ const searchQuery = useMemo(() => { const baseFilters = [ 'span.op:[ui.load,navigation]', - 'is_transaction:true', + `is_transaction:true`, `transaction:${transaction}`, ]; diff --git a/static/app/views/insights/mobile/screenload/components/platformSelector.spec.tsx b/static/app/views/insights/mobile/screenload/components/platformSelector.spec.tsx index 569d00be8133..ef204eb1ecc3 100644 --- a/static/app/views/insights/mobile/screenload/components/platformSelector.spec.tsx +++ b/static/app/views/insights/mobile/screenload/components/platformSelector.spec.tsx @@ -12,7 +12,7 @@ describe('PlatformSelector', () => { pathname: '/organizations/org-slug/insights/mobile/screen-loads/', query: {}, }, - route: '/organizations/:orgId/insights/mobile/screen-loads/', + route: `/organizations/:orgId/insights/mobile/screen-loads/`, }; it('renders with iOS and Android options', () => { diff --git a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx index 837ae6f79efd..8c80fa2132c5 100644 --- a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx +++ b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx @@ -14,7 +14,7 @@ describe('EventSamplesTable', () => { statsPeriod: '99d', }, }, - route: '/organizations/:orgId/insights/mobile/screen-loads/', + route: `/organizations/:orgId/insights/mobile/screen-loads/`, }; const mockLocation = LocationFixture({ @@ -36,7 +36,7 @@ describe('EventSamplesTable', () => { mockEventView = EventView.fromNewQueryWithLocation(mockQuery, mockLocation); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events/', + url: `/organizations/org-slug/events/`, method: 'GET', match: [ MockApiClient.matchQuery({ diff --git a/static/app/views/insights/mobile/screens/components/screensOverview.tsx b/static/app/views/insights/mobile/screens/components/screensOverview.tsx index dbd52749eb2e..99ffb4c6bfbe 100644 --- a/static/app/views/insights/mobile/screens/components/screensOverview.tsx +++ b/static/app/views/insights/mobile/screens/components/screensOverview.tsx @@ -48,14 +48,14 @@ const getQueryString = ( const fields = [ SpanFields.PROJECT_ID, SpanFields.TRANSACTION, - 'count()', + `count()`, 'avg(measurements.app_start_cold)', 'avg(measurements.app_start_warm)', - 'avg(measurements.time_to_initial_display)', - 'avg(measurements.time_to_full_display)', - 'division(mobile.slow_frames,mobile.total_frames)', - 'division(mobile.frozen_frames,mobile.total_frames)', - 'avg(mobile.frames_delay)', + `avg(measurements.time_to_initial_display)`, + `avg(measurements.time_to_full_display)`, + `division(mobile.slow_frames,mobile.total_frames)`, + `division(mobile.frozen_frames,mobile.total_frames)`, + `avg(mobile.frames_delay)`, ] as const satisfies SpanProperty[]; export function ScreensOverview() { diff --git a/static/app/views/insights/mobile/screens/components/screensOverviewTable.tsx b/static/app/views/insights/mobile/screens/components/screensOverviewTable.tsx index 34330a7c9b14..19b21d9b2e67 100644 --- a/static/app/views/insights/mobile/screens/components/screensOverviewTable.tsx +++ b/static/app/views/insights/mobile/screens/components/screensOverviewTable.tsx @@ -48,23 +48,23 @@ export function ScreensOverviewTable({data, eventView, isLoading, pageLinks}: Pr const columnNameMap = { transaction: t('Screen'), - ['count()']: t('Screen Loads'), - ['division(mobile.slow_frames,mobile.total_frames)']: t('Slow Frame Rate'), - ['division(mobile.frozen_frames,mobile.total_frames)']: t('Frozen Frame Rate'), - ['avg(mobile.frames_delay)']: t('Frame Delay'), - ['avg(measurements.time_to_initial_display)']: t('TTID'), - ['avg(measurements.time_to_full_display)']: t('TTFD'), + [`count()`]: t('Screen Loads'), + [`division(mobile.slow_frames,mobile.total_frames)`]: t('Slow Frame Rate'), + [`division(mobile.frozen_frames,mobile.total_frames)`]: t('Frozen Frame Rate'), + [`avg(mobile.frames_delay)`]: t('Frame Delay'), + [`avg(measurements.time_to_initial_display)`]: t('TTID'), + [`avg(measurements.time_to_full_display)`]: t('TTFD'), ['avg(measurements.app_start_warm)']: t('Warm Start'), ['avg(measurements.app_start_cold)']: t('Cold Start'), }; const columnTooltipMap = { ['avg(measurements.app_start_cold)']: t('Average Cold Start duration'), - ['avg(measurements.app_start_warm)']: t('Average Warm Start duration'), - ['division(mobile.slow_frames,mobile.total_frames)']: t('Slow Frame Rate'), - ['division(mobile.frozen_frames,mobile.total_frames)']: t('Frozen Frame Rate'), - ['avg(mobile.frames_delay)']: t('Average Frame Delay'), - ['avg(measurements.time_to_initial_display)']: t('Average Time to Initial Display'), - ['avg(measurements.time_to_full_display)']: t('Average Time to Full Display'), + [`avg(measurements.app_start_warm)`]: t('Average Warm Start duration'), + [`division(mobile.slow_frames,mobile.total_frames)`]: t('Slow Frame Rate'), + [`division(mobile.frozen_frames,mobile.total_frames)`]: t('Frozen Frame Rate'), + [`avg(mobile.frames_delay)`]: t('Average Frame Delay'), + [`avg(measurements.time_to_initial_display)`]: t('Average Time to Initial Display'), + [`avg(measurements.time_to_full_display)`]: t('Average Time to Full Display'), }; function renderBodyCell(column: any, row: any): React.ReactNode | null { @@ -85,7 +85,7 @@ export function ScreensOverviewTable({data, eventView, isLoading, pageLinks}: Pr return ( - + {row.transaction} @@ -134,16 +134,16 @@ export function ScreensOverviewTable({data, eventView, isLoading, pageLinks}: Pr 'transaction', 'avg(measurements.app_start_cold)', 'avg(measurements.app_start_warm)', - 'division(mobile.slow_frames,mobile.total_frames)', - 'division(mobile.frozen_frames,mobile.total_frames)', - 'avg(mobile.frames_delay)', - 'avg(measurements.time_to_initial_display)', - 'avg(measurements.time_to_full_display)', - 'count()', + `division(mobile.slow_frames,mobile.total_frames)`, + `division(mobile.frozen_frames,mobile.total_frames)`, + `avg(mobile.frames_delay)`, + `avg(measurements.time_to_initial_display)`, + `avg(measurements.time_to_full_display)`, + `count()`, ]} defaultSort={[ { - key: 'count()', + key: `count()`, order: 'desc', }, ]} diff --git a/static/app/views/insights/mobile/screens/views/screensLandingPage.spec.tsx b/static/app/views/insights/mobile/screens/views/screensLandingPage.spec.tsx index b17e8e546f01..3fbc37b142b3 100644 --- a/static/app/views/insights/mobile/screens/views/screensLandingPage.spec.tsx +++ b/static/app/views/insights/mobile/screens/views/screensLandingPage.spec.tsx @@ -31,7 +31,7 @@ describe('Screens Landing Page', () => { project: project.id, }, }, - route: '/organizations/:orgId/insights/mobile-vitals/', + route: `/organizations/:orgId/insights/mobile-vitals/`, }; ProjectsStore.loadInitialData([project]); diff --git a/static/app/views/insights/mobile/screens/views/screensLandingPage.tsx b/static/app/views/insights/mobile/screens/views/screensLandingPage.tsx index 8e86349bb4c3..72d5789750ea 100644 --- a/static/app/views/insights/mobile/screens/views/screensLandingPage.tsx +++ b/static/app/views/insights/mobile/screens/views/screensLandingPage.tsx @@ -125,7 +125,7 @@ function ScreensLandingPage() { 'https://docs.sentry.io/platforms/android/tracing/instrumentation/automatic-instrumentation/#slow-and-frozen-frames', iOS: 'https://docs.sentry.io/platforms/apple/guides/ios/tracing/instrumentation/automatic-instrumentation/#slow-and-frozen-frames', }, - field: 'division(mobile.slow_frames,mobile.total_frames)' as const, + field: `division(mobile.slow_frames,mobile.total_frames)` as const, dataset: 'spansMetrics', getStatus: getDefaultMetricPerformance, }, @@ -142,7 +142,7 @@ function ScreensLandingPage() { 'https://docs.sentry.io/platforms/android/tracing/instrumentation/automatic-instrumentation/#slow-and-frozen-frames', iOS: 'https://docs.sentry.io/platforms/apple/guides/ios/tracing/instrumentation/automatic-instrumentation/#slow-and-frozen-frames', }, - field: 'division(mobile.frozen_frames,mobile.total_frames)' as const, + field: `division(mobile.frozen_frames,mobile.total_frames)` as const, dataset: 'spansMetrics', getStatus: getDefaultMetricPerformance, }, @@ -161,7 +161,7 @@ function ScreensLandingPage() { 'https://docs.sentry.io/platforms/android/tracing/instrumentation/automatic-instrumentation/#slow-and-frozen-frames', iOS: 'https://docs.sentry.io/platforms/apple/guides/ios/tracing/instrumentation/automatic-instrumentation/#slow-and-frozen-frames', }, - field: 'avg(mobile.frames_delay)' as const, + field: `avg(mobile.frames_delay)` as const, dataset: 'spansMetrics', getStatus: getDefaultMetricPerformance, }, @@ -179,7 +179,7 @@ function ScreensLandingPage() { 'https://docs.sentry.io/platforms/android/tracing/instrumentation/automatic-instrumentation/#time-to-initial-display', iOS: 'https://docs.sentry.io/platforms/apple/features/experimental-features/', }, - field: 'avg(measurements.time_to_initial_display)' as const, + field: `avg(measurements.time_to_initial_display)` as const, dataset: 'metrics', getStatus: getDefaultMetricPerformance, }, @@ -197,7 +197,7 @@ function ScreensLandingPage() { 'https://docs.sentry.io/platforms/android/tracing/instrumentation/automatic-instrumentation/#time-to-full-display', iOS: 'https://docs.sentry.io/platforms/apple/features/experimental-features/', }, - field: 'avg(measurements.time_to_full_display)' as const, + field: `avg(measurements.time_to_full_display)` as const, dataset: 'metrics', getStatus: getDefaultMetricPerformance, }, diff --git a/static/app/views/insights/pages/agents/components/llmCallsWidget.tsx b/static/app/views/insights/pages/agents/components/llmCallsWidget.tsx index d0bded9f982f..e1629cd5331a 100644 --- a/static/app/views/insights/pages/agents/components/llmCallsWidget.tsx +++ b/static/app/views/insights/pages/agents/components/llmCallsWidget.tsx @@ -151,7 +151,7 @@ export function LLMCallsWidget() { ], groupBy: ['gen_ai.request.model'], query: fullQuery, - sort: '-count(span.duration)', + sort: `-count(span.duration)`, interval: pageFilterChartParams.interval, }} onOpenFullScreen={() => { diff --git a/static/app/views/insights/pages/agents/components/modelCostWidget.tsx b/static/app/views/insights/pages/agents/components/modelCostWidget.tsx index 9e01c3d8c086..4adb472b617f 100644 --- a/static/app/views/insights/pages/agents/components/modelCostWidget.tsx +++ b/static/app/views/insights/pages/agents/components/modelCostWidget.tsx @@ -151,7 +151,7 @@ export function ModelCostWidget() { ], groupBy: ['gen_ai.request.model'], query: fullQuery, - sort: '-sum(gen_ai.cost.total_tokens)', + sort: `-sum(gen_ai.cost.total_tokens)`, interval: pageFilterChartParams.interval, }} onOpenFullScreen={() => { diff --git a/static/app/views/insights/pages/agents/components/tokenTypesWidget.tsx b/static/app/views/insights/pages/agents/components/tokenTypesWidget.tsx index 9390e651df77..7442d956d356 100644 --- a/static/app/views/insights/pages/agents/components/tokenTypesWidget.tsx +++ b/static/app/views/insights/pages/agents/components/tokenTypesWidget.tsx @@ -249,7 +249,7 @@ export function TokenTypesWidget() { }, ], query: fullQuery, - sort: '-sum(gen_ai.usage.input_tokens)', + sort: `-sum(gen_ai.usage.input_tokens)`, interval: pageFilterChartParams.interval, }} onOpenFullScreen={() => { diff --git a/static/app/views/insights/pages/agents/components/tokenUsageWidget.tsx b/static/app/views/insights/pages/agents/components/tokenUsageWidget.tsx index e52ae039973e..bd649f4874f4 100644 --- a/static/app/views/insights/pages/agents/components/tokenUsageWidget.tsx +++ b/static/app/views/insights/pages/agents/components/tokenUsageWidget.tsx @@ -154,7 +154,7 @@ export function TokenUsageWidget() { ], groupBy: ['gen_ai.request.model'], query: fullQuery, - sort: '-sum(gen_ai.usage.total_tokens)', + sort: `-sum(gen_ai.usage.total_tokens)`, interval: pageFilterChartParams.interval, }} onOpenFullScreen={() => { diff --git a/static/app/views/insights/pages/agents/components/toolCallsWidget.tsx b/static/app/views/insights/pages/agents/components/toolCallsWidget.tsx index 7d13a9bef7fc..4005443a4468 100644 --- a/static/app/views/insights/pages/agents/components/toolCallsWidget.tsx +++ b/static/app/views/insights/pages/agents/components/toolCallsWidget.tsx @@ -149,7 +149,7 @@ export function ToolCallsWidget() { ], groupBy: ['gen_ai.tool.name'], query: fullQuery, - sort: '-count(span.duration)', + sort: `-count(span.duration)`, interval: pageFilterChartParams.interval, }} onOpenFullScreen={() => { diff --git a/static/app/views/insights/pages/agents/components/toolErrorsWidget.tsx b/static/app/views/insights/pages/agents/components/toolErrorsWidget.tsx index 8ed7d485961b..5c642d650c51 100644 --- a/static/app/views/insights/pages/agents/components/toolErrorsWidget.tsx +++ b/static/app/views/insights/pages/agents/components/toolErrorsWidget.tsx @@ -151,7 +151,7 @@ export function ToolErrorsWidget() { ], groupBy: ['gen_ai.tool.name'], query: fullQuery, - sort: '-count(span.duration)', + sort: `-count(span.duration)`, interval: pageFilterChartParams.interval, }} onOpenFullScreen={() => { diff --git a/static/app/views/insights/pages/agents/components/tracesTable.tsx b/static/app/views/insights/pages/agents/components/tracesTable.tsx index 2add559a6281..64af2928fa59 100644 --- a/static/app/views/insights/pages/agents/components/tracesTable.tsx +++ b/static/app/views/insights/pages/agents/components/tracesTable.tsx @@ -136,7 +136,7 @@ export function TracesTable({ const tracesRequest = useTraces({ query: combinedQuery, - sort: '-timestamp', + sort: `-timestamp`, keepPreviousData: true, cursor, limit, diff --git a/static/app/views/insights/pages/agents/utils/query.tsx b/static/app/views/insights/pages/agents/utils/query.tsx index 7e9022ccfdfd..20a4bbd916ae 100644 --- a/static/app/views/insights/pages/agents/utils/query.tsx +++ b/static/app/views/insights/pages/agents/utils/query.tsx @@ -14,7 +14,7 @@ export function getIsAiGenerationSpan(genAiOpType: string | undefined) { } export function getHasAiSpansFilter() { - return 'has:gen_ai.operation.type'; + return `has:gen_ai.operation.type`; } export const getAgentRunsFilter = ({negated = false}: {negated?: boolean} = {}) => { @@ -22,15 +22,15 @@ export const getAgentRunsFilter = ({negated = false}: {negated?: boolean} = {}) }; export const getToolSpansFilter = () => { - return 'gen_ai.operation.type:tool'; + return `gen_ai.operation.type:tool`; }; export const getAgentAndAIClientFilter = () => { - return 'gen_ai.operation.type:[agent, ai_client]'; + return `gen_ai.operation.type:[agent, ai_client]`; }; export const getAIGenerationsFilter = () => { - return 'gen_ai.operation.type:ai_client'; + return `gen_ai.operation.type:ai_client`; }; /** diff --git a/static/app/views/insights/pages/agents/utils/tokenBreakdown.spec.ts b/static/app/views/insights/pages/agents/utils/tokenBreakdown.spec.ts deleted file mode 100644 index 60331ae1bb16..000000000000 --- a/static/app/views/insights/pages/agents/utils/tokenBreakdown.spec.ts +++ /dev/null @@ -1,160 +0,0 @@ -import {getTokenBreakdown} from './tokenBreakdown'; - -describe('getTokenBreakdown', () => { - it('returns input as netNewInput when there are no cached tokens', () => { - const result = getTokenBreakdown({ - inputTokens: 100, - cachedTokens: 0, - outputTokens: 50, - reasoningTokens: 0, - totalTokens: 150, - }); - - expect(result).toEqual({ - netNewInput: 100, - cached: 0, - output: 50, - total: 150, - }); - }); - - it('separates cached from input when cached is included (OTel convention)', () => { - // OTel: inputTokens=100 includes 80 cached, output=50, total=150 - const result = getTokenBreakdown({ - inputTokens: 100, - cachedTokens: 80, - outputTokens: 50, - reasoningTokens: 0, - totalTokens: 150, - }); - - expect(result).toEqual({ - netNewInput: 20, - cached: 80, - output: 50, - total: 150, - }); - }); - - it('handles providers that report input exclusive of cached', () => { - // Some providers: inputTokens=30 (net new only), cached=80, output=90, total=200 - // getAdjustedInput detects that 30+90+80=200 matches total, so adjusts input to 110 - const result = getTokenBreakdown({ - inputTokens: 30, - cachedTokens: 80, - outputTokens: 90, - reasoningTokens: 0, - totalTokens: 200, - }); - - expect(result).toEqual({ - netNewInput: 30, - cached: 80, - output: 90, - total: 200, - }); - }); - - it('clamps netNewInput to 0 when cached exceeds input (inconsistent data)', () => { - // Bad data: provider says cached=80 but input=50, and heuristic decides cached is included - // input+output=100 is closer to total=100 than input+output+cached=180 - const result = getTokenBreakdown({ - inputTokens: 50, - cachedTokens: 80, - outputTokens: 50, - reasoningTokens: 0, - totalTokens: 100, - }); - - expect(result.netNewInput).toBeGreaterThanOrEqual(0); - }); - - it('folds reasoning into output tokens (OTel convention)', () => { - // OTel: outputTokens=100 includes 30 reasoning, total=200 - const result = getTokenBreakdown({ - inputTokens: 100, - cachedTokens: 0, - outputTokens: 100, - reasoningTokens: 30, - totalTokens: 200, - }); - - expect(result).toEqual({ - netNewInput: 100, - cached: 0, - output: 100, - total: 200, - }); - }); - - it('adjusts output when reasoning is reported separately', () => { - // Provider reports output=70 exclusive of reasoning=30, total=200 - // getAdjustedOutput detects that 100+70+30=200 matches total - const result = getTokenBreakdown({ - inputTokens: 100, - cachedTokens: 0, - outputTokens: 70, - reasoningTokens: 30, - totalTokens: 200, - }); - - expect(result).toEqual({ - netNewInput: 100, - cached: 0, - output: 100, - total: 200, - }); - }); - - it('handles both cached and reasoning adjustments together', () => { - // input=20 (net new), cached=80 (exclusive), output=50 (exclusive of reasoning=30), total=180 - const result = getTokenBreakdown({ - inputTokens: 20, - cachedTokens: 80, - outputTokens: 50, - reasoningTokens: 30, - totalTokens: 180, - }); - - expect(result).toEqual({ - netNewInput: 20, - cached: 80, - output: 80, - total: 180, - }); - }); - - it('treats NaN cached as 0', () => { - const result = getTokenBreakdown({ - inputTokens: 100, - cachedTokens: NaN, - outputTokens: 50, - reasoningTokens: 0, - totalTokens: 150, - }); - - expect(result).toEqual({ - netNewInput: 100, - cached: 0, - output: 50, - total: 150, - }); - }); - - it('treats NaN reasoning as 0', () => { - const result = getTokenBreakdown({ - inputTokens: 100, - cachedTokens: 0, - outputTokens: 50, - reasoningTokens: NaN, - totalTokens: 150, - }); - - expect(result).toEqual({ - netNewInput: 100, - cached: 0, - output: 50, - total: 150, - }); - }); -}); diff --git a/static/app/views/insights/pages/agents/utils/tokenBreakdown.ts b/static/app/views/insights/pages/agents/utils/tokenBreakdown.ts deleted file mode 100644 index f8bcfbd070d0..000000000000 --- a/static/app/views/insights/pages/agents/utils/tokenBreakdown.ts +++ /dev/null @@ -1,89 +0,0 @@ -export interface TokenBreakdown { - cached: number; - netNewInput: number; - output: number; - total: number; -} - -/** - * Computes a display-ready token breakdown from raw span attributes. - * - * Per OTel conventions, `inputTokens` includes cached and `outputTokens` - * includes reasoning. Some providers don't follow this, so we detect the - * gap and adjust as a fallback (see `getAdjustedInput` / `getAdjustedOutput`). - * - * Returns net-new input (excluding cached), cached, output (including - * reasoning), and total. - */ -export function getTokenBreakdown({ - inputTokens, - cachedTokens, - outputTokens, - reasoningTokens, - totalTokens, -}: { - cachedTokens: number; - inputTokens: number; - outputTokens: number; - reasoningTokens: number; - totalTokens: number; -}): TokenBreakdown { - const cached = isNaN(cachedTokens) ? 0 : cachedTokens; - const reasoning = isNaN(reasoningTokens) ? 0 : reasoningTokens; - - const adjustedInput = getAdjustedInput(inputTokens, cached, outputTokens, totalTokens); - const adjustedOutput = getAdjustedOutput( - adjustedInput, - outputTokens, - reasoning, - totalTokens - ); - - return { - netNewInput: cached > 0 ? Math.max(0, adjustedInput - cached) : adjustedInput, - cached, - output: adjustedOutput, - total: totalTokens, - }; -} - -/** - * Some providers report `inputTokens` exclusive of cached; others inclusive. - * We pick whichever interpretation makes `input + output` closest to `total`. - */ -function getAdjustedInput( - inputTokens: number, - cachedTokens: number, - outputTokens: number, - totalTokens: number -): number { - if (cachedTokens <= 0) { - return inputTokens; - } - const without = inputTokens + outputTokens; - const withCached = without + cachedTokens; - if (Math.abs(withCached - totalTokens) < Math.abs(without - totalTokens)) { - return inputTokens + cachedTokens; - } - return inputTokens; -} - -/** - * Same idea for reasoning tokens inside `outputTokens`. - */ -function getAdjustedOutput( - adjustedInput: number, - outputTokens: number, - reasoningTokens: number, - totalTokens: number -): number { - if (reasoningTokens <= 0) { - return outputTokens; - } - const without = adjustedInput + outputTokens; - const withReasoning = without + reasoningTokens; - if (Math.abs(withReasoning - totalTokens) < Math.abs(without - totalTokens)) { - return outputTokens + reasoningTokens; - } - return outputTokens; -} diff --git a/static/app/views/insights/pages/backend/backendTable.tsx b/static/app/views/insights/pages/backend/backendTable.tsx index 13a30deb6bed..d4f4e7e90938 100644 --- a/static/app/views/insights/pages/backend/backendTable.tsx +++ b/static/app/views/insights/pages/backend/backendTable.tsx @@ -81,7 +81,7 @@ const COLUMN_ORDER: Column[] = [ tooltip: SPAN_HEADER_TOOLTIPS.tpm, }, { - key: 'p50(span.duration)', + key: `p50(span.duration)`, name: t('p50()'), width: COL_WIDTH_UNDEFINED, tooltip: SPAN_HEADER_TOOLTIPS.p50, diff --git a/static/app/views/insights/pages/conversations/hooks/useConversation.spec.tsx b/static/app/views/insights/pages/conversations/hooks/useConversation.spec.tsx index 17933d91724f..4d835c2367de 100644 --- a/static/app/views/insights/pages/conversations/hooks/useConversation.spec.tsx +++ b/static/app/views/insights/pages/conversations/hooks/useConversation.spec.tsx @@ -228,23 +228,16 @@ describe('useConversation', () => { expect(result.current.isLoading).toBe(false); }); - // Verify the API was called with correct timestamps (with 1-hour padding), - // ALL_ACCESS_PROJECTS (-1) so it searches across all projects, - // and no environment filter so it searches across all environments + // Verify the API was called with correct timestamps (with 1-hour padding) expect(mockRequest).toHaveBeenCalledWith( expect.stringContaining('/ai-conversations/conv-timestamps/'), expect.objectContaining({ query: expect.objectContaining({ start: new Date(startTimestamp - 60 * 60 * 1000).toISOString(), end: new Date(endTimestamp + 60 * 60 * 1000).toISOString(), - project: [-1], }), }) ); - - // Ensure environment is not included in the query when using conversation timestamps - const queryArg = mockRequest.mock.calls[0]![1]!.query; - expect(queryArg).not.toHaveProperty('environment'); }); it('falls back to span.name when span.description is empty', async () => { diff --git a/static/app/views/insights/pages/conversations/hooks/useConversation.tsx b/static/app/views/insights/pages/conversations/hooks/useConversation.tsx index 3a39015fcb10..829d2c249a38 100644 --- a/static/app/views/insights/pages/conversations/hooks/useConversation.tsx +++ b/static/app/views/insights/pages/conversations/hooks/useConversation.tsx @@ -1,6 +1,5 @@ import {useEffect, useMemo} from 'react'; -import {ALL_ACCESS_PROJECTS} from 'sentry/components/pageFilters/constants'; import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; @@ -194,12 +193,10 @@ export function useConversation( const hasConversationTimestamps = conversation.startTimestamp !== undefined && conversation.endTimestamp !== undefined; - // When conversation timestamps are provided (e.g. from a shared link), - // search across all projects and environments so the conversation is found - // regardless of which project/environment the page filter is currently set to. const queryParams = hasConversationTimestamps ? { - project: [ALL_ACCESS_PROJECTS], + project: selection.projects, + environment: selection.environments, start: new Date(conversation.startTimestamp! - ONE_HOUR_MS).toISOString(), end: new Date(conversation.endTimestamp! + ONE_HOUR_MS).toISOString(), per_page: 1000, diff --git a/static/app/views/insights/pages/domainViewHeader.spec.tsx b/static/app/views/insights/pages/domainViewHeader.spec.tsx index 9143e34dcaef..7d1ae8021f29 100644 --- a/static/app/views/insights/pages/domainViewHeader.spec.tsx +++ b/static/app/views/insights/pages/domainViewHeader.spec.tsx @@ -15,7 +15,7 @@ describe('DomainViewHeader', () => { pathname: `/organizations/${organization.slug}/insights/frontend/`, query: {}, }, - route: '/organizations/:orgId/insights/frontend/', + route: `/organizations/:orgId/insights/frontend/`, }; it('renders', () => { diff --git a/static/app/views/insights/pages/frontend/frontendOverviewTable.tsx b/static/app/views/insights/pages/frontend/frontendOverviewTable.tsx index 3e5c8d4d943e..87c5b9f052dc 100644 --- a/static/app/views/insights/pages/frontend/frontendOverviewTable.tsx +++ b/static/app/views/insights/pages/frontend/frontendOverviewTable.tsx @@ -75,19 +75,19 @@ const COLUMN_ORDER: Column[] = [ tooltip: SPAN_HEADER_TOOLTIPS.tpm, }, { - key: 'p50_if(span.duration,is_transaction,equals,true)', + key: `p50_if(span.duration,is_transaction,equals,true)`, name: t('p50()'), width: COL_WIDTH_UNDEFINED, tooltip: SPAN_HEADER_TOOLTIPS.p50, }, { - key: 'p75_if(span.duration,is_transaction,equals,true)', + key: `p75_if(span.duration,is_transaction,equals,true)`, name: t('p75()'), width: COL_WIDTH_UNDEFINED, tooltip: SPAN_HEADER_TOOLTIPS.p75, }, { - key: 'p95_if(span.duration,is_transaction,equals,true)', + key: `p95_if(span.duration,is_transaction,equals,true)`, name: t('p95()'), width: COL_WIDTH_UNDEFINED, tooltip: SPAN_HEADER_TOOLTIPS.p95, diff --git a/static/app/views/insights/pages/mcp/components/groupedDurationWidget.tsx b/static/app/views/insights/pages/mcp/components/groupedDurationWidget.tsx index 09630a20420e..c7e72a0d30b9 100644 --- a/static/app/views/insights/pages/mcp/components/groupedDurationWidget.tsx +++ b/static/app/views/insights/pages/mcp/components/groupedDurationWidget.tsx @@ -150,7 +150,7 @@ export function GroupedDurationWidget(props: GroupedDurationWidgetProps) { ], groupBy: [props.groupBy], query: fullQuery, - sort: '-avg(span.duration)', + sort: `-avg(span.duration)`, interval: pageFilterChartParams.interval, }} onOpenFullScreen={() => { diff --git a/static/app/views/insights/pages/mcp/components/groupedErrorRateWidget.tsx b/static/app/views/insights/pages/mcp/components/groupedErrorRateWidget.tsx index ec135eb03d44..b01077194c1f 100644 --- a/static/app/views/insights/pages/mcp/components/groupedErrorRateWidget.tsx +++ b/static/app/views/insights/pages/mcp/components/groupedErrorRateWidget.tsx @@ -150,7 +150,7 @@ export function GroupedErrorRateWidget(props: GroupedErrorRateWidgetProps) { ], groupBy: [props.groupBy], query: fullQuery, - sort: '-failure_rate()', + sort: `-failure_rate()`, interval: pageFilterChartParams.interval, }} onOpenFullScreen={() => { diff --git a/static/app/views/insights/pages/mcp/components/groupedTrafficWidget.tsx b/static/app/views/insights/pages/mcp/components/groupedTrafficWidget.tsx index 5eacff590332..e06dfa782754 100644 --- a/static/app/views/insights/pages/mcp/components/groupedTrafficWidget.tsx +++ b/static/app/views/insights/pages/mcp/components/groupedTrafficWidget.tsx @@ -153,7 +153,7 @@ export function GroupedTrafficWidget(props: GroupedTrafficWidgetProps) { ], groupBy: [props.groupBy], query: fullQuery, - sort: '-count(span.duration)', + sort: `-count(span.duration)`, interval: pageFilterChartParams.interval, }} onOpenFullScreen={() => { diff --git a/static/app/views/insights/pages/mcp/components/mcpOverviewTable.tsx b/static/app/views/insights/pages/mcp/components/mcpOverviewTable.tsx index bed2137d6daa..46549ca7a71a 100644 --- a/static/app/views/insights/pages/mcp/components/mcpOverviewTable.tsx +++ b/static/app/views/insights/pages/mcp/components/mcpOverviewTable.tsx @@ -53,7 +53,7 @@ const rightAlignColumns = new Set([ export function McpOverviewTable() { const organization = useOrganization(); const {selection} = usePageFilters(); - const query = useCombinedQuery('span.op:mcp.server'); + const query = useCombinedQuery(`span.op:mcp.server`); const {tableSort} = useTableSort(); const tableDataRequest = useSpanTableData({ query, @@ -201,7 +201,7 @@ function SpanDescriptionCell({ }, ], query: search.formatString(), - sort: '-count(span.duration)', + sort: `-count(span.duration)`, field: fields, }); return {spanDescription}; diff --git a/static/app/views/insights/pages/mcp/components/mcpPromptsTable.tsx b/static/app/views/insights/pages/mcp/components/mcpPromptsTable.tsx index 8095888965e3..4c9f2fb5995c 100644 --- a/static/app/views/insights/pages/mcp/components/mcpPromptsTable.tsx +++ b/static/app/views/insights/pages/mcp/components/mcpPromptsTable.tsx @@ -176,7 +176,7 @@ function McpPromptCell({prompt}: {prompt: string}) { 'timestamp', ], query: search.formatString(), - sort: '-count(span.duration)', + sort: `-count(span.duration)`, }); return {prompt}; } diff --git a/static/app/views/insights/pages/mcp/components/mcpResourcesTable.tsx b/static/app/views/insights/pages/mcp/components/mcpResourcesTable.tsx index 2333e9be2c66..2fb528b4a88c 100644 --- a/static/app/views/insights/pages/mcp/components/mcpResourcesTable.tsx +++ b/static/app/views/insights/pages/mcp/components/mcpResourcesTable.tsx @@ -170,7 +170,7 @@ function McpResourceCell({resource}: {resource: string}) { ], field: ['span.description', 'span.status', 'span.duration', 'timestamp'], query: search.formatString(), - sort: '-count(span.duration)', + sort: `-count(span.duration)`, }); return {resource}; } diff --git a/static/app/views/insights/pages/mcp/components/mcpToolsTable.tsx b/static/app/views/insights/pages/mcp/components/mcpToolsTable.tsx index ea3541196266..e60bc346eafe 100644 --- a/static/app/views/insights/pages/mcp/components/mcpToolsTable.tsx +++ b/static/app/views/insights/pages/mcp/components/mcpToolsTable.tsx @@ -169,7 +169,7 @@ function McpToolCell({tool}: {tool: string}) { }, ], query: search.formatString(), - sort: '-count(span.duration)', + sort: `-count(span.duration)`, field: ['span.description', 'mcp.tool.result.content', 'span.duration', 'timestamp'], }); return {tool}; diff --git a/static/app/views/insights/pages/platform/laravel/commandsTable.tsx b/static/app/views/insights/pages/platform/laravel/commandsTable.tsx index d956c7228eae..76ca8918f4c2 100644 --- a/static/app/views/insights/pages/platform/laravel/commandsTable.tsx +++ b/static/app/views/insights/pages/platform/laravel/commandsTable.tsx @@ -144,7 +144,7 @@ function CommandCell({command}: {command: string}) { }, ], query: `span.op:console.command* command:${command}`, - sort: '-count(span.duration)', + sort: `-count(span.duration)`, }); return {command}; } diff --git a/static/app/views/insights/pages/useOverviewPageTrackAnalytics.ts b/static/app/views/insights/pages/useOverviewPageTrackAnalytics.ts index 59f92f53f360..442988ae8db1 100644 --- a/static/app/views/insights/pages/useOverviewPageTrackAnalytics.ts +++ b/static/app/views/insights/pages/useOverviewPageTrackAnalytics.ts @@ -33,7 +33,7 @@ export function useOverviewPageTrackPageload() { useEffect(() => { if (pageFilters.isReady && projects.initiallyLoaded) { const selectedPlatforms = JSON.parse(selectedPlatformsString); - trackAnalytics('insights.page_loads.overview', { + trackAnalytics(`insights.page_loads.overview`, { organization, platforms: selectedPlatforms, domain: view, diff --git a/static/app/views/insights/queues/components/messageSpanSamplesPanel.spec.tsx b/static/app/views/insights/queues/components/messageSpanSamplesPanel.spec.tsx index dfb367d81f64..cbc1bac8ac6c 100644 --- a/static/app/views/insights/queues/components/messageSpanSamplesPanel.spec.tsx +++ b/static/app/views/insights/queues/components/messageSpanSamplesPanel.spec.tsx @@ -16,7 +16,7 @@ describe('messageSpanSamplesPanel', () => { let traceItemAttributesMock: jest.Mock; const basePath = `/organizations/${organization.slug}/insights/queues/`; - const baseRoute = '/organizations/:orgId/insights/queues/'; + const baseRoute = `/organizations/:orgId/insights/queues/`; const baseRouterConfig = { location: { diff --git a/static/app/views/insights/queues/components/messageSpanSamplesPanel.tsx b/static/app/views/insights/queues/components/messageSpanSamplesPanel.tsx index f349c770b4c3..77821edc5ac8 100644 --- a/static/app/views/insights/queues/components/messageSpanSamplesPanel.tsx +++ b/static/app/views/insights/queues/components/messageSpanSamplesPanel.tsx @@ -192,7 +192,7 @@ export function MessageSpanSamplesPanel() { } = useFetchSpanTimeSeries( { query: timeseriesFilters, - yAxis: ['avg(span.duration)'], + yAxis: [`avg(span.duration)`], enabled: isPanelOpen, }, timeseriesReferrer diff --git a/static/app/views/insights/queues/components/tables/queuesTable.tsx b/static/app/views/insights/queues/components/tables/queuesTable.tsx index d80668554e2a..9b6278e69086 100644 --- a/static/app/views/insights/queues/components/tables/queuesTable.tsx +++ b/static/app/views/insights/queues/components/tables/queuesTable.tsx @@ -92,7 +92,7 @@ const SORTABLE_FIELDS = [ 'count_op(queue.process)', 'avg_if(span.duration,span.op,equals,queue.process)', 'avg(messaging.message.receive.latency)', - 'sum(span.duration)', + `sum(span.duration)`, 'trace_status_rate(ok)', ] as const; diff --git a/static/app/views/insights/queues/components/tables/transactionsTable.spec.tsx b/static/app/views/insights/queues/components/tables/transactionsTable.spec.tsx index c5a0daf95cd3..c24609b58dd3 100644 --- a/static/app/views/insights/queues/components/tables/transactionsTable.spec.tsx +++ b/static/app/views/insights/queues/components/tables/transactionsTable.spec.tsx @@ -19,7 +19,7 @@ describe('transactionsTable', () => { pathname: `/organizations/${organization.slug}/insights/queues/`, query: {statsPeriod: '10d', project: '1'}, }, - route: '/organizations/:orgId/insights/queues/', + route: `/organizations/:orgId/insights/queues/`, }; beforeEach(() => { diff --git a/static/app/views/insights/queues/components/tables/transactionsTable.tsx b/static/app/views/insights/queues/components/tables/transactionsTable.tsx index 8e25b6bd1edd..dc6c35f9e0d9 100644 --- a/static/app/views/insights/queues/components/tables/transactionsTable.tsx +++ b/static/app/views/insights/queues/components/tables/transactionsTable.tsx @@ -90,7 +90,7 @@ const SORTABLE_FIELDS = [ 'count_op(queue.process)', 'avg_if(span.duration,span.op,equals,queue.process)', 'avg(messaging.message.receive.latency)', - 'sum(span.duration)', + `sum(span.duration)`, 'trace_status_rate(ok)', ] as const; diff --git a/static/app/views/insights/queues/views/destinationSummaryPage.spec.tsx b/static/app/views/insights/queues/views/destinationSummaryPage.spec.tsx index 289ed41717e8..c64f00efc60c 100644 --- a/static/app/views/insights/queues/views/destinationSummaryPage.spec.tsx +++ b/static/app/views/insights/queues/views/destinationSummaryPage.spec.tsx @@ -23,7 +23,7 @@ describe('destinationSummaryPage', () => { pathname: `/organizations/${organization.slug}/insights/backend/queues/destination/`, query: {statsPeriod: '10d', project: project.id}, }, - route: '/organizations/:orgId/insights/backend/queues/destination/', + route: `/organizations/:orgId/insights/backend/queues/destination/`, }; jest.mocked(useReleaseStats).mockReturnValue({ diff --git a/static/app/views/insights/queues/views/queuesLandingPage.spec.tsx b/static/app/views/insights/queues/views/queuesLandingPage.spec.tsx index ab226945c26b..7e6a532c812c 100644 --- a/static/app/views/insights/queues/views/queuesLandingPage.spec.tsx +++ b/static/app/views/insights/queues/views/queuesLandingPage.spec.tsx @@ -23,7 +23,7 @@ describe('queuesLandingPage', () => { pathname: `/organizations/${organization.slug}/insights/backend/queues/`, query: {statsPeriod: '10d', project: '1'}, }, - route: '/organizations/:orgId/insights/backend/queues/', + route: `/organizations/:orgId/insights/backend/queues/`, }; jest.mocked(useReleaseStats).mockReturnValue({ diff --git a/static/app/views/insights/sessions/charts/chartWithIssues.tsx b/static/app/views/insights/sessions/charts/chartWithIssues.tsx index cce62242e1ca..f6fc015b6a22 100644 --- a/static/app/views/insights/sessions/charts/chartWithIssues.tsx +++ b/static/app/views/insights/sessions/charts/chartWithIssues.tsx @@ -129,7 +129,7 @@ export function ChartWithIssues(props: Props) { {title} {hasData && recentIssues?.length ? ( - + {t('View All')} ) : null} @@ -154,7 +154,7 @@ export function ChartWithIssues(props: Props) { }} /> {hasData && recentIssues?.length ? ( - + {t('View All')} ) : null} diff --git a/static/app/views/insights/sessions/queries/useCrashFreeSessions.tsx b/static/app/views/insights/sessions/queries/useCrashFreeSessions.tsx index b941bd758d08..0f83370eb73d 100644 --- a/static/app/views/insights/sessions/queries/useCrashFreeSessions.tsx +++ b/static/app/views/insights/sessions/queries/useCrashFreeSessions.tsx @@ -59,7 +59,7 @@ export function useCrashFreeSessions({pageFilters}: {pageFilters?: PageFilters}) })), meta: { fields: { - ['crash_free_session_rate']: 'percentage' as const, + [`crash_free_session_rate`]: 'percentage' as const, time: 'date' as const, }, units: {}, diff --git a/static/app/views/insights/sessions/queries/useReleaseSessionCounts.tsx b/static/app/views/insights/sessions/queries/useReleaseSessionCounts.tsx index f824f4ce9381..3b7053f1b621 100644 --- a/static/app/views/insights/sessions/queries/useReleaseSessionCounts.tsx +++ b/static/app/views/insights/sessions/queries/useReleaseSessionCounts.tsx @@ -55,7 +55,7 @@ export function useReleaseSessionCounts({pageFilters}: {pageFilters?: PageFilter })), meta: { fields: { - ['total_sessions']: 'integer' as const, + [`total_sessions`]: 'integer' as const, time: 'date' as const, }, units: {}, diff --git a/static/app/views/insights/sessions/queries/useReleaseSessionPercentage.tsx b/static/app/views/insights/sessions/queries/useReleaseSessionPercentage.tsx index 417d18a2b9f2..17c76b2d480e 100644 --- a/static/app/views/insights/sessions/queries/useReleaseSessionPercentage.tsx +++ b/static/app/views/insights/sessions/queries/useReleaseSessionPercentage.tsx @@ -55,7 +55,7 @@ export function useReleaseSessionPercentage({pageFilters}: {pageFilters?: PageFi })), meta: { fields: { - ['session_percent']: 'percentage' as const, + [`session_percent`]: 'percentage' as const, time: 'date' as const, }, units: {}, diff --git a/static/app/views/insights/uptime/views/overview.tsx b/static/app/views/insights/uptime/views/overview.tsx index 5b23b50080d1..64df5b7700f1 100644 --- a/static/app/views/insights/uptime/views/overview.tsx +++ b/static/app/views/insights/uptime/views/overview.tsx @@ -86,7 +86,7 @@ export default function UptimeOverview() { } disabled={!canCreateAlert} tooltipProps={{title: canCreateAlert ? undefined : permissionTooltipText}} @@ -155,7 +155,7 @@ export default function UptimeOverview() { } > {t('Add Uptime Monitor')} diff --git a/static/app/views/integrationOrganizationLink/index.tsx b/static/app/views/integrationOrganizationLink/index.tsx index 1c8f87455e3b..55620a2a68f0 100644 --- a/static/app/views/integrationOrganizationLink/index.tsx +++ b/static/app/views/integrationOrganizationLink/index.tsx @@ -326,7 +326,7 @@ export default function IntegrationOrganizationLink() { const target_url = `https://github.com/${installationData?.account.login}`; const alertText = tct( - 'GitHub user [sender_login] has installed GitHub app to [account_type] [account_login]. Proceed if you want to attach this installation to your Sentry account.', + `GitHub user [sender_login] has installed GitHub app to [account_type] [account_login]. Proceed if you want to attach this installation to your Sentry account.`, { account_type: {installationData?.account.type}, account_login: ( diff --git a/static/app/views/issueDetails/actions/index.spec.tsx b/static/app/views/issueDetails/actions/index.spec.tsx index b8e6921bbd21..acd65fe34deb 100644 --- a/static/app/views/issueDetails/actions/index.spec.tsx +++ b/static/app/views/issueDetails/actions/index.spec.tsx @@ -197,7 +197,7 @@ describe('GroupActions', () => { location: { pathname: `/organizations/${org.slug}/issues/${group.id}/`, }, - route: '/organizations/:orgId/issues/:groupId/', + route: `/organizations/:orgId/issues/:groupId/`, }; const {router} = render( @@ -248,7 +248,7 @@ describe('GroupActions', () => { location: { pathname: `/organizations/${org.slug}/issues/${issuePlatformGroup.id}/`, }, - route: '/organizations/:orgId/issues/:groupId/', + route: `/organizations/:orgId/issues/:groupId/`, }; const {router} = render( diff --git a/static/app/views/issueDetails/actions/index.tsx b/static/app/views/issueDetails/actions/index.tsx index f5dc4ab279ba..058338205360 100644 --- a/static/app/views/issueDetails/actions/index.tsx +++ b/static/app/views/issueDetails/actions/index.tsx @@ -299,7 +299,7 @@ export function GroupActions({group, project, disabled, event}: GroupActionsProp typeof renderDisabled === 'function' && renderDisabled({...innerProps, hasFeature, children: null})} {t( - 'Discarding this event will result in the deletion of most data associated with this issue and future events being discarded before reaching your stream. Are you sure you wish to continue?' + `Discarding this event will result in the deletion of most data associated with this issue and future events being discarded before reaching your stream. Are you sure you wish to continue?` )}