Skip to content

Commit 1eede39

Browse files
authored
fix: Incorrect mapper behaviour for SDK metrics (#5816)
1 parent 9d2c62a commit 1eede39

4 files changed

Lines changed: 119 additions & 14 deletions

File tree

api/app_analytics/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"Flagsmith-Application-Version": "client_application_version",
2121
"User-Agent": "user_agent",
2222
}
23-
LABELS: tuple[Label, ...] = get_args(Label)
23+
LABELS: tuple[str, ...] = tuple(str(label) for label in get_args(Label))
2424

2525
NO_ANALYTICS_DATABASE_CONFIGURED_WARNING = (
2626
"No analytics database configured. "

api/app_analytics/mappers.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from collections import defaultdict
2+
from functools import partial
23
from typing import Any, Iterable
34

45
from django.http import HttpRequest
56
from influxdb_client.client.flux_table import FluxTable
67
from pydantic import Field, create_model
78
from pydantic.type_adapter import TypeAdapter
89

9-
from app_analytics.constants import TRACK_HEADERS
10+
from app_analytics.constants import LABELS, TRACK_HEADERS
1011
from app_analytics.dataclasses import FeatureEvaluationData, UsageData
1112
from app_analytics.models import FeatureEvaluationRaw, Resource
1213
from app_analytics.types import (
@@ -29,6 +30,11 @@
2930
)
3031
_labels_type_adapter: TypeAdapter[Labels] = TypeAdapter(Labels)
3132

33+
map_influx_record_values_to_labels = partial(
34+
_labels_type_adapter.dump_python,
35+
include=set(LABELS),
36+
)
37+
3238

3339
def map_annotated_api_usage_buckets_to_usage_data(
3440
api_usage_buckets: Iterable[AnnotatedAPIUsageBucket],
@@ -63,25 +69,43 @@ def map_annotated_api_usage_buckets_to_usage_data(
6369
def map_flux_tables_to_usage_data(
6470
flux_tables: list[FluxTable],
6571
) -> list[UsageData]:
66-
return [
67-
UsageData(
68-
day=(values := record.values)["_time"].strftime("%Y-%m-%d"),
69-
labels=_labels_type_adapter.validate_python(values),
70-
**{values["resource"]: values["_value"]},
71-
)
72-
for flux_table in flux_tables
73-
for record in flux_table.records
74-
]
72+
"""
73+
Aggregates API usage data buckets by date and labels.
74+
Each resulting `UsageData` object contains the total count for each resource
75+
for that date and labels combination.
76+
"""
77+
data_by_key: dict[AnnotatedAPIUsageKey, UsageData] = {}
78+
for flux_table in flux_tables:
79+
for record in flux_table.records:
80+
values = record.values
81+
date = values["_time"].date()
82+
labels: Labels = map_influx_record_values_to_labels(values)
83+
key = AnnotatedAPIUsageKey(
84+
date=date,
85+
labels=tuple(labels.items()),
86+
)
87+
if key not in data_by_key:
88+
data_by_key[key] = UsageData(
89+
day=date,
90+
labels=labels,
91+
)
92+
if resource := values.get("resource"):
93+
setattr(
94+
data_by_key[key],
95+
resource,
96+
values["_value"],
97+
)
98+
return list(data_by_key.values())
7599

76100

77101
def map_flux_tables_to_feature_evaluation_data(
78102
flux_tables: list[FluxTable],
79103
) -> list[FeatureEvaluationData]:
80104
return [
81105
FeatureEvaluationData(
82-
day=(values := record.values)["_time"].strftime("%Y-%m-%d"),
106+
day=(values := record.values)["_time"].date(),
83107
count=values["_value"],
84-
labels=_labels_type_adapter.validate_python(values),
108+
labels=map_influx_record_values_to_labels(values),
85109
)
86110
for flux_table in flux_tables
87111
for record in flux_table.records

api/app_analytics/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,6 @@ def save(self, **kwargs: Any) -> None:
119119

120120
def _get_label_fields() -> dict[str, serializers.Field[Any, Any, Any, Any]]:
121121
return {
122-
str(label): serializers.CharField(allow_null=True, required=False)
122+
label: serializers.CharField(allow_null=True, required=False)
123123
for label in LABELS
124124
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from datetime import date, datetime
2+
3+
from influxdb_client.client.flux_table import FluxRecord, FluxTable
4+
5+
from app_analytics.dataclasses import FeatureEvaluationData, UsageData
6+
from app_analytics.mappers import (
7+
map_flux_tables_to_feature_evaluation_data,
8+
map_flux_tables_to_usage_data,
9+
)
10+
11+
12+
def test_map_flux_tables_to_feature_evaluation_data__returns_expected() -> None:
13+
# Given
14+
flux_table = FluxTable()
15+
flux_table.records.append(
16+
FluxRecord(
17+
flux_table,
18+
values={
19+
"_time": datetime.fromisoformat("2023-10-01T00:00:00Z"),
20+
"_value": 5,
21+
"feature_name": "feature_1",
22+
"client_application_name": "test-app",
23+
"unrelated": "value",
24+
},
25+
)
26+
)
27+
28+
# When
29+
result = map_flux_tables_to_feature_evaluation_data(flux_tables=[flux_table])
30+
31+
# Then
32+
assert result == [
33+
FeatureEvaluationData(
34+
day=date(2023, 10, 1),
35+
count=5,
36+
labels={"client_application_name": "test-app"},
37+
)
38+
]
39+
40+
41+
def test_map_flux_tables_to_usage_data__returns_expected() -> None:
42+
# Given
43+
flux_table = FluxTable()
44+
flux_table.records.append(
45+
FluxRecord(
46+
flux_table,
47+
values={
48+
"_time": datetime.fromisoformat("2023-10-01T00:00:00Z"),
49+
"_value": 10,
50+
"resource": "flags",
51+
"client_application_name": "test-app",
52+
"unrelated": "value",
53+
},
54+
),
55+
)
56+
flux_table.records.append(
57+
FluxRecord(
58+
flux_table,
59+
values={
60+
"_time": datetime.fromisoformat("2023-10-01T00:00:00Z"),
61+
"_value": 10,
62+
"resource": "identities",
63+
"client_application_name": "test-app",
64+
"unrelated": "value",
65+
},
66+
),
67+
)
68+
69+
# When
70+
result = map_flux_tables_to_usage_data(flux_tables=[flux_table])
71+
72+
# Then
73+
assert result == [
74+
UsageData(
75+
day=date(2023, 10, 1),
76+
flags=10,
77+
traits=0,
78+
identities=10,
79+
labels={"client_application_name": "test-app"},
80+
)
81+
]

0 commit comments

Comments
 (0)