Skip to content
15 changes: 14 additions & 1 deletion api/segment_membership/tasks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import timedelta
from typing import cast
from typing import Any, cast

import structlog
from django.conf import settings
Expand Down Expand Up @@ -61,6 +61,9 @@ def seed_organisation_identities(organisation_id: int) -> None:

Rows are versioned at scan start via `inserted_at`
so writes arriving mid-scan win ReplacingMergeTree dedup over the seeded row.

Identities carrying no traits are skipped as they carry little to no value
for segment membership.
"""
log = logger.bind(organisation__id=organisation_id)
if not settings.CLICKHOUSE_ENABLED:
Expand Down Expand Up @@ -104,7 +107,10 @@ def seed_organisation_identities(organisation_id: int) -> None:
scan_started_at,
)
for doc in batch
if not _is_empty_identity(doc)
]
if not rows:
continue
# Django's CursorWrapper stub forbids dicts in
# the params sequence; clickhouse-driver accepts
# them as JSON-column payloads.
Expand Down Expand Up @@ -269,3 +275,10 @@ def refresh_project_segment_counts(project_id: int) -> None:
membership_counts__count=len(membership_counts),
stale_counts__count=stale_deleted,
)


def _is_empty_identity(identity_document: dict[str, Any]) -> bool:
return not (
identity_document.get("identity_traits")
or identity_document.get("system_traits")
)
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def test_seed_organisation_identities__happy_path__rows_land_in_clickhouse(
"composite_key": "k2",
"environment_api_key": environment_api_key,
"created_date": "2026-05-08T00:00:00Z",
"identity_traits": [],
"identity_traits": [{"trait_key": "foo2", "trait_value": "bar2"}],
},
]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,54 @@ def test_seed_organisation_identities__insert_fails__logs_and_continues(
]


def test_seed_organisation_identities__traitless_identities__are_not_mirrored(
mocker: MockerFixture,
settings: SettingsWrapper,
project: Project,
environment: Environment,
segment: Segment,
flagsmith_identities_table: Table,
dynamo_identities: None,
enable_features: EnableFeaturesFixture,
) -> None:
# Given
enable_features("segment_membership_inspection")
settings.CLICKHOUSE_ENABLED = True
mocker.patch.object(tasks, "_INSERT_BATCH_SIZE", 1)

cursor = MagicMock()
open_cursor = mocker.patch.object(tasks, "open_clickhouse_cursor")
open_cursor.return_value.__enter__.return_value = cursor
mocker.patch.object(tasks, "enqueue_membership_refresh")

for identifier, extra in (
("dave", {}),
("erin", {"system_traits": {"flagsmith_cohort_e2b1": True}}),
):
Comment thread
coderabbitai[bot] marked this conversation as resolved.
flagsmith_identities_table.put_item(
Item={
"composite_key": f"{environment.api_key}_{identifier}",
"environment_api_key": environment.api_key,
"identifier": identifier,
"identity_uuid": f"f47ac10b-58cc-4372-a567-0e02b2c3d4{identifier[:2]}",
"identity_traits": [],
**extra,
}
)

# When
seed_organisation_identities(project.organisation_id)

# Then
insert_identity_payloads = [
call.args[1][0] for call in cursor.executemany.call_args_list
]
inserted_identifiers = sorted(payload[1] for payload in insert_identity_payloads)

# 'dave' is not included in the list of inserted identities
assert inserted_identifiers == ["alice", "carol", "erin"]


@pytest.mark.clickhouse
def test_seed_organisation_identities__matching_identities__inserts_rows_versioned_at_scan_start(
mocker: MockerFixture,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ Attributes:
### `segment_membership.refresh.project.completed`

Logged at `info` from:
- `api/segment_membership/tasks.py:266`
- `api/segment_membership/tasks.py:272`

Attributes:
- `membership_counts.count`
Expand All @@ -628,16 +628,16 @@ Attributes:
### `segment_membership.refresh.project.failed`

Logged at `exception` from:
- `api/segment_membership/tasks.py:239`
- `api/segment_membership/tasks.py:245`

Attributes:
- `project.id`

### `segment_membership.refresh.project.skipped`

Logged at `info` from:
- `api/segment_membership/tasks.py:206`
- `api/segment_membership/tasks.py:218`
- `api/segment_membership/tasks.py:212`
- `api/segment_membership/tasks.py:224`

Attributes:
- `project.id`
Expand All @@ -647,7 +647,7 @@ Attributes:
### `segment_membership.seed.environment.completed`

Logged at `info` from:
- `api/segment_membership/tasks.py:121`
- `api/segment_membership/tasks.py:127`

Attributes:
- `environment.id`
Expand All @@ -658,7 +658,7 @@ Attributes:
### `segment_membership.seed.environment.failed`

Logged at `exception` from:
- `api/segment_membership/tasks.py:114`
- `api/segment_membership/tasks.py:120`

Attributes:
- `environment.id`
Expand All @@ -668,9 +668,9 @@ Attributes:
### `segment_membership.seed.skipped`

Logged at `warning` from:
- `api/segment_membership/tasks.py:67`
- `api/segment_membership/tasks.py:72`
- `api/segment_membership/tasks.py:77`
- `api/segment_membership/tasks.py:70`
- `api/segment_membership/tasks.py:75`
- `api/segment_membership/tasks.py:80`

Attributes:
- `organisation.id`
Expand Down
Loading