diff --git a/api/cohorts/sync_views.py b/api/cohorts/sync_views.py index 3c4aa0fadb45..1f8e1c51f588 100644 --- a/api/cohorts/sync_views.py +++ b/api/cohorts/sync_views.py @@ -22,7 +22,13 @@ ) _LIST_RESPONSE = inline_serializer( - "AmplitudeListResponse", {"listId": serializers.UUIDField()} + "AmplitudeListResponse", + { + "list_id": serializers.UUIDField(), + "response": inline_serializer( + "AmplitudeListResponseEnvelope", {"list_id": serializers.UUIDField()} + ), + }, ) _MIXPANEL_RESPONSE = inline_serializer( @@ -73,10 +79,15 @@ def create(self, request: Request) -> Response: name=serializer.validated_data["name"], source_type=CohortSourceType.AMPLITUDE, ) - # Amplitude's production sync worker reads the list ID from its - # documented default key, camelCase "listId", ignoring the response - # path configured in the Integration Portal. - return Response({"listId": str(cohort.uuid)}) + # Amplitude reads the list ID from this body at the path configured + # in the Integration Portal, which their portal requires to start + # with "response." — but the two sides of Amplitude disagree on what + # that path is applied to. Their Testing tab wraps this body in a + # {"response": ...} envelope first, so it finds the flat copy; their + # production sync worker applies the same path to the raw body, so + # it needs the nested copy. Verified against both, 2026-08-28. + list_id = str(cohort.uuid) + return Response({"list_id": list_id, "response": {"list_id": list_id}}) @action(detail=True, methods=["POST"]) def add(self, request: Request, pk: str) -> Response: diff --git a/api/tests/unit/cohorts/test_sync_views.py b/api/tests/unit/cohorts/test_sync_views.py index cbc5a87bac97..a2556bea3458 100644 --- a/api/tests/unit/cohorts/test_sync_views.py +++ b/api/tests/unit/cohorts/test_sync_views.py @@ -50,7 +50,9 @@ def test_amplitude_create_list__valid_key__creates_amplitude_cohort( # Then assert response.status_code == status.HTTP_200_OK - cohort = Cohort.objects.get(uuid=response.json()["listId"]) + cohort = Cohort.objects.get(uuid=response.json()["list_id"]) + # Amplitude's Testing tab reads the flat copy, production the nested one. + assert response.json()["response"]["list_id"] == response.json()["list_id"] assert cohort.environment == key.environment assert cohort.source_type == CohortSourceType.AMPLITUDE assert cohort.segment.name == "[Amplitude] Beta users: 1234" @@ -74,7 +76,7 @@ def test_amplitude_create_list__postgres_environment__creates_cohort( # Then assert response.status_code == status.HTTP_200_OK - cohort = Cohort.objects.get(uuid=response.json()["listId"]) + cohort = Cohort.objects.get(uuid=response.json()["list_id"]) assert cohort.environment == environment @@ -350,7 +352,7 @@ def test_amplitude_create_list__valid_key__audits_and_queues_environment_update( # Then - the audit record carries no user, names the source, and is the # hook that rebuilds the environment document. assert response.status_code == status.HTTP_200_OK - cohort = Cohort.objects.get(uuid=response.json()["listId"]) + cohort = Cohort.objects.get(uuid=response.json()["list_id"]) audit_log = AuditLog.objects.get(related_object_id=cohort.segment_id) assert audit_log.author is None assert audit_log.master_api_key is None @@ -377,7 +379,7 @@ def test_amplitude_create_list__valid_key__history_records_no_user( # Then - a machine caller leaves no user on historical records; stamping # one would fail, since the sync key is not a Flagsmith user. assert response.status_code == status.HTTP_200_OK - cohort = Cohort.objects.get(uuid=response.json()["listId"]) + cohort = Cohort.objects.get(uuid=response.json()["list_id"]) history_record = cohort.segment.history.get() assert history_record.history_user is None assert history_record.master_api_key is None diff --git a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md index 4e77c78106c0..9c89874b5296 100644 --- a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md +++ b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md @@ -166,7 +166,7 @@ Attributes: ### `cohorts.sync_webhook.rejected` Logged at `warning` from: - - `api/cohorts/sync_views.py:217` + - `api/cohorts/sync_views.py:228` Attributes: - `action` diff --git a/openapi.yaml b/openapi.yaml index 78578f6e6a84..7481be959734 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -18943,11 +18943,22 @@ components: AmplitudeListResponse: type: object properties: - listId: + list_id: type: string format: uuid + response: + $ref: '#/components/schemas/AmplitudeListResponseEnvelope' required: - - listId + - list_id + - response + AmplitudeListResponseEnvelope: + type: object + properties: + list_id: + type: string + format: uuid + required: + - list_id AuditLogList: type: object properties: