From 63c9708e8159684ca130f15ed71dbf4505c4016e Mon Sep 17 00:00:00 2001 From: Gagan Trivedi Date: Fri, 31 Jul 2026 13:11:09 +0530 Subject: [PATCH 1/7] feat(experimentation): log per-object warehouse delivery outcomes Contributes to #8149 --- .../migrations/0012_warehouse_delivery_log.py | 55 +++++++++++++++++++ api/experimentation/models.py | 27 +++++++++ api/experimentation/services.py | 19 ++++++- api/tests/unit/experimentation/test_tasks.py | 43 +++++++++++++++ .../observability/_events-catalogue.md | 24 ++++---- 5 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 api/experimentation/migrations/0012_warehouse_delivery_log.py diff --git a/api/experimentation/migrations/0012_warehouse_delivery_log.py b/api/experimentation/migrations/0012_warehouse_delivery_log.py new file mode 100644 index 000000000000..7bb50f6fa987 --- /dev/null +++ b/api/experimentation/migrations/0012_warehouse_delivery_log.py @@ -0,0 +1,55 @@ +# Generated by Django 5.2.16 on 2026-07-31 07:41 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("experimentation", "0011_organisation_ingestion_infrastructure"), + ] + + operations = [ + migrations.CreateModel( + name="WarehouseDeliveryLog", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("s3_key", models.CharField(max_length=1024)), + ( + "outcome", + models.CharField( + choices=[("delivered", "Delivered"), ("rejected", "Rejected")], + max_length=50, + ), + ), + ("rows_count", models.PositiveIntegerField(blank=True, null=True)), + ("error", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "connection", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="delivery_logs", + to="experimentation.warehouseconnection", + ), + ), + ], + options={ + "indexes": [ + models.Index( + fields=["connection", "created_at"], + name="experimenta_connect_11e6cf_idx", + ) + ], + }, + ), + ] diff --git a/api/experimentation/models.py b/api/experimentation/models.py index fb9cb9ed3125..569b71cc404a 100644 --- a/api/experimentation/models.py +++ b/api/experimentation/models.py @@ -125,6 +125,33 @@ class OrganisationIngestionInfrastructure(models.Model): updated_at = models.DateTimeField(auto_now=True) +class WarehouseDeliveryOutcome(models.TextChoices): + DELIVERED = "delivered", "Delivered" + REJECTED = "rejected", "Rejected" + + +class WarehouseDeliveryLog(models.Model): + connection = models.ForeignKey( + WarehouseConnection, + on_delete=models.CASCADE, + related_name="delivery_logs", + ) + # S3's own key length limit. + s3_key = models.CharField(max_length=1024) + outcome = models.CharField( + max_length=50, + choices=WarehouseDeliveryOutcome.choices, + ) + rows_count = models.PositiveIntegerField(null=True, blank=True) + error = models.TextField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + indexes = [ + models.Index(fields=["connection", "created_at"]), + ] + + class ExperimentStatus(models.TextChoices): CREATED = "created", "Created" RUNNING = "running", "Running" diff --git a/api/experimentation/services.py b/api/experimentation/services.py index ba87749a97df..e94da042c0f9 100644 --- a/api/experimentation/services.py +++ b/api/experimentation/services.py @@ -56,6 +56,8 @@ MetricAggregation, MetricDirection, WarehouseConnectionStatus, + WarehouseDeliveryLog, + WarehouseDeliveryOutcome, WarehouseType, ) from experimentation.results_query import _EXPOSURES_CTE, ResultsQueryBuilder @@ -861,12 +863,12 @@ def _deliver_pending_objects( ) break try: - rows_count += warehouse_delivery_service.deliver_object( + object_rows_count = warehouse_delivery_service.deliver_object( client, bucket_name, s3_key, ) - except warehouse_delivery_service.ObjectRejectedError: + except warehouse_delivery_service.ObjectRejectedError as exc: # This object's contents are the problem; the ones behind it are # still deliverable. warehouse_delivery_service.move_object( @@ -874,6 +876,12 @@ def _deliver_pending_objects( s3_key, to_prefix=warehouse_delivery_service.FAILED_PREFIX, ) + WarehouseDeliveryLog.objects.create( + connection=connection, + s3_key=s3_key, + outcome=WarehouseDeliveryOutcome.REJECTED, + error=str(exc), + ) rejected_count += 1 flagsmith_experimentation_warehouse_delivery_objects_total.labels( result="rejected" @@ -889,6 +897,13 @@ def _deliver_pending_objects( s3_key, to_prefix=warehouse_delivery_service.ARCHIVE_PREFIX, ) + WarehouseDeliveryLog.objects.create( + connection=connection, + s3_key=s3_key, + outcome=WarehouseDeliveryOutcome.DELIVERED, + rows_count=object_rows_count, + ) + rows_count += object_rows_count delivered_count += 1 flagsmith_experimentation_warehouse_delivery_objects_total.labels( result="delivered" diff --git a/api/tests/unit/experimentation/test_tasks.py b/api/tests/unit/experimentation/test_tasks.py index f0115cbb0717..861adfc0ade5 100644 --- a/api/tests/unit/experimentation/test_tasks.py +++ b/api/tests/unit/experimentation/test_tasks.py @@ -34,6 +34,8 @@ OrganisationIngestionInfrastructure, WarehouseConnection, WarehouseConnectionStatus, + WarehouseDeliveryLog, + WarehouseDeliveryOutcome, WarehouseType, ) from experimentation.stats import VariantStats @@ -908,6 +910,26 @@ def test_deliver_events_for_connection__pending_objects__delivers_archives_and_r == [] ) + # Then each delivery is recorded in the audit ledger + assert list( + WarehouseDeliveryLog.objects.filter(connection=clickhouse_connection) + .order_by("s3_key") + .values_list("s3_key", "outcome", "rows_count", "error") + ) == [ + ( + _pending_key(environment.api_key, hour="13"), + WarehouseDeliveryOutcome.DELIVERED, + 100, + None, + ), + ( + _pending_key(environment.api_key, hour="14"), + WarehouseDeliveryOutcome.DELIVERED, + 100, + None, + ), + ] + # Then the delivery success resolves the earlier breakage clickhouse_connection.refresh_from_db() assert clickhouse_connection.status == WarehouseConnectionStatus.CONNECTED @@ -980,6 +1002,27 @@ def test_deliver_events_for_connection__rejected_object__moves_to_failed_and_con assert clickhouse_connection.status == WarehouseConnectionStatus.CONNECTED assert _delivery_objects_count("rejected") == rejected_objects_before + 1 assert log.has("delivery.object_rejected", level="error") + + # Then both outcomes are recorded in the audit ledger, the rejection with + # its warehouse error + assert list( + WarehouseDeliveryLog.objects.filter(connection=clickhouse_connection) + .order_by("s3_key") + .values_list("s3_key", "outcome", "rows_count", "error") + ) == [ + ( + _pending_key(environment.api_key, hour="13"), + WarehouseDeliveryOutcome.REJECTED, + None, + "Constraint `event_not_empty` violated", + ), + ( + _pending_key(environment.api_key, hour="14"), + WarehouseDeliveryOutcome.DELIVERED, + 100, + None, + ), + ] assert { "level": "info", "event": "delivery.completed", diff --git a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md index 78e67df87d3b..a074755d17fc 100644 --- a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md +++ b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md @@ -631,7 +631,7 @@ Attributes: ### `warehouse.connection.connected` Logged at `info` from: - - `api/experimentation/services.py:1026` + - `api/experimentation/services.py:1041` Attributes: - `environment.id` @@ -640,7 +640,7 @@ Attributes: ### `warehouse.connection.event_stats_failed` Logged at `warning` from: - - `api/experimentation/services.py:1089` + - `api/experimentation/services.py:1104` Attributes: - `environment.id` @@ -649,7 +649,7 @@ Attributes: ### `warehouse.connection.test_event_sent` Logged at `info` from: - - `api/experimentation/services.py:813` + - `api/experimentation/services.py:815` Attributes: - `environment.id` @@ -658,7 +658,7 @@ Attributes: ### `warehouse.connection.verification_failed` Logged at `warning` from: - - `api/experimentation/services.py:1001` + - `api/experimentation/services.py:1016` Attributes: - `environment.id` @@ -668,7 +668,7 @@ Attributes: ### `warehouse.connection.verification_succeeded` Logged at `info` from: - - `api/experimentation/services.py:1011` + - `api/experimentation/services.py:1026` Attributes: - `environment.id` @@ -677,7 +677,7 @@ Attributes: ### `warehouse.delivery.all_objects_rejected` Logged at `error` from: - - `api/experimentation/services.py:956` + - `api/experimentation/services.py:971` Attributes: - `connection.id` @@ -688,7 +688,7 @@ Attributes: ### `warehouse.delivery.budget_exhausted` Logged at `info` from: - - `api/experimentation/services.py:858` + - `api/experimentation/services.py:860` Attributes: - `connection.id` @@ -699,7 +699,7 @@ Attributes: ### `warehouse.delivery.completed` Logged at `info` from: - - `api/experimentation/services.py:966` + - `api/experimentation/services.py:981` Attributes: - `connection.id` @@ -712,7 +712,7 @@ Attributes: ### `warehouse.delivery.failed` Logged at `error` from: - - `api/experimentation/services.py:939` + - `api/experimentation/services.py:954` Attributes: - `connection.id` @@ -723,7 +723,7 @@ Attributes: ### `warehouse.delivery.object_rejected` Logged at `error` from: - - `api/experimentation/services.py:881` + - `api/experimentation/services.py:889` Attributes: - `connection.id` @@ -735,7 +735,7 @@ Attributes: ### `warehouse.srm.overallocated` Logged at `error` from: - - `api/experimentation/services.py:435` + - `api/experimentation/services.py:437` Attributes: - `environment.id` @@ -745,7 +745,7 @@ Attributes: ### `warehouse.srm.unkeyed_variant` Logged at `error` from: - - `api/experimentation/services.py:421` + - `api/experimentation/services.py:423` Attributes: - `environment.id` From 0dac30bd0518afe0a6fe1f5c132cc63838d4e188 Mon Sep 17 00:00:00 2001 From: Gagan Trivedi Date: Fri, 31 Jul 2026 13:26:14 +0530 Subject: [PATCH 2/7] feat(experimentation): expire warehouse delivery logs after 60 days --- api/experimentation/constants.py | 1 + api/experimentation/tasks.py | 10 ++++++- api/tests/unit/experimentation/test_tasks.py | 30 +++++++++++++++++++ .../observability/_events-catalogue.md | 4 +-- 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/api/experimentation/constants.py b/api/experimentation/constants.py index 561ca2718041..70779adf9bc9 100644 --- a/api/experimentation/constants.py +++ b/api/experimentation/constants.py @@ -15,6 +15,7 @@ CONTROL_VARIANT_KEY = "control" DELIVERY_INTERVAL = timedelta(minutes=10) +DELIVERY_LOG_RETENTION = timedelta(days=60) # Below these per-variant floors a metric shows "collecting data" rather than # inference; sample-ratio is only checked once there is enough traffic to judge. diff --git a/api/experimentation/tasks.py b/api/experimentation/tasks.py index 105dae4d4d71..78abee5723b3 100644 --- a/api/experimentation/tasks.py +++ b/api/experimentation/tasks.py @@ -9,12 +9,13 @@ from environments.models import Environment, EnvironmentAPIKey from experimentation import ingestion_sync_service -from experimentation.constants import DELIVERY_INTERVAL +from experimentation.constants import DELIVERY_INTERVAL, DELIVERY_LOG_RETENTION from experimentation.models import ( Experiment, ExperimentExposures, ExperimentResults, WarehouseConnection, + WarehouseDeliveryLog, WarehouseType, ) from experimentation.organisation_ingestion_service import ( @@ -154,6 +155,13 @@ def deliver_events_for_connection(connection_id: int) -> None: deliver_warehouse_events(connection, bucket_name=infrastructure.bucket_name) +@register_recurring_task(run_every=timedelta(days=1)) +def clean_up_old_warehouse_delivery_logs() -> None: + WarehouseDeliveryLog.objects.filter( + created_at__lt=timezone.now() - DELIVERY_LOG_RETENTION, + ).delete() + + @register_task_handler() def compute_experiment_exposures(experiment_id: int) -> None: experiment = ( diff --git a/api/tests/unit/experimentation/test_tasks.py b/api/tests/unit/experimentation/test_tasks.py index 861adfc0ade5..aa378cd44d0a 100644 --- a/api/tests/unit/experimentation/test_tasks.py +++ b/api/tests/unit/experimentation/test_tasks.py @@ -17,6 +17,7 @@ from environments.models import Environment, EnvironmentAPIKey from experimentation import warehouse_delivery_service +from experimentation.constants import DELIVERY_LOG_RETENTION from experimentation.dataclasses import ( ExposuresSummary, ExposuresTimeseries, @@ -40,6 +41,7 @@ ) from experimentation.stats import VariantStats from experimentation.tasks import ( + clean_up_old_warehouse_delivery_logs, compute_experiment_exposures, compute_experiment_results, deliver_events_for_connection, @@ -1157,3 +1159,31 @@ def test_deliver_events_for_connection__time_budget_exhausted__defers_remaining( "organisation__id": environment.project.organisation_id, "objects__remaining_count": 2, } in log.events + + +def test_clean_up_old_warehouse_delivery_logs__old_and_recent_logs__deletes_only_expired( + clickhouse_connection: WarehouseConnection, + environment: Environment, +) -> None: + # Given a log older than the retention window and a recent one + expired_log = WarehouseDeliveryLog.objects.create( + connection=clickhouse_connection, + s3_key=_pending_key(environment.api_key, hour="13"), + outcome=WarehouseDeliveryOutcome.DELIVERED, + rows_count=100, + ) + WarehouseDeliveryLog.objects.filter(id=expired_log.id).update( + created_at=timezone.now() - DELIVERY_LOG_RETENTION - timedelta(days=1), + ) + recent_log = WarehouseDeliveryLog.objects.create( + connection=clickhouse_connection, + s3_key=_pending_key(environment.api_key, hour="14"), + outcome=WarehouseDeliveryOutcome.REJECTED, + error="Constraint `event_not_empty` violated", + ) + + # When + clean_up_old_warehouse_delivery_logs() + + # Then + assert list(WarehouseDeliveryLog.objects.all()) == [recent_log] diff --git a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md index a074755d17fc..b2b00d168525 100644 --- a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md +++ b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md @@ -91,7 +91,7 @@ Attributes: ### `experimentation.exposures.compute_failed` Logged at `error` from: - - `api/experimentation/tasks.py:181` + - `api/experimentation/tasks.py:189` Attributes: - `environment.id` @@ -159,7 +159,7 @@ Attributes: ### `experimentation.results.compute_failed` Logged at `error` from: - - `api/experimentation/tasks.py:217` + - `api/experimentation/tasks.py:225` Attributes: - `environment.id` From 84494e9de9cfa915d8d0783290a5017326c5e02c Mon Sep 17 00:00:00 2001 From: Gagan Trivedi Date: Fri, 31 Jul 2026 13:48:20 +0530 Subject: [PATCH 3/7] feat(experimentation): index delivery logs by created_at for retention cleanup --- .../migrations/0012_warehouse_delivery_log.py | 7 +++++-- api/experimentation/models.py | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/api/experimentation/migrations/0012_warehouse_delivery_log.py b/api/experimentation/migrations/0012_warehouse_delivery_log.py index 7bb50f6fa987..bccb6ee3f5fa 100644 --- a/api/experimentation/migrations/0012_warehouse_delivery_log.py +++ b/api/experimentation/migrations/0012_warehouse_delivery_log.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.16 on 2026-07-31 07:41 +# Generated by Django 5.2.16 on 2026-07-31 08:17 import django.db.models.deletion from django.db import migrations, models @@ -48,7 +48,10 @@ class Migration(migrations.Migration): models.Index( fields=["connection", "created_at"], name="experimenta_connect_11e6cf_idx", - ) + ), + models.Index( + fields=["created_at"], name="experimenta_created_4fef63_idx" + ), ], }, ), diff --git a/api/experimentation/models.py b/api/experimentation/models.py index 569b71cc404a..8ab02ada4a8c 100644 --- a/api/experimentation/models.py +++ b/api/experimentation/models.py @@ -149,6 +149,8 @@ class WarehouseDeliveryLog(models.Model): class Meta: indexes = [ models.Index(fields=["connection", "created_at"]), + # Serves the retention cleanup, which filters on created_at alone. + models.Index(fields=["created_at"]), ] From 90ffeb4972570f4f041d7a771f389ae0f3e354a5 Mon Sep 17 00:00:00 2001 From: Gagan Trivedi Date: Fri, 31 Jul 2026 14:02:05 +0530 Subject: [PATCH 4/7] refactor(experimentation): make delivery log rows_count non-nullable --- api/experimentation/migrations/0012_warehouse_delivery_log.py | 4 ++-- api/experimentation/models.py | 2 +- api/experimentation/services.py | 1 + api/tests/unit/experimentation/test_tasks.py | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/api/experimentation/migrations/0012_warehouse_delivery_log.py b/api/experimentation/migrations/0012_warehouse_delivery_log.py index bccb6ee3f5fa..ee5d0de019ce 100644 --- a/api/experimentation/migrations/0012_warehouse_delivery_log.py +++ b/api/experimentation/migrations/0012_warehouse_delivery_log.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.16 on 2026-07-31 08:17 +# Generated by Django 5.2.16 on 2026-07-31 08:31 import django.db.models.deletion from django.db import migrations, models @@ -31,7 +31,7 @@ class Migration(migrations.Migration): max_length=50, ), ), - ("rows_count", models.PositiveIntegerField(blank=True, null=True)), + ("rows_count", models.PositiveIntegerField()), ("error", models.TextField(blank=True, null=True)), ("created_at", models.DateTimeField(auto_now_add=True)), ( diff --git a/api/experimentation/models.py b/api/experimentation/models.py index 8ab02ada4a8c..bc2ecf9de700 100644 --- a/api/experimentation/models.py +++ b/api/experimentation/models.py @@ -142,7 +142,7 @@ class WarehouseDeliveryLog(models.Model): max_length=50, choices=WarehouseDeliveryOutcome.choices, ) - rows_count = models.PositiveIntegerField(null=True, blank=True) + rows_count = models.PositiveIntegerField() error = models.TextField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) diff --git a/api/experimentation/services.py b/api/experimentation/services.py index e94da042c0f9..b12c7e63df62 100644 --- a/api/experimentation/services.py +++ b/api/experimentation/services.py @@ -880,6 +880,7 @@ def _deliver_pending_objects( connection=connection, s3_key=s3_key, outcome=WarehouseDeliveryOutcome.REJECTED, + rows_count=0, error=str(exc), ) rejected_count += 1 diff --git a/api/tests/unit/experimentation/test_tasks.py b/api/tests/unit/experimentation/test_tasks.py index aa378cd44d0a..240943359556 100644 --- a/api/tests/unit/experimentation/test_tasks.py +++ b/api/tests/unit/experimentation/test_tasks.py @@ -1015,7 +1015,7 @@ def test_deliver_events_for_connection__rejected_object__moves_to_failed_and_con ( _pending_key(environment.api_key, hour="13"), WarehouseDeliveryOutcome.REJECTED, - None, + 0, "Constraint `event_not_empty` violated", ), ( @@ -1179,6 +1179,7 @@ def test_clean_up_old_warehouse_delivery_logs__old_and_recent_logs__deletes_only connection=clickhouse_connection, s3_key=_pending_key(environment.api_key, hour="14"), outcome=WarehouseDeliveryOutcome.REJECTED, + rows_count=0, error="Constraint `event_not_empty` violated", ) From e48f592f9098aac9feadc8f0501355c8601f72e1 Mon Sep 17 00:00:00 2001 From: "flagsmith-engineering[bot]" Date: Fri, 31 Jul 2026 08:34:03 +0000 Subject: [PATCH 5/7] chore: Update documentation artefacts --- .../observability/_events-catalogue.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md index b2b00d168525..e99b163a91f6 100644 --- a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md +++ b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md @@ -631,7 +631,7 @@ Attributes: ### `warehouse.connection.connected` Logged at `info` from: - - `api/experimentation/services.py:1041` + - `api/experimentation/services.py:1042` Attributes: - `environment.id` @@ -640,7 +640,7 @@ Attributes: ### `warehouse.connection.event_stats_failed` Logged at `warning` from: - - `api/experimentation/services.py:1104` + - `api/experimentation/services.py:1105` Attributes: - `environment.id` @@ -658,7 +658,7 @@ Attributes: ### `warehouse.connection.verification_failed` Logged at `warning` from: - - `api/experimentation/services.py:1016` + - `api/experimentation/services.py:1017` Attributes: - `environment.id` @@ -668,7 +668,7 @@ Attributes: ### `warehouse.connection.verification_succeeded` Logged at `info` from: - - `api/experimentation/services.py:1026` + - `api/experimentation/services.py:1027` Attributes: - `environment.id` @@ -677,7 +677,7 @@ Attributes: ### `warehouse.delivery.all_objects_rejected` Logged at `error` from: - - `api/experimentation/services.py:971` + - `api/experimentation/services.py:972` Attributes: - `connection.id` @@ -699,7 +699,7 @@ Attributes: ### `warehouse.delivery.completed` Logged at `info` from: - - `api/experimentation/services.py:981` + - `api/experimentation/services.py:982` Attributes: - `connection.id` @@ -712,7 +712,7 @@ Attributes: ### `warehouse.delivery.failed` Logged at `error` from: - - `api/experimentation/services.py:954` + - `api/experimentation/services.py:955` Attributes: - `connection.id` @@ -723,7 +723,7 @@ Attributes: ### `warehouse.delivery.object_rejected` Logged at `error` from: - - `api/experimentation/services.py:889` + - `api/experimentation/services.py:890` Attributes: - `connection.id` From 8e95db075f70975dafd5da826a71437d231b0256 Mon Sep 17 00:00:00 2001 From: Gagan Trivedi Date: Fri, 31 Jul 2026 14:18:06 +0530 Subject: [PATCH 6/7] Revert "refactor(experimentation): make delivery log rows_count non-nullable" This reverts commit 90ffeb4972570f4f041d7a771f389ae0f3e354a5. --- api/experimentation/migrations/0012_warehouse_delivery_log.py | 4 ++-- api/experimentation/models.py | 2 +- api/experimentation/services.py | 1 - api/tests/unit/experimentation/test_tasks.py | 3 +-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/api/experimentation/migrations/0012_warehouse_delivery_log.py b/api/experimentation/migrations/0012_warehouse_delivery_log.py index ee5d0de019ce..bccb6ee3f5fa 100644 --- a/api/experimentation/migrations/0012_warehouse_delivery_log.py +++ b/api/experimentation/migrations/0012_warehouse_delivery_log.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.16 on 2026-07-31 08:31 +# Generated by Django 5.2.16 on 2026-07-31 08:17 import django.db.models.deletion from django.db import migrations, models @@ -31,7 +31,7 @@ class Migration(migrations.Migration): max_length=50, ), ), - ("rows_count", models.PositiveIntegerField()), + ("rows_count", models.PositiveIntegerField(blank=True, null=True)), ("error", models.TextField(blank=True, null=True)), ("created_at", models.DateTimeField(auto_now_add=True)), ( diff --git a/api/experimentation/models.py b/api/experimentation/models.py index bc2ecf9de700..8ab02ada4a8c 100644 --- a/api/experimentation/models.py +++ b/api/experimentation/models.py @@ -142,7 +142,7 @@ class WarehouseDeliveryLog(models.Model): max_length=50, choices=WarehouseDeliveryOutcome.choices, ) - rows_count = models.PositiveIntegerField() + rows_count = models.PositiveIntegerField(null=True, blank=True) error = models.TextField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) diff --git a/api/experimentation/services.py b/api/experimentation/services.py index b12c7e63df62..e94da042c0f9 100644 --- a/api/experimentation/services.py +++ b/api/experimentation/services.py @@ -880,7 +880,6 @@ def _deliver_pending_objects( connection=connection, s3_key=s3_key, outcome=WarehouseDeliveryOutcome.REJECTED, - rows_count=0, error=str(exc), ) rejected_count += 1 diff --git a/api/tests/unit/experimentation/test_tasks.py b/api/tests/unit/experimentation/test_tasks.py index 240943359556..aa378cd44d0a 100644 --- a/api/tests/unit/experimentation/test_tasks.py +++ b/api/tests/unit/experimentation/test_tasks.py @@ -1015,7 +1015,7 @@ def test_deliver_events_for_connection__rejected_object__moves_to_failed_and_con ( _pending_key(environment.api_key, hour="13"), WarehouseDeliveryOutcome.REJECTED, - 0, + None, "Constraint `event_not_empty` violated", ), ( @@ -1179,7 +1179,6 @@ def test_clean_up_old_warehouse_delivery_logs__old_and_recent_logs__deletes_only connection=clickhouse_connection, s3_key=_pending_key(environment.api_key, hour="14"), outcome=WarehouseDeliveryOutcome.REJECTED, - rows_count=0, error="Constraint `event_not_empty` violated", ) From 70b890edbfe395928633e94580df16e483fa6ded Mon Sep 17 00:00:00 2001 From: "flagsmith-engineering[bot]" Date: Fri, 31 Jul 2026 08:50:37 +0000 Subject: [PATCH 7/7] chore: Update documentation artefacts --- .../observability/_events-catalogue.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md index e99b163a91f6..b2b00d168525 100644 --- a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md +++ b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md @@ -631,7 +631,7 @@ Attributes: ### `warehouse.connection.connected` Logged at `info` from: - - `api/experimentation/services.py:1042` + - `api/experimentation/services.py:1041` Attributes: - `environment.id` @@ -640,7 +640,7 @@ Attributes: ### `warehouse.connection.event_stats_failed` Logged at `warning` from: - - `api/experimentation/services.py:1105` + - `api/experimentation/services.py:1104` Attributes: - `environment.id` @@ -658,7 +658,7 @@ Attributes: ### `warehouse.connection.verification_failed` Logged at `warning` from: - - `api/experimentation/services.py:1017` + - `api/experimentation/services.py:1016` Attributes: - `environment.id` @@ -668,7 +668,7 @@ Attributes: ### `warehouse.connection.verification_succeeded` Logged at `info` from: - - `api/experimentation/services.py:1027` + - `api/experimentation/services.py:1026` Attributes: - `environment.id` @@ -677,7 +677,7 @@ Attributes: ### `warehouse.delivery.all_objects_rejected` Logged at `error` from: - - `api/experimentation/services.py:972` + - `api/experimentation/services.py:971` Attributes: - `connection.id` @@ -699,7 +699,7 @@ Attributes: ### `warehouse.delivery.completed` Logged at `info` from: - - `api/experimentation/services.py:982` + - `api/experimentation/services.py:981` Attributes: - `connection.id` @@ -712,7 +712,7 @@ Attributes: ### `warehouse.delivery.failed` Logged at `error` from: - - `api/experimentation/services.py:955` + - `api/experimentation/services.py:954` Attributes: - `connection.id` @@ -723,7 +723,7 @@ Attributes: ### `warehouse.delivery.object_rejected` Logged at `error` from: - - `api/experimentation/services.py:890` + - `api/experimentation/services.py:889` Attributes: - `connection.id`