Skip to content

Commit 7cb3758

Browse files
authored
fix: deduplicate metrics live features states (#5571)
1 parent 449ec6f commit 7cb3758

File tree

6 files changed

+104
-23
lines changed

6 files changed

+104
-23
lines changed

api/environments/dynamodb/wrappers/identity_wrapper.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,31 @@ def get_segment_ids(
196196

197197
def get_identity_overrides_count(self, environment_api_key: str) -> int:
198198
return sum(
199-
len(identity["identity_features"])
199+
len({f["feature_id"] for f in identity["identity_features"]})
200200
for identity in self.iter_all_items_paginated(
201201
environment_api_key=environment_api_key,
202202
overrides_only=True,
203203
)
204204
)
205+
206+
def get_identity_override_feature_counts(
207+
self, environment_api_key: str
208+
) -> dict[int, int]:
209+
feature_to_identity_count: dict[int, int] = {}
210+
211+
for identity in self.iter_all_items_paginated(
212+
environment_api_key=environment_api_key,
213+
overrides_only=True,
214+
):
215+
unique_feature_ids: set[int] = set()
216+
217+
for feature_override in identity.get("identity_features", []):
218+
feature_id = feature_override.get("feature", {}).get("id", 0)
219+
unique_feature_ids.add(feature_id)
220+
221+
for feature_id in unique_feature_ids:
222+
feature_to_identity_count[feature_id] = (
223+
feature_to_identity_count.get(feature_id, 0) + 1
224+
)
225+
226+
return feature_to_identity_count

api/environments/models.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -383,9 +383,11 @@ def _get_active_feature_states_ids(
383383
base_qs = FeatureState.objects.get_live_feature_states(
384384
environment=self,
385385
**(filter_kwargs or {}),
386+
).filter(
387+
feature__is_archived=False,
386388
)
387389

388-
group_fields = ["feature_id"]
390+
group_fields = ["feature_id", "environment_id"]
389391
if extra_group_by_fields is not None:
390392
group_fields.append(extra_group_by_fields)
391393

@@ -410,9 +412,15 @@ def _get_latest_segment_state_ids_subquery(self) -> list[int]:
410412
identity_id__isnull=True,
411413
feature_segment_id__isnull=False,
412414
),
413-
).values_list("id", flat=True)
415+
).filter(feature__is_archived=False)
414416

415-
return list(feature_states_qs)
417+
return list(
418+
feature_states_qs.values(
419+
"feature_id", "feature_segment_id", "environment_id"
420+
)
421+
.annotate(id=Max("id"))
422+
.values_list("id", flat=True)
423+
)
416424

417425
def get_segment_metrics_queryset(self) -> QuerySet[FeatureState]:
418426
ids = self._get_latest_segment_state_ids_subquery()

api/features/managers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def get_live_feature_states( # type: ignore[no-untyped-def]
4141
environment.id
4242
)
4343
)
44+
4445
latest_version_uuids = [efv.uuid for efv in latest_versions]
4546

4647
# Note that since identity overrides aren't part of the versioning system,

api/metrics/metrics_service.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from edge_api.identities.models import EdgeIdentity
99
from environments.models import Environment
10+
from features.models import Feature
1011
from metrics.constants import DEFAULT_METRIC_DEFINITIONS, WORKFLOW_METRIC_DEFINITIONS
1112
from metrics.types import EnvMetricsName, EnvMetricsPayload, MetricDefinition
1213

@@ -75,14 +76,33 @@ def _get_identity_metrics(self) -> dict[EnvMetricsName, Callable[[], int]]:
7576
"""
7677
return {
7778
EnvMetricsName.IDENTITY_OVERRIDES: (
78-
lambda: EdgeIdentity.dynamo_wrapper.get_identity_overrides_count(
79-
self.environment.api_key
80-
)
79+
lambda: self._get_active_identity_edge_overrides_count()
8180
)
8281
if self.uses_dynamo
8382
else (lambda: self.environment.get_identity_overrides_queryset().count()),
8483
}
8584

85+
def _get_active_identity_edge_overrides_count(self) -> int:
86+
override_feature_id_counts = (
87+
EdgeIdentity.dynamo_wrapper.get_identity_override_feature_counts(
88+
self.environment.api_key
89+
)
90+
)
91+
92+
valid_feature_ids = set(
93+
Feature.objects.filter(
94+
project=self.environment.project,
95+
is_archived=False,
96+
deleted_at__isnull=True,
97+
).values_list("id", flat=True)
98+
)
99+
100+
return sum(
101+
count
102+
for feature_id, count in override_feature_id_counts.items()
103+
if feature_id in valid_feature_ids
104+
)
105+
86106
def _get_feature_metrics(
87107
self,
88108
) -> dict[EnvMetricsName, Callable[[], int]]:

api/tests/unit/environments/dynamodb/wrappers/test_unit_dynamodb_identity_wrapper.py

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -604,49 +604,71 @@ def test_delete_all_identities__deletes_all_identities_documents_from_dynamodb(
604604

605605

606606
@pytest.mark.parametrize(
607-
"identity_features",
607+
"identity_features, expected_counts",
608608
[
609-
[[1, 2, 3], [99], []],
610-
[[1, 2, 3], [99], [1, 2, 3, 99]],
611-
[[], [], []],
612-
[[], [1, 2, 3], []],
613-
[[4], [4, 25, 18, 19, 85, 100], [4]],
609+
(
610+
[[1, 2, 3], [99], []],
611+
{1: 1, 2: 1, 3: 1, 99: 1},
612+
),
613+
(
614+
[[1, 2, 3], [99], [1, 2, 3, 99]],
615+
{1: 2, 2: 2, 3: 2, 99: 2},
616+
),
617+
(
618+
[[], [], []],
619+
{},
620+
),
621+
(
622+
[[], [1, 2, 3], []],
623+
{1: 1, 2: 1, 3: 1},
624+
),
625+
(
626+
[[4], [4, 25, 18, 19, 85, 100], [4]],
627+
{4: 3, 25: 1, 18: 1, 19: 1, 85: 1, 100: 1},
628+
),
614629
],
615630
)
616-
def test_get_identity_overrides_count_dynamo_returns_correct_total(
631+
def test_get_identity_override_feature_counts_dynamo_returns_correct_total(
617632
flagsmith_identities_table: Table,
618633
dynamodb_identity_wrapper: DynamoIdentityWrapper,
619634
identity_features: list[list[int]],
635+
expected_counts: dict[int, int],
620636
) -> None:
621637
environment_api_key = "env_test"
622638

623639
identity_one = {
624640
"composite_key": f"{environment_api_key}_identity1",
625641
"environment_api_key": environment_api_key,
626642
"identifier": "user1",
627-
"identity_features": identity_features[0],
643+
"identity_features": [
644+
{"feature": {"id": feature_id}} for feature_id in identity_features[0]
645+
],
628646
}
629647

630648
identity_two = {
631649
"composite_key": f"{environment_api_key}_identity2",
632650
"environment_api_key": environment_api_key,
633651
"identifier": "user2",
634-
"identity_features": identity_features[1],
652+
"identity_features": [
653+
{"feature": {"id": feature_id}} for feature_id in identity_features[1]
654+
],
635655
}
636656

637657
identity_three = {
638658
"composite_key": f"{environment_api_key}_identity3",
639659
"environment_api_key": environment_api_key,
640660
"identifier": "user3",
641-
"identity_features": identity_features[2],
661+
"identity_features": [
662+
{"feature": {"id": feature_id}} for feature_id in identity_features[2]
663+
],
642664
}
643665

644666
flagsmith_identities_table.put_item(Item=identity_one)
645667
flagsmith_identities_table.put_item(Item=identity_two)
646668
flagsmith_identities_table.put_item(Item=identity_three)
647669

648-
result = dynamodb_identity_wrapper.get_identity_overrides_count(environment_api_key)
649-
650-
assert result == len(identity_features[0]) + len(identity_features[1]) + len(
651-
identity_features[2]
670+
result = dynamodb_identity_wrapper.get_identity_override_feature_counts(
671+
environment_api_key
652672
)
673+
674+
assert result == expected_counts

api/tests/unit/metrics/test_unit_metrics_service.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55
from environments.models import Environment
6+
from features.models import Feature
67
from metrics.metrics_service import EnvironmentMetricsService
78
from metrics.types import EnvMetricsName
89

@@ -82,16 +83,23 @@ def test_dynamo_identity_metric_used(
8283
monkeypatch.setattr(
8384
environment, "get_segment_metrics_queryset", lambda: MagicMock(count=lambda: 1)
8485
)
86+
Feature.objects.create(
87+
id=10,
88+
project=environment.project,
89+
name="feature-10",
90+
is_archived=False,
91+
deleted_at=None,
92+
)
8593
identity_count_mock = MagicMock(return_value=1)
8694
monkeypatch.setattr(
8795
environment,
8896
"get_identity_overrides_queryset",
8997
lambda: MagicMock(count=identity_count_mock),
9098
)
9199

92-
dynamo_mock = MagicMock(return_value=99)
100+
dynamo_mock = MagicMock(return_value={10: 99})
93101
monkeypatch.setattr(
94-
"edge_api.identities.models.EdgeIdentity.dynamo_wrapper.get_identity_overrides_count",
102+
"edge_api.identities.models.EdgeIdentity.dynamo_wrapper.get_identity_override_feature_counts",
95103
dynamo_mock,
96104
)
97105
# When

0 commit comments

Comments
 (0)