Skip to content

feat(cohorts): Amplitude cohort sync endpoints and sync keys - #8290

Open
gagantrivedi wants to merge 10 commits into
mainfrom
feat/cohort-sync-amplitude
Open

feat(cohorts): Amplitude cohort sync endpoints and sync keys#8290
gagantrivedi wants to merge 10 commits into
mainfrom
feat/cohort-sync-amplitude

Conversation

@gagantrivedi

@gagantrivedi gagantrivedi commented Aug 14, 2026

Copy link
Copy Markdown
Member
  • I have read the Contributing Guide.
  • I have added information to docs/ if required so people know about the feature.
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

Changes

Contributes to Flagsmith/flagsmith-private#260.

The receiving side of Amplitude's list-based cohort sync contract, feeding the existing membership ledger. Spec copied from Create a cohort sync integration and Receiving behavioral cohorts — routes, field names and status codes are fixed by Amplitude.

  • CohortSyncKey: per-environment bearer credential, hashed at rest, plaintext returned once; managed at /environments/<key>/cohorts/sync-keys/.
  • /cohort-sync/amplitude/lists/ creates the cohort and returns list_id; lists/<uuid>/add|remove/ turn {"user_ids": [...]} into pending ledger rows for the existing apply task.
  • Cohorts created this way write their own audit log, which is also what rebuilds the environment document.

How did you test this code?

Unit tests cover the auth matrix, list creation, add/remove reaching (moto) DynamoDB via the apply task, the 404 fences, and key management.

Beyond that, this can only really be tested once Amplitude grants us Integration Portal access: the endpoints are exercised for real by Amplitude's own calls, first through the portal's testing tab and Release internally mode, then in production.

@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

3 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs Ignored Ignored Preview Aug 19, 2026 9:11am
flagsmith-frontend-preview Ignored Ignored Preview Aug 19, 2026 9:11am
flagsmith-frontend-staging Ignored Ignored Preview Aug 19, 2026 9:11am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0b5fe3df-e89f-404d-8f3e-edbf412d7d55

📥 Commits

Reviewing files that changed from the base of the PR and between 3964c41 and 040cac7.

📒 Files selected for processing (2)
  • mcp/src/flagsmith_mcp/openapi.json
  • openapi.yaml

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Adds Bearer-token authentication and environment-scoped lifecycle management for cohort sync keys. Adds Amplitude cohort creation and member add/remove endpoints. Persists source types, records audit events, updates membership state, and documents the API and security scheme. Adds unit tests for authentication, validation, environment isolation, membership updates, audit records, and key revocation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: 🟠 High · up to 040ca

The new Amplitude cohort-sync endpoints can acknowledge requests that later fail to apply membership changes, especially for oversized identifiers or removals, unavailable synchronization services, transaction timing failures, or audit-write failures; the API schema also does not fully match runtime requirements. These are high-impact correctness and data-consistency risks, so the PR is not ready to merge without fixes or explicit acceptance.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added api Issue related to the REST API docs Documentation updates feature New feature or request and removed docs Documentation updates labels Aug 14, 2026
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.79%. Comparing base (65ce12c) to head (040cac7).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8290      +/-   ##
==========================================
+ Coverage   98.64%   98.79%   +0.14%     
==========================================
  Files        1609     1614       +5     
  Lines       64570    64934     +364     
==========================================
+ Hits        63696    64150     +454     
+ Misses        874      784      -90     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions Bot added feature New feature or request docs Documentation updates and removed feature New feature or request docs Documentation updates labels Aug 17, 2026
@github-actions github-actions Bot added feature New feature or request docs Documentation updates and removed feature New feature or request docs Documentation updates labels Aug 17, 2026
@gagantrivedi
gagantrivedi marked this pull request as ready for review August 17, 2026 09:29
@gagantrivedi
gagantrivedi requested review from a team as code owners August 17, 2026 09:29
@gagantrivedi
gagantrivedi removed the request for review from a team August 17, 2026 09:29
@github-actions github-actions Bot added the feature New feature or request label Aug 17, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5882fd93-bbb9-44ac-bfa1-bfd21628caaf

📥 Commits

Reviewing files that changed from the base of the PR and between 0ef78e8 and 9902b86.

📒 Files selected for processing (16)
  • api/api/urls/v1.py
  • api/cohorts/authentication.py
  • api/cohorts/migrations/0003_cohort_sync_key.py
  • api/cohorts/models.py
  • api/cohorts/permissions.py
  • api/cohorts/serializers.py
  • api/cohorts/services.py
  • api/cohorts/sync_urls.py
  • api/cohorts/sync_views.py
  • api/cohorts/urls.py
  • api/cohorts/views.py
  • api/tests/unit/cohorts/conftest.py
  • api/tests/unit/cohorts/test_sync_views.py
  • api/tests/unit/cohorts/test_views.py
  • docs/docs/deployment-self-hosting/observability/_events-catalogue.md
  • openapi.yaml

Included review availability: Your plan includes up to 8 reviews per rolling hour; 7 remain after this review.

Comment thread api/cohorts/serializers.py
Comment thread api/cohorts/services.py
Comment thread api/cohorts/sync_views.py
Comment thread openapi.yaml
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor
✅ oss · depot-ubuntu-latest-arm-16 — run #19533 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  44.6 seconds
commit  040cac7
info  🔄 Run: #19533 (attempt 1)

🗂️ Previous results
✅ oss · depot-ubuntu-latest-16 — run #19533 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  41.2 seconds
commit  040cac7
info  🔄 Run: #19533 (attempt 1)

✅ private-cloud · depot-ubuntu-latest-arm-16 — run #19532 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  36.5 seconds
commit  3964c41
info  🔄 Run: #19532 (attempt 1)

✅ private-cloud · depot-ubuntu-latest-16 — run #19532 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  33.1 seconds
commit  3964c41
info  🔄 Run: #19532 (attempt 1)

✅ oss · depot-ubuntu-latest-arm-16 — run #19532 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  44.3 seconds
commit  3964c41
info  🔄 Run: #19532 (attempt 1)

✅ oss · depot-ubuntu-latest-16 — run #19532 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  34 seconds
commit  3964c41
info  🔄 Run: #19532 (attempt 1)

❌ private-cloud · depot-ubuntu-latest-16 — run #19458 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

failed  2 failed

Details

stats  2 tests across 2 suites
duration  1 minute, 1 second
commit  cc6791e
info  📦 Artifacts: View test results and HTML report
🔄 Run: #19458 (attempt 1)

Failed tests

firefox › tests/environment-permission-test.pw.ts › Environment Permission Tests › Environment-level permissions control access to features, identities, and segments @enterprise
firefox › tests/project-permission-test.pw.ts › Project Permission Tests › Project-level permissions control access to features, environments, audit logs, and segments @enterprise

✅ private-cloud · depot-ubuntu-latest-arm-16 — run #19458 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  1 minute, 3 seconds
commit  cc6791e
info  🔄 Run: #19458 (attempt 1)

✅ oss · depot-ubuntu-latest-arm-16 — run #19458 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  45.7 seconds
commit  cc6791e
info  🔄 Run: #19458 (attempt 1)

✅ oss · depot-ubuntu-latest-arm-16 — run #19459 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  40.5 seconds
commit  b439ee4
info  🔄 Run: #19459 (attempt 1)

✅ oss · depot-ubuntu-latest-16 — run #19458 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  40.5 seconds
commit  cc6791e
info  🔄 Run: #19458 (attempt 1)

✅ oss · depot-ubuntu-latest-16 — run #19459 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  40.3 seconds
commit  b439ee4
info  🔄 Run: #19459 (attempt 1)

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Visual Regression

19 screenshots compared. See report for details.
View full report

@github-actions github-actions Bot added docs Documentation updates feature New feature or request and removed feature New feature or request docs Documentation updates labels Aug 17, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
openapi.yaml (2)

1528-1533: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Declare list IDs as UUIDs.

AmplitudeCohortSyncViewSet._get_cohort parses the path value with uuid.UUID and returns 404 for non-UUID values, as shown in api/cohorts/sync_views.py, Lines [38-88]. The add and remove parameters currently declare only type: string. Add format: uuid to both schemas so generated clients use the server's actual contract.

Proposed OpenAPI fix
           schema:
             type: string
+            format: uuid

Apply this to both id path parameters.

Also applies to: 1556-1560


19015-19045: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Align the public identifier limit with persistence.

CohortSyncMembers.user_ids[*] permits up to 2000 characters, while CohortMembership.identifier has a 1024-byte limit. The supplied api/cohorts/serializers.py, Lines [63-71], confirms that a multibyte identifier can pass validation and fail when the membership row is written. A documented request can therefore fail after acceptance.

Enforce the byte-length limit before creating the pending membership row, then publish the same effective constraint in this schema. Do not fix this only by reducing the character limit.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 91f1770b-4706-4a6f-8e3b-db3ad55bad7d

📥 Commits

Reviewing files that changed from the base of the PR and between cc6791e and b439ee4.

📒 Files selected for processing (2)
  • mcp/src/flagsmith_mcp/openapi.json
  • openapi.yaml

Included review availability: Your plan includes up to 8 reviews per rolling hour; 7 remain after this review.

@gagantrivedi
gagantrivedi requested review from khvn26 and removed request for emyller August 18, 2026 07:10
@gagantrivedi
gagantrivedi force-pushed the feat/cohort-sync-amplitude branch from b439ee4 to 3964c41 Compare August 19, 2026 09:08
@github-actions github-actions Bot added feature New feature or request and removed feature New feature or request docs Documentation updates labels Aug 19, 2026
@github-actions github-actions Bot added the docs Documentation updates label Aug 19, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
openapi.yaml (1)

1661-1737: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Bearer authentication is still undocumented for the Amplitude cohort-sync operations.

The three operations api_v1_cohort_sync_amplitude_lists_create, api_v1_cohort_sync_amplitude_lists_add_create, and api_v1_cohort_sync_amplitude_lists_remove_create have no security field. CohortSyncKeyAuthentication requires Authorization: Bearer <sync-key> for these endpoints, per the PR objectives.

A prior review thread on this file flagged the identical issue and recorded it as resolved by a commit that added a "Cohort Sync Key" security-scheme extension in api/api/openapi.py, with the note that CI regenerates this root openapi.yaml automatically from that source. In this file version, the fix has not taken effect:

  • Lines 1661-1737 still show no security field on the three operations.
  • The securitySchemes block (around line 29354) still lists only Environment API Key, Master API Key, basicAuth, and tokenAuth. No "Cohort Sync Key" scheme is defined.

Confirm that the authentication extension still exists and is wired to these operations, and that the generation step that produces this file actually runs before merge.

#!/bin/bash
# Description: Verify the Cohort Sync Key OpenAPI authentication extension exists and is applied.
set -euo pipefail

echo '--- Check for the authentication extension in api/api/openapi.py ---'
fd -a openapi.py api/api | xargs -r rg -n -C 5 'CohortSyncKeyAuthentication|OpenApiAuthenticationExtension|Cohort Sync Key'

echo '--- Check CohortSyncKeyAuthentication usage in cohorts sync views ---'
fd -a sync_views.py api/cohorts | xargs -r rg -n -C 3 'authentication_classes|CohortSyncKeyAuthentication'

echo '--- Check sdk/openapi.yaml for the scheme ---'
rg -n -C 3 'Cohort Sync Key' sdk/openapi.yaml || echo 'not found in sdk/openapi.yaml'

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 94df6b2d-a068-4e8b-a583-933430a093e8

📥 Commits

Reviewing files that changed from the base of the PR and between b439ee4 and 3964c41.

📒 Files selected for processing (7)
  • api/cohorts/services.py
  • api/cohorts/sync_views.py
  • api/cohorts/views.py
  • api/tests/unit/cohorts/test_sync_views.py
  • api/tests/unit/cohorts/test_views.py
  • docs/docs/deployment-self-hosting/observability/_events-catalogue.md
  • openapi.yaml

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Comment thread api/cohorts/services.py
Comment on lines +122 to +145
def create_cohort_for_source(
*,
environment: "Environment",
name: str,
source_type: CohortSourceType,
) -> Cohort:
"""Create a cohort on behalf of an external source, where no Flagsmith
user is acting."""
cohort = create_cohort(environment=environment, name=name, source_type=source_type)
# Nothing records a user for these calls, so the audit log that Flagsmith
# derives from historical records is skipped — and with it the environment
# document rebuild that makes the new segment visible to SDKs. Write the
# record here instead, naming the source that asked for the cohort.
AuditLog.objects.create(
environment=environment,
project=environment.project,
related_object_id=cohort.segment_id,
related_object_type=RelatedObjectType.SEGMENT.name,
log=(
f"{SEGMENT_CREATED_MESSAGE % cohort.segment.name} "
f"(via {CohortSourceType(source_type).label} cohort sync)"
),
)
return cohort

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make source cohort creation and audit creation atomic.

create_cohort() commits before AuditLog.objects.create() runs. If the audit write fails, the endpoint returns an error but leaves a cohort without the audit event that rebuilds the environment document. A retry can then create another cohort.

Wrap both operations in an outer transaction.atomic() block. Add a regression test that makes the audit write fail and asserts that no cohort remains.

Proposed fix
 def create_cohort_for_source(...):
-    cohort = create_cohort(...)
-    AuditLog.objects.create(...)
+    with transaction.atomic():
+        cohort = create_cohort(...)
+        AuditLog.objects.create(...)
     return cohort
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def create_cohort_for_source(
*,
environment: "Environment",
name: str,
source_type: CohortSourceType,
) -> Cohort:
"""Create a cohort on behalf of an external source, where no Flagsmith
user is acting."""
cohort = create_cohort(environment=environment, name=name, source_type=source_type)
# Nothing records a user for these calls, so the audit log that Flagsmith
# derives from historical records is skipped — and with it the environment
# document rebuild that makes the new segment visible to SDKs. Write the
# record here instead, naming the source that asked for the cohort.
AuditLog.objects.create(
environment=environment,
project=environment.project,
related_object_id=cohort.segment_id,
related_object_type=RelatedObjectType.SEGMENT.name,
log=(
f"{SEGMENT_CREATED_MESSAGE % cohort.segment.name} "
f"(via {CohortSourceType(source_type).label} cohort sync)"
),
)
return cohort
def create_cohort_for_source(
*,
environment: "Environment",
name: str,
source_type: CohortSourceType,
) -> Cohort:
"""Create a cohort on behalf of an external source, where no Flagsmith
user is acting."""
with transaction.atomic():
cohort = create_cohort(environment=environment, name=name, source_type=source_type)
# Nothing records a user for these calls, so the audit log that Flagsmith
# derives from historical records is skipped — and with it the environment
# document rebuild that makes the new segment visible to SDKs. Write the
# record here instead, naming the source that asked for the cohort.
AuditLog.objects.create(
environment=environment,
project=environment.project,
related_object_id=cohort.segment_id,
related_object_type=RelatedObjectType.SEGMENT.name,
log=(
f"{SEGMENT_CREATED_MESSAGE % cohort.segment.name} "
f"(via {CohortSourceType(source_type).label} cohort sync)"
),
)
return cohort

Comment thread api/cohorts/services.py
Comment on lines +177 to +186
def remove_cohort_members(cohort: Cohort, identifiers: "typing.Iterable[str]") -> None:
from cohorts.tasks import apply_cohort_membership_deltas

unique_identifiers = set(identifiers)
with transaction.atomic():
# Removing a non-member is a no-op: only existing rows flip.
matched = CohortMembership.objects.filter(
cohort=cohort, identifier__in=unique_identifiers
).update(state=CohortMembershipState.PENDING_REMOVE, updated_at=timezone.now())
apply_cohort_membership_deltas.delay(kwargs={"cohort_id": cohort.id})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
git ls-files 'api/cohorts/services.py' 'api/cohorts/tests*' 'api/**' | sed -n '1,120p'

printf '%s\n' '--- service structure ---'
ast-grep outline api/cohorts/services.py --view expanded | sed -n '1,220p'

printf '%s\n' '--- relevant service code ---'
sed -n '1,240p' api/cohorts/services.py

printf '%s\n' '--- references to removal and batching ---'
rg -n -C 4 'remove_cohort_members|batch_size|identifier__in|apply_cohort_membership_deltas' api

Repository: Flagsmith/flagsmith

Length of output: 35347


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- cohort tests and API validation ---'
fd -i 'cohort' api/tests api/cohorts | sed -n '1,160p'
rg -n -C 5 'CohortSyncMembersSerializer|user_ids|add_cohort_members|remove_cohort_members' api/tests api/cohorts

printf '%s\n' '--- model and database configuration ---'
rg -n -C 8 'class CohortMembership|identifier\s*=|DATABASES|ENGINE|postgres|PostgreSQL|psycopg' api/cohorts api/app api/requirements* api/pyproject.toml api/setup.cfg api/tox.ini 2>/dev/null || true

printf '%s\n' '--- relevant constants and task behaviour ---'
cat -n api/cohorts/constants.py
sed -n '1,90p' api/cohorts/tasks.py

printf '%s\n' '--- tracked dependency/config files ---'
git ls-files | rg '(^|/)(requirements[^/]*|pyproject\.toml|setup\.cfg|tox\.ini|Pipfile|.*settings.*\.py)$' | sed -n '1,160p'

Repository: Flagsmith/flagsmith

Length of output: 49639


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

services = Path("api/cohorts/services.py").read_text()
serializers = Path("api/cohorts/serializers.py").read_text()
settings = Path("api/app/settings/common.py").read_text()
models = Path("api/cohorts/models.py").read_text()

print("serializer_list_max_length=",
      re.search(r"user_ids\s*=\s*serializers\.ListField\((.*?)\n\s*\)", serializers, re.S).group(1).find("max_length=") >= 0)
print("service_uses_set=", "unique_identifiers = set(identifiers)" in services)
print("service_uses_single_identifier_in=",
      "identifier__in=unique_identifiers" in services)
print("default_database_is_postgresql=",
      '"ENGINE": "django.db.backends.postgresql"' in settings)
print("membership_identifier_max_length=",
      re.search(r"identifier\s*=\s*models\.CharField\(max_length=(\d+)\)", models).group(1))
print("add_batch_size=",
      re.search(r"bulk_create\(\s*rows,.*?batch_size=(\d+)", services, re.S).group(1))

# PostgreSQL's extended-query protocol uses an Int16 parameter count.
postgresql_bind_limit = 2**16 - 1
# The update binds at least one value per identifier, plus the cohort
# predicate and update values. This lower bound is sufficient to show that
# an unbounded IN list can exceed the protocol limit.
fixed_update_parameters = 3  # cohort, state, updated_at
for identifiers in (postgresql_bind_limit - fixed_update_parameters,
                    postgresql_bind_limit - fixed_update_parameters + 1):
    total = identifiers + fixed_update_parameters
    print(f"identifiers={identifiers} total_update_parameters={total} "
          f"within_limit={total <= postgresql_bind_limit}")
PY

printf '%s\n' '--- exact removal tests ---'
sed -n '180,250p' api/tests/unit/cohorts/test_sync_views.py

printf '%s\n' '--- model uniqueness and database routers ---'
sed -n '66,105p' api/cohorts/models.py
rg -n -C 5 'class .*Router|db_for_write|allow_migrate|default' api/app/routers.py api/task_processor/routers.py api/cohorts 2>/dev/null || true

Repository: Flagsmith/flagsmith

Length of output: 17487


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

services = Path("api/cohorts/services.py").read_text()
serializers = Path("api/cohorts/serializers.py").read_text()
settings = Path("api/app/settings/common.py").read_text()
models = Path("api/cohorts/models.py").read_text()

print("serializer_list_max_length=",
      re.search(r"user_ids\s*=\s*serializers\.ListField\((.*?)\n\s*\)", serializers, re.S).group(1).find("max_length=") >= 0)
print("service_uses_set=", "unique_identifiers = set(identifiers)" in services)
print("service_uses_single_identifier_in=",
      "identifier__in=unique_identifiers" in services)
print("default_database_is_postgresql=",
      '"ENGINE": "django.db.backends.postgresql"' in settings)
print("membership_identifier_max_length=",
      re.search(r"identifier\s*=\s*models\.CharField\(max_length=(\d+)\)", models).group(1))
print("add_batch_size=",
      re.search(r"bulk_create\(\s*rows,.*?batch_size=(\d+)", services, re.S).group(1))

postgresql_bind_limit = 2**16 - 1
fixed_update_parameters = 3
for identifiers in (postgresql_bind_limit - fixed_update_parameters,
                    postgresql_bind_limit - fixed_update_parameters + 1):
    total = identifiers + fixed_update_parameters
    print(f"identifiers={identifiers} total_update_parameters={total} "
          f"within_limit={total <= postgresql_bind_limit}")
PY

printf '%s\n' '--- exact removal tests ---'
sed -n '180,250p' api/tests/unit/cohorts/test_sync_views.py

printf '%s\n' '--- model uniqueness and database routers ---'
sed -n '66,105p' api/cohorts/models.py
rg -n -C 5 'class .*Router|db_for_write|allow_migrate|default' api/app/routers.py api/task_processor/routers.py api/cohorts 2>/dev/null || true

Repository: Flagsmith/flagsmith

Length of output: 17487


Batch membership removals.

The unbounded identifier__in query can exceed PostgreSQL’s 65,535-parameter limit and return HTTP 500. Split unique_identifiers into bounded chunks, such as 1,000, within the transaction. Accumulate matched across updates and dispatch the task once. Add a regression test with more than one chunk.

Comment thread api/cohorts/sync_views.py
Comment on lines +55 to +64
serializer = CohortSyncMembersSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
services.add_cohort_members(cohort, serializer.validated_data["user_ids"])
return Response()

@action(detail=True, methods=["POST"])
def remove(self, request: Request, pk: str) -> Response:
cohort = self._get_cohort(request, pk)
serializer = CohortSyncMembersSerializer(data=request.data)
serializer.is_valid(raise_exception=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject identifiers above 1,024 bytes before queueing membership work.

CohortSyncMembersSerializer accepts up to 2,000 characters. A multibyte user_id can therefore pass Lines 55 and 63, persist in CohortMembership, and return HTTP 200 before the asynchronous Edge write rejects it.

Validate UTF-8 byte length in the shared serializer and return HTTP 400. Apply the same validation to CSV membership input. Add a regression test with a value above 1,024 bytes.

Based on learnings, cohort identifiers must be rejected above 1,024 bytes with HTTP 400 to match Edge DynamoDB constraints.

Source: Learnings

Comment on lines +132 to +143
### `cohorts.membership.deltas_received`

Logged at `info` from:
- `api/cohorts/services.py:168`
- `api/cohorts/services.py:187`

Attributes:
- `action`
- `cohort.id`
- `deltas.count`
- `environment.id`
- `members.matched`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document members.matched as a remove-only attribute.

add_cohort_members does not emit members.matched. Only remove_cohort_members emits that attribute. Mark it as remove-only, or split the add and remove event contracts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Issue related to the REST API docs Documentation updates feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant