Skip to content

feat: cohort CSV sync endpoint and cohort summary on segments - #8294

Open
Zaimwa9 wants to merge 2 commits into
feat/create-segment-from-csvfrom
feat/cohort-csv-sync
Open

feat: cohort CSV sync endpoint and cohort summary on segments#8294
Zaimwa9 wants to merge 2 commits into
feat/create-segment-from-csvfrom
feat/cohort-csv-sync

Conversation

@Zaimwa9

@Zaimwa9 Zaimwa9 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Thanks for submitting a PR! Please check the boxes below:

  • 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

Adds the API surface the dashboard needs to create segments from a CSV of identifiers, on top of the cohort CRUD from #8248.

CSV sync endpoint

  • POST /api/v1/environments/{api_key}/cohorts/{id}/sync-csv/: multipart upload with file, identifier_column (0-based, defaults to the first column) and has_header (defaults to true).
  • Declarative replace: the uploaded identifiers become the cohort's desired membership. Responds 202 with {version, added, removed, unchanged, ignored: {empty, duplicates, too_long}} and queues the membership delta application inside the transaction, with a row lock on the cohort and a version bump.
  • The file is parsed and discarded, never stored. Files over 10MB are rejected with 413.

Cohort creation

  • POST .../cohorts/ now accepts segment metadata (custom fields), applied to the managed segment.

Segments API

  • Segment payloads carry a read-only cohort summary (id, environment, source_type, version, deletion_requested_at), prefetched on the list view. This lets the dashboard tag CSV segments, route their deletion through the cohorts endpoint, and grey out segments awaiting drain.

Observability

  • New metrics flagsmith_cohorts_csv_syncs_total and flagsmith_cohorts_csv_sync_identifiers, new cohorts.csv.synced event, both documented in the observability catalogues.

How did you test this code?

  • Unit tests for the CSV extraction and sync services (delta semantics, duplicates, empty rows, too-long identifiers, version bumps), the sync-csv view (validation, size cap, permissions), metadata on create, and the segment list cohort summary.
  • Manually through the dashboard drawer (stacked frontend branch): created cohorts from CSV files, verified membership rows and delta counts in the database, re-synced and deleted cohorts.

@Zaimwa9
Zaimwa9 requested review from a team as code owners August 14, 2026 14:33
@Zaimwa9
Zaimwa9 requested review from Holmus and gagantrivedi and removed request for a team August 14, 2026 14:33
@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 14, 2026 3:57pm
flagsmith-frontend-preview Ignored Ignored Preview Aug 14, 2026 3:57pm
flagsmith-frontend-staging Ignored Ignored Preview Aug 14, 2026 3:57pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7fbdcb20-efcb-424c-b34d-3630d1a233f3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds multipart CSV synchronisation for cohorts. The change validates CSV size and content, extracts identifiers, updates memberships in batches, tracks ignored rows, increments cohort versions, and queues delta application. Cohort creation now accepts and persists metadata. Segment responses expose related cohort details. The change adds metrics, event catalogue entries, tests, and OpenAPI schemas.

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

Merge Risk: 🟠 High · up to aa61a

The CSV synchronization feature can strand membership updates, accept identifiers that downstream processing cannot handle, or leave external membership state inconsistent with the database under concurrent sync and application. These correctness and data-integrity risks make the PR unsafe to merge until the synchronization and validation paths are fixed.


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
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Docker builds report

Image Build Status Security report
ghcr.io/flagsmith/flagsmith-e2e:pr-8294 Finished ✅ Skipped
ghcr.io/flagsmith/flagsmith-api-test:pr-8294 Finished ✅ Skipped
ghcr.io/flagsmith/flagsmith-frontend:pr-8294 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-api:pr-8294 Finished ✅ Results
ghcr.io/flagsmith/flagsmith:pr-8294 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-private-cloud:pr-8294 Finished ✅ Results

@Zaimwa9

Zaimwa9 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

@themis-blindfold review

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor
✅ private-cloud · depot-ubuntu-latest-arm-16 — run #19370 (attempt 1)

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

passed  2 passed

Details

stats  2 tests across 2 suites
duration  38.9 seconds
commit  3a5f36a
info  🔄 Run: #19370 (attempt 1)

🗂️ Previous results
✅ private-cloud · depot-ubuntu-latest-16 — run #19370 (attempt 1)

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

passed  2 passed

Details

stats  2 tests across 2 suites
duration  35 seconds
commit  3a5f36a
info  🔄 Run: #19370 (attempt 1)

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

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  46.8 seconds
commit  3a5f36a
info  🔄 Run: #19370 (attempt 1)

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

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  44.9 seconds
commit  fd0adac
info  🔄 Run: #19373 (attempt 1)

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

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  41.3 seconds
commit  3a5f36a
info  🔄 Run: #19370 (attempt 1)

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

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  41.4 seconds
commit  fd0adac
info  🔄 Run: #19373 (attempt 1)

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

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  55.2 seconds
commit  4083994
info  🔄 Run: #19365 (attempt 1)

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

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  46.1 seconds
commit  71a387d
info  🔄 Run: #19366 (attempt 1)

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

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

passed  4 passed

Details

stats  4 tests across 3 suites
duration  38.4 seconds
commit  4083994
info  🔄 Run: #19365 (attempt 1)

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

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  40.2 seconds
commit  71a387d
info  🔄 Run: #19366 (attempt 1)

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

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  47.1 seconds
commit  4083994
info  🔄 Run: #19365 (attempt 1)

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

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  32.4 seconds
commit  4083994
info  🔄 Run: #19365 (attempt 1)

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Visual Regression

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

@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: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6fc61ada-1f5d-43a2-8a9d-94781de30573

📥 Commits

Reviewing files that changed from the base of the PR and between 05b2fcf and aa61a35.

📒 Files selected for processing (17)
  • api/cohorts/constants.py
  • api/cohorts/dataclasses.py
  • api/cohorts/exceptions.py
  • api/cohorts/metrics.py
  • api/cohorts/serializers.py
  • api/cohorts/services.py
  • api/cohorts/views.py
  • api/segments/serializers.py
  • api/segments/views.py
  • api/tests/unit/cohorts/conftest.py
  • api/tests/unit/cohorts/test_services.py
  • api/tests/unit/cohorts/test_views.py
  • api/tests/unit/segments/test_unit_segments_views.py
  • docs/docs/deployment-self-hosting/observability/_events-catalogue.md
  • docs/docs/deployment-self-hosting/observability/_metrics-catalogue.md
  • mcp/src/flagsmith_mcp/openapi.json
  • openapi.yaml

Comment thread api/cohorts/services.py Outdated
Comment on lines +135 to +154
max_length: int = CohortMembership._meta.get_field("identifier").max_length # type: ignore[assignment]
# The upload size cap keeps a full read cheap; utf-8-sig strips any BOM.
text = io.StringIO(file.read().decode("utf-8-sig", errors="replace"), newline="")
reader = csv.reader(text)
seen: set[str] = set()
identifiers: list[str] = []
empty_count = duplicate_count = too_long_count = 0
try:
for row_number, row in enumerate(reader):
if has_header and row_number == 0:
continue
if not row:
continue
value = (
row[identifier_column].strip() if identifier_column < len(row) else ""
)
if not value:
empty_count += 1
elif len(value) > max_length:
too_long_count += 1

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

Enforce the Edge identifier byte limit before persistence.

Line 135 checks the model character limit. It does not enforce the 1,024-byte Edge limit. For example, a 600-emoji identifier passes this check but uses 2,400 UTF-8 bytes.

Reject an identifier above 1,024 UTF-8 bytes with HTTP 400. Do not classify it only as an ignored row. Otherwise, the service can persist memberships that the DynamoDB identity path cannot process.

Based on learnings: “validate cohort identifiers before persistence and reject identifiers exceeding 1024 bytes with HTTP 400, matching Edge DynamoDB constraints.”

Source: Learnings

Comment thread api/cohorts/services.py Outdated
Comment on lines +217 to +229
elif membership.state == CohortMembershipState.PENDING_ADD:
# Never applied, so no identity data to drain: drop the row.
discard_ids.append(membership.id)
# Rows already pending removal stay on their way out.

added += CohortMembership.objects.filter(id__in=readd_ids).update(
state=CohortMembershipState.PENDING_ADD, updated_at=timezone.now()
)
removed += CohortMembership.objects.filter(id__in=remove_ids).update(
state=CohortMembershipState.PENDING_REMOVE, updated_at=timezone.now()
)
discarded, _ = CohortMembership.objects.filter(id__in=discard_ids).delete()
removed += discarded

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 | 🏗️ Heavy lift

Coordinate sync deletion with membership application.

The sync deletes a PENDING_ADD row when its identifier is absent. apply_pending_memberships() can already have selected that row, write its DynamoDB trait, then find that its state update affects zero rows. The worker then sees no pending row, so the trait remains set without a membership record.

Use one shared claim or locking protocol for the sync path and applier. The sync must not discard a pending add while an applier can write its external side effect.

Comment thread api/cohorts/services.py
Comment on lines +231 to +233
locked_cohort.version += 1
locked_cohort.save(update_fields=["version"])
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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Queue the applier after the transaction commits.

apply_cohort_membership_deltas.delay() runs before the membership rows and version commit. A separate worker can read no pending rows, return without rescheduling, and leave the committed rows pending indefinitely.

Register the task with transaction.on_commit() so the worker can only run after this synchronisation is durable.

Based on learnings: “do not rely on ATOMIC_REQUESTS being enabled” and “treat transaction.atomic() in service code as defining the transaction boundary itself.”

Source: Learnings

Comment thread openapi.yaml
Comment on lines +2152 to +2186
'/api/v1/environments/{environment_api_key}/cohorts/{cohort_id}/sync-csv/':
post:
operationId: api_v1_environments_cohorts_sync_csv_create
description: Replace the cohort's members with the identifiers found in the uploaded CSV file and trigger a sync to identity data. `identifier_column` is the 0-based index of the column holding the identifiers; `has_header` skips the first row when true.
parameters:
- name: cohort_id
in: path
description: A unique integer value identifying this cohort.
required: true
schema:
type: integer
- name: environment_api_key
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/CohortCsvSync'
responses:
'202':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/CohortCsvSyncResult'
security:
- tokenAuth: []
- Master API Key: []
tags:
- Environments
x-flagsmith-minimum-plan: START_UP

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 | 🔵 Trivial | ⚡ Quick win

Consider documenting error responses for the CSV size/validation limits.

The PR summary states that files larger than 10 MB are rejected, and rows are validated (empty, duplicate, too-long identifiers). The operation only documents a 202 response. Add a 400 (or 413) response schema so API consumers can distinguish a rejected upload from a successful sync without relying on undocumented behaviour.

Comment thread openapi.yaml
Comment on lines +28563 to +28583
_SegmentCohort:
type: object
properties:
id:
type: integer
readOnly: true
environment:
type: integer
source_type:
$ref: '#/components/schemas/SourceTypeEnum'
version:
type: integer
maximum: 2147483647
minimum: 0
deletion_requested_at:
type:
- string
- 'null'
format: date-time
required:
- environment

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 | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare `version` field declarations across cohort serializers.
set -euo pipefail

fd -e py . api/cohorts api/segments 2>/dev/null | xargs rg -n -B3 -A3 "version" 2>/dev/null

Repository: Flagsmith/flagsmith

Length of output: 21068


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- cohort serializers ---'
cat -n api/cohorts/serializers.py | sed -n '1,125p'

printf '%s\n' '--- segment serializer cohort fields ---'
cat -n api/segments/serializers.py | sed -n '90,125p'

printf '%s\n' '--- cohort schema declarations ---'
rg -n -A18 -B3 '^    (Cohort|CohortCsvSyncResult|_SegmentCohort):$' openapi.yaml

Repository: Flagsmith/flagsmith

Length of output: 7342


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- complete Cohort schema ---'
sed -n '18841,18903p' openapi.yaml

printf '%s\n' '--- cohort model and sync result construction ---'
cat -n api/cohorts/models.py | sed -n '1,45p'
cat -n api/cohorts/services.py | sed -n '210,255p'

Repository: Flagsmith/flagsmith

Length of output: 5694


Align the documented version constraints.

_SegmentCohort.version declares minimum: 0 and maximum: 2147483647, but Cohort.version and CohortCsvSyncResult.version do not. Expose the same constraints in each schema to prevent inconsistent client validation.

@themis-blindfold

Copy link
Copy Markdown
Contributor

⚖️ Themis review: ✅ Ship it

Clean, well-structured addition of the CSV sync endpoint and cohort summary on segments. The delta computation in sync_cohort_memberships_from_csv correctly handles all membership state transitions (new adds, re-adds of pending removals, discards of unapplied pending adds, removal of applied members), and the select_for_update lock on the cohort serialises concurrent syncs. Tests are thorough: parametrised extraction covering headers, multi-column, quoted values, BOM, duplicates, empty rows, and too-long identifiers; plus full delta-semantics tests for the sync service and permission/validation tests for the view.

Area Score
🎯 Correctness 5/5
🧪 Test coverage 5/5
📐 Code quality 4/5
🚀 Product impact 4/5
📝 Walkthrough
  • CSV sync endpoint (cohorts/views.py, cohorts/services.py) - New POST .../cohorts/{id}/sync-csv/ action with multipart upload; parses the CSV, computes a membership delta against existing rows, creates/updates/deletes CohortMembership rows inside a locked transaction, bumps the cohort version, and enqueues the applier task.
  • CSV extraction (cohorts/services.py) - extract_identifiers_from_csv handles BOM stripping, configurable identifier column, header skipping, deduplication, empty/too-long filtering, and wraps csv.Error into a ValidationError.
  • Serializer & validation (cohorts/serializers.py) - CohortCsvSyncSerializer validates the uploaded file against the 10 MB size cap; CohortSerializer now accepts optional metadata on create and validates required metadata fields against the managed segment's content type.
  • Cohort summary on segments (segments/serializers.py, segments/views.py) - SegmentSerializer gains a read-only cohort field via SerializerMethodField, backed by a prefetch on the list queryset. _SegmentCohortSerializer exposes id, environment, source_type, version, and deletion_requested_at.
  • Observability (cohorts/metrics.py, docs) - New flagsmith_cohorts_csv_syncs_total counter and flagsmith_cohorts_csv_sync_identifiers histogram; cohorts.csv.synced structured log event. Docs catalogues updated with new entries and corrected line numbers.
  • OpenAPI / MCP spec - openapi.yaml and mcp/openapi.json updated with the new endpoint, _SegmentCohort schema, SourceTypeEnum, and the cohort field on segment schemas.
🧪 How to verify
  1. Run the new unit tests: pytest api/tests/unit/cohorts/test_services.py api/tests/unit/cohorts/test_views.py api/tests/unit/segments/test_unit_segments_views.py -k "csv or cohort".
  2. POST a multipart CSV to /api/v1/environments/{key}/cohorts/{id}/sync-csv/ and confirm a 202 with correct added/removed/unchanged/ignored counts.
  3. Re-upload a different CSV to the same cohort and verify the delta: removed identifiers move to pending_remove, new ones appear as pending_add, and the version increments.
  4. GET /api/v1/projects/{id}/segments/ and confirm each cohort-managed segment includes a cohort summary with source_type, version, and deletion_requested_at; non-cohort segments return cohort: null.
  5. Verify permissions: a user with only VIEW_ENVIRONMENT (no MANAGE_SEGMENTS) receives 403 on sync-csv.

Product take: This closes the loop on CSV-based cohort management by giving the dashboard everything it needs to upload, re-sync, and display cohort membership. Solid capability addition that unlocks the full create-from-CSV workflow.

🧭 Assumptions & unverified claims
  • The task processor stores tasks in the database, so .delay() inside transaction.atomic() is transaction-safe (task becomes visible only after commit). This is consistent with usage across the codebase but could not be verified against the task processor source in this environment.
  • SoftDeleteExportableModel's default manager filters out records with deleted_at IS NOT NULL, so segment.cohorts.all() correctly excludes fully deleted cohorts while including those pending drain.

A CSV walks into a bar; the parser orders one identifier, neat — no duplicates, no empties, hold the BOM. · reviewed at aa61a35

@Zaimwa9
Zaimwa9 force-pushed the feat/create-segment-from-csv branch from 05b2fcf to 033c67d Compare August 14, 2026 14:48
@Zaimwa9
Zaimwa9 requested a review from a team as a code owner August 14, 2026 14:48
@Zaimwa9
Zaimwa9 requested review from talissoncosta and removed request for a team August 14, 2026 14:48
@Zaimwa9
Zaimwa9 force-pushed the feat/cohort-csv-sync branch from aa61a35 to 4083994 Compare August 14, 2026 14:49
@github-actions github-actions Bot removed the feature New feature or request label Aug 14, 2026
@github-actions github-actions Bot added feature New feature or request docs Documentation updates and removed docs Documentation updates labels Aug 14, 2026
@Zaimwa9
Zaimwa9 force-pushed the feat/cohort-csv-sync branch from 71a387d to 3a5f36a Compare August 14, 2026 15:55
@github-actions github-actions Bot added feature New feature or request and removed feature New feature or request docs Documentation updates labels Aug 14, 2026
@github-actions github-actions Bot added the docs Documentation updates label 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.
⚠️ Please upload report for BASE (feat/create-segment-from-csv@033c67d). Learn more about missing BASE report.

Additional details and impacted files
@@                       Coverage Diff                       @@
##             feat/create-segment-from-csv    #8294   +/-   ##
===============================================================
  Coverage                                ?   98.70%           
===============================================================
  Files                                   ?     1586           
  Lines                                   ?    63478           
  Branches                                ?        0           
===============================================================
  Hits                                    ?    62657           
  Misses                                  ?      821           
  Partials                                ?        0           

☔ 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.

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