Replays Self-Serve Bulk Delete System

getsentry/sentry#93864
Golden recall: N/A Extra findings: 4

Not a valid reproduction — golden comments do not match this PR

The Kodus benchmark dataset associates this test entry (index 4) with the URL ai-code-review-evaluation/sentry-greptile/pull/5. That PR has 106 changed files and is described by the dataset itself as "a mix of many PRs". Its 3 golden comments reference code from the workflow_engine/detector module, the events pipeline, and error response format changes — none of which have any relation to replays infrastructure.

The actual replays PR (getsentry/sentry#93864) is a clean, scoped change with 12 files, all in the replays domain: a new model/migration, API endpoint, Celery tasks, and corresponding tests. FriendlyReviewer was run on a faithful reproduction of this PR (same 12 files, same +1269/−26 diff).

Since the golden comments were extracted from a different, unrelated codebase, any comparison against them would be meaningless. The Kodus dataset likely assembled this entry from a composite PR — possibly bundling the replays PR with other, unrelated Sentry changes — and assigned golden comments that belong to those other changes rather than to the replays feature. Without aligned ground truth, no Golden Recall score can be calculated.

The findings below are FriendlyReviewer's analysis of the actual content of getsentry/sentry#93864.

Supplementary Findings (16 findings)

Critical & Medium

Finding File Severity Legitimate?
status CharField missing max_length — Django 5.2.1 blocks at system check models.py:33 HIGH ✓ Real bug — blocking in Django 5.2.1+
environments field required=True but documented as optional in the API blueprint project_replay_jobs_delete.py:39 MEDIUM ✓ API contract mismatch
N+1 query: Organization.objects.filter(project__id=project_id).get().id executed on every batch — caller already has organization_id usecases/delete.py:159 MEDIUM ✓ Unnecessary query per invocation
Potential infinite recursion in tests: run_bulk_replay_delete_job.delay not mocked — if CELERY_ALWAYS_EAGER=True, chained call runs synchronously causing infinite loop test_delete_replays_bulk.py:50 MEDIUM ✓ Real test failure scenario

Minor (12 findings)

Finding File Severity Legitimate?
countDeleted poorly named — represents a processing checkpoint, not the final deletion count project_replay_jobs_delete.py:34 LOW ✓ Acceptable
job_id annotated int but DRF passes URL kwargs as str project_replay_jobs_delete.py:111 LOW ✓ Acceptable
Task dispatch not wrapped in transaction.on_commit() project_replay_jobs_delete.py:100 LOW ✓ Acceptable
Return type → int | None but function always returns None usecases/delete.py:30 LOW ✓ Acceptable
String literals instead of DeletionJobStatus.XXX.value in tests (5 occurrences) test_delete_replays_bulk.py:25,82,118,155,179 LOW ✓ Acceptable
Fragile ordering in pagination test (same date_added for multiple jobs) test_project_replay_jobs_delete.py:136 LOW ✓ Acceptable
Missing range validation on deletion job duration project_replay_jobs_delete.py:59 LOW ✓ Acceptable
Duplicated code: _delete_if_exists defined in both tasks.py and usecases/delete.py tasks.py:153 LOW ✓ Acceptable

Honest Analysis

FriendlyReviewer delivered a thorough and technically accurate analysis of the PR's actual content — 12 files implementing a replay bulk-deletion system. The findings are all legitimate and relevant to the code as reviewed.

The most critical finding — a status CharField without max_length — is a real blocking issue with modern Django versions. The medium-severity findings (API contract mismatch on environments, the N+1 query for organization lookup, and the test recursion risk) are all actionable design problems. The remaining Low findings cover code quality concerns like enum usage, naming, and test fragility, none of which are false positives.

However, this test cannot be meaningfully scored against the Kodus benchmark. The golden comments the dataset assigns to this entry reference code from workflow_engine/detector/, events/, and error response format changes — none of which appear in the replays PR. The Kodus dataset's own annotation ("there is no such PR, it is a mix of many PRs") confirms the composite nature of the source PR. Since the ground truth doesn't correspond to the code FriendlyReviewer analyzed, any recall or coverage calculation would be misleading. FriendlyReviewer's output for this test should be assessed on its own merits: 16 findings, all legitimate, covering model correctness, API contracts, performance, and test quality.

FriendlyReviewer's full review

As posted on the PR — the complete output from FriendlyReviewer.

friendly-reviewer bot commented now

Hi there 👋

🌥️ Tech
1 high, 3 medium, 12 low

This Merge Request introduces replay bulk-deletion infrastructure, including a new model, migration, API endpoint, and Celery tasks. The overall architecture is sound, but several issues must be addressed before merging. The most critical is a missing max_length on the status CharField in both the model and migration, which will cause Django system check failures. Additionally, there is an API contract mismatch where the environments field is required but documented as optional, an inefficient N+1 query for organization lookup, type annotation errors, potential infinite recursion in tests, and use of string literals instead of enums for status comparisons. Other minor concerns include misleading naming, missing range validation, fragile test assumptions, and code duplication.


These might need a close look

  • 🔴 src/sentry/replays/models.py (L33)
    The status CharField is missing max_length. Django 5.2.1 requires it; this will cause a system check error. The longest choice value is 11 characters. Add max_length=16 (or at least 11) to both the model and migration.

Worth checking

  • 🟡 src/sentry/replays/endpoints/project_replay_jobs_delete.py (L39)
    API contract mismatch: environments field is required=True but documented as optional in the API blueprint. Either make it optional (defaulting to []) or update the documentation to clarify that [] must be sent explicitly.
  • 🟡 src/sentry/replays/usecases/delete.py (L159)
    N+1 query: Organization.objects.filter(project__id=project_id).get().id is executed on every batch invocation, despite the caller already having organization_id. Pass organization_id directly to avoid this inefficiency.
  • 🟡 tests/sentry/replays/tasks/test_delete_replays_bulk.py (L50)
    Potential infinite recursion: The test does not mock run_bulk_replay_delete_job.delay. If CELERY_ALWAYS_EAGER is True, the chained call runs synchronously, causing infinite recursion. Mock the .delay() call or set has_more=False.

Small things (take or leave)

  • 🔵 src/sentry/replays/endpoints/project_replay_jobs_delete.py (L34)
    Semantic concern: The API field countDeleted maps to obj.offset, which for in-progress jobs represents a processing checkpoint, not the final deletion count. Consider renaming to rowsProcessed or clearly documenting this in the serializer.
  • 🔵 src/sentry/replays/endpoints/project_replay_jobs_delete.py (L111)
    Type annotation error: job_id is annotated as int but Django/DRF passes URL kwargs as strings. Change annotation to str or add explicit int() conversion.
  • 🔵 src/sentry/replays/endpoints/project_replay_jobs_delete.py (L100)
    Task dispatch is not wrapped in transaction.on_commit(). If the database transaction is not yet committed (e.g., future transactional wrapping), the worker may fail with a DoesNotExist error. Use transaction.on_commit(lambda: task.delay(...)).
  • 🔵 src/sentry/replays/usecases/delete.py (L30)
    Return type annotation says -> int | None but the function always returns None. Change to -> None.
  • 🔵 tests/sentry/replays/tasks/test_delete_replays_bulk.py (L25)
    Use string literals for status comparisons instead of the DeletionJobStatus enum. Prefer DeletionJobStatus.PENDING.value for consistency and type safety.
  • 🔵 tests/sentry/replays/tasks/test_delete_replays_bulk.py (L82)
    Use string literal "in-progress" instead of DeletionJobStatus.IN_PROGRESS.value. Use the enum value for consistency.
  • 🔵 tests/sentry/replays/tasks/test_delete_replays_bulk.py (L118)
    Use string literal "completed" instead of DeletionJobStatus.COMPLETED.value. Use the enum value.
  • 🔵 tests/sentry/replays/tasks/test_delete_replays_bulk.py (L155)
    Use string literal "completed" instead of DeletionJobStatus.COMPLETED.value. Use the enum value.
  • 🔵 tests/sentry/replays/tasks/test_delete_replays_bulk.py (L179)
    Use string literal "completed" instead of DeletionJobStatus.COMPLETED.value. Use the enum value.
  • 🔵 tests/sentry/replays/test_project_replay_jobs_delete.py (L136)
    Fragile ordering assumption in pagination test: jobs created in the same loop iteration may share the same date_added, leading to non-deterministic ordering. Use explicit ordering by known IDs or manipulate timestamps.
  • 🔵 src/sentry/replays/endpoints/project_replay_jobs_delete.py (L59)
    Missing range duration validation: no upper bound on the time range for deletion jobs, which could lead to extremely long-running operations. Consider adding a maximum duration check (e.g., 90 days).
  • 🔵 src/sentry/replays/tasks.py (L153)
    Code duplication: _delete_if_exists is defined in both tasks.py and usecases/delete.py. Extract to a shared utility module to avoid duplication.