From 3520c76c40de9f0ace85f104d7fdf633e9dafe93 Mon Sep 17 00:00:00 2001 From: George Gritsouk <989898+gggritso@users.noreply.github.com> Date: Thu, 6 Aug 2026 23:56:14 -0400 Subject: [PATCH 1/2] feat(replays): Let the deletion script select which steps it runs `delete_replays` archives, deletes Seer summaries and deletes RRWeb blobs. Each step is now selectable, defaulting to on, so a caller that has already performed one of them by hand can skip it. Skipping the blob step also skips the legacy `ReplayRecordingSegment` cleanup, which lives inside `delete_replays_script_async`. --- src/sentry/replays/scripts/delete_replays.py | 72 ++++++++++++-------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/src/sentry/replays/scripts/delete_replays.py b/src/sentry/replays/scripts/delete_replays.py index 273c2e4704db..d3f129c76d24 100644 --- a/src/sentry/replays/scripts/delete_replays.py +++ b/src/sentry/replays/scripts/delete_replays.py @@ -46,6 +46,10 @@ def delete_replays( tags: list[str], start_utc: datetime, end_utc: datetime, + *, + archive: bool = True, + delete_seer_data: bool = True, + delete_blobs: bool = True, ) -> None: """Delete a set of replays from a query.""" search_filters = translate_cli_tags_param_to_snuba_tag_param(tags) @@ -89,6 +93,9 @@ def delete_replays( "window_end": window_end, "has_more": has_more, "total_replays": total_replays, + "archive": archive, + "delete_seer_data": delete_seer_data, + "delete_blobs": delete_blobs, } if dry_run: logger.info( @@ -101,6 +108,9 @@ def delete_replays( rows=replays, has_seer_data=has_seer_data, total_replays=total_replays, + archive=archive, + delete_seer_data=delete_seer_data, + delete_blobs=delete_blobs, ) @@ -114,48 +124,54 @@ def delete_replay_ids( rows: list[tuple[int, str, int]], has_seer_data: bool, total_replays: int, + *, + archive: bool = True, + delete_seer_data: bool = True, + delete_blobs: bool = True, ) -> None: """Delete a set of replay-ids for a specific project.""" logging_context = { "project_id": project_id, "num_replays": len(rows), "total_replays": total_replays, + "archive": archive, + "delete_seer_data": delete_seer_data, + "delete_blobs": delete_blobs, } - logger.info("Archiving %d replays.", len(rows), extra=logging_context) - - # Bulk produce archived replay rows to the ingest-replay-events topic before flushing. - # - # This operation is fast enough that it can be performed synchronously. Archiving - # synchronously gives the script runner the feeling that the script executed to completion - # and will allow them to immediately spot-check before moving on with their day. In a - # purely asynchronous world the script runner will have to continually worry if their tasks - # executed. - # - # This also gives us reasonable assurances that if the script ran to completion the customer - # will not be able to access their deleted data even if the actual deletion takes place some - # time later - for _, replay_id, _ in rows: - archive_replay(project_id, replay_id) - - if has_seer_data: + + if archive: + logger.info("Archiving %d Replays.", len(rows), extra=logging_context) + + for _, replay_id, _ in rows: + archive_replay(project_id, replay_id) + else: + # Archiving is the only step the customer can observe. Without it they keep their Replays. + logger.warning( + "Not archiving %d Replays; they remain visible to the customer.", + len(rows), + extra=logging_context, + ) + + if has_seer_data and delete_seer_data: # The finder strips dashes from `replay_id`; Seer keys on the dashed UUID + logger.info("Deleting Seer data for %d Replays.", len(rows), extra=logging_context) replay_ids = [str(uuid.UUID(replay_id)) for _, replay_id, _ in rows] delete_seer_replay_data(organization_id, project_id, replay_ids) - logger.info("Scheduling %d replays for deletion.", len(rows), extra=logging_context) + if delete_blobs: + logger.info("Scheduling %d Replays for blob deletion.", len(rows), extra=logging_context) - # Asynchronously delete RRWeb recording data. - # - # Because this operation could involve millions of requests to the blob storage provider we - # schedule the tasks to run on a cluster of workers. This allows us to parallelize the work - # and complete the task as quickly as possible. - for retention_days, replay_id, max_segment_id in rows: - delete_replays_script_async.delay(retention_days, project_id, replay_id, max_segment_id) + # Asynchronously delete RRWeb recording data. + # + # Because this operation could involve millions of requests to the blob storage provider we + # schedule the tasks to run on a cluster of workers. This allows us to parallelize the work + # and complete the task as quickly as possible. + for retention_days, replay_id, max_segment_id in rows: + delete_replays_script_async.delay(retention_days, project_id, replay_id, max_segment_id) - logger.info("%d replays were successfully deleted.", len(rows), extra=logging_context) logger.info( - "The customer will no longer have access to the replays passed to this function. Deletion " - "of RRWeb data will complete asynchronously.", + "Finished processing %d Replays.", + len(rows), extra=logging_context, ) From 5cacf21d7e35705577c530ed027196ba3d194eb3 Mon Sep 17 00:00:00 2001 From: George Gritsouk <989898+gggritso@users.noreply.github.com> Date: Fri, 7 Aug 2026 00:02:54 -0400 Subject: [PATCH 2/2] ref(replays): Restore the comment on why archiving is synchronous --- src/sentry/replays/scripts/delete_replays.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/sentry/replays/scripts/delete_replays.py b/src/sentry/replays/scripts/delete_replays.py index d3f129c76d24..1ce7aab4174e 100644 --- a/src/sentry/replays/scripts/delete_replays.py +++ b/src/sentry/replays/scripts/delete_replays.py @@ -142,6 +142,17 @@ def delete_replay_ids( if archive: logger.info("Archiving %d Replays.", len(rows), extra=logging_context) + # Bulk produce archived replay rows to the ingest-replay-events topic before flushing. + # + # This operation is fast enough that it can be performed synchronously. Archiving + # synchronously gives the script runner the feeling that the script executed to completion + # and will allow them to immediately spot-check before moving on with their day. In a + # purely asynchronous world the script runner will have to continually worry if their tasks + # executed. + # + # This also gives us reasonable assurances that if the script ran to completion the customer + # will not be able to access their deleted data even if the actual deletion takes place some + # time later for _, replay_id, _ in rows: archive_replay(project_id, replay_id) else: