Skip to content

Commit b91eb3f

Browse files
authored
FFM-5955 - Metrics fixes (#48)
* FFM-5995 Start storing unique targets separately in the analyitcs class * FFM-5995 Start storing unique targets separately in the analyitcs class * FFM-5995 Start refactoring to build unique target data * FFM-5995 Start refactoring to build unique target data * FFM-5995 Use new variation value * FFM-5995 tidy up * FFM-5995 tidy up * FFM-5995 lint * FFM-5995 lint * FFM-5995 lint * FFM-5995 Stop early client init variations getting posted to metrics * FFM-5995 Use != to compare constant literals per linter * FFM-5995 Linter
1 parent 9572f86 commit b91eb3f

File tree

4 files changed

+78
-29
lines changed

4 files changed

+78
-29
lines changed

featureflags/analytics.py

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import time
22
from threading import Lock, Thread
3-
from typing import Dict, List
3+
from typing import Dict, List, Union
44

55
import attr
66

@@ -11,6 +11,7 @@
1111
from .api.default.post_metrics import sync_detailed as post_metrics
1212
from .config import Config
1313
from .evaluations.target import Target
14+
from .evaluations.target_attributes import TargetAttributes
1415
from .evaluations.variation import Variation
1516
from .models.key_value import KeyValue
1617
from .models.metrics import Metrics
@@ -23,7 +24,7 @@
2324
FEATURE_IDENTIFIER_ATTRIBUTE = 'featureIdentifier'
2425
FEATURE_NAME_ATTRIBUTE = 'featureName'
2526
VARIATION_IDENTIFIER_ATTRIBUTE = 'variationIdentifier'
26-
VARIATION_VALUE_ATTRIBUTE = 'featureValue'
27+
VARIATION_VALUE_ATTRIBUTE = 'variationValue'
2728
TARGET_ATTRIBUTE = 'target'
2829
SDK_VERSION_ATTRIBUTE = 'SDK_VERSION'
2930
SDK_VERSION = '1.0.0'
@@ -42,6 +43,13 @@ class AnalyticsEvent(object):
4243
count: int = 0
4344

4445

46+
@attr.s(auto_attribs=True)
47+
class MetricTargetData(object):
48+
identifier: str
49+
name: str
50+
attributes: Union[Unset, TargetAttributes] = Unset
51+
52+
4553
class AnalyticsService(object):
4654

4755
def __init__(self, config: Config, client: AuthenticatedClient,
@@ -51,6 +59,7 @@ def __init__(self, config: Config, client: AuthenticatedClient,
5159
self._client = client
5260
self._environment = environment
5361
self._data: Dict[str, AnalyticsEvent] = {}
62+
self._target_data: Dict[str, MetricTargetData] = {}
5463

5564
self._running = False
5665
self._runner = Thread(target=self._sync)
@@ -67,15 +76,33 @@ def enqueue(self, target: Target, identifier: str,
6776

6877
self._lock.acquire()
6978
try:
70-
key = self.get_key(event)
71-
if key in self._data:
72-
self._data[key].count += 1
79+
# Store unique evaluation events. We map a unique evaluation
80+
# event to its count.
81+
unique_evaluation_key = self.get_key(event)
82+
if unique_evaluation_key in self._data:
83+
self._data[unique_evaluation_key].count += 1
7384
else:
7485
event.count = 1
75-
self._data[key] = event
86+
self._data[unique_evaluation_key] = event
87+
88+
# Store unique targets. If the target already exists
89+
# just ignore it.
90+
if event.target is not None and not event.target.anonymous:
91+
unique_target_key = self.get_target_key(event)
92+
if unique_target_key not in self._target_data:
93+
target_name = event.target.name
94+
# If the target has no name use the identifier
95+
if not target_name:
96+
target_name = event.target.identifier
97+
self._target_data[unique_target_key] = MetricTargetData(
98+
identifier=event.target.identifier,
99+
name=target_name,
100+
attributes=event.target.attributes
101+
)
76102
finally:
77103
self._lock.release()
78104

105+
# Returns a key for unique evaluations events.
79106
def get_key(self, event: AnalyticsEvent) -> str:
80107
return '{feature}-{variation}-{value}-{target}'.format(
81108
feature=event.flag_identifier,
@@ -84,6 +111,11 @@ def get_key(self, event: AnalyticsEvent) -> str:
84111
target=GLOBAL_TARGET,
85112
)
86113

114+
# Returns a key for unique targets. Targets are considered unique
115+
# if they have different identifiers.
116+
def get_target_key(self, event: AnalyticsEvent) -> str:
117+
return event.target.identifier
118+
87119
def _sync(self) -> None:
88120
if not self._running:
89121
log.info("Starting AnalyticsService with request interval: %d",
@@ -103,22 +135,6 @@ def _send_data(self) -> None:
103135
metrics_data: List[MetricsData] = []
104136
try:
105137
for _, event in self._data.items():
106-
if event.target is not None and not event.target.anonymous:
107-
target_attributes: List[KeyValue] = []
108-
if not isinstance(event.target.attributes, Unset):
109-
for key, value in event.target.attributes.items():
110-
target_attributes.append(KeyValue(key, value))
111-
target_name = event.target.identifier
112-
if event.target.name:
113-
target_name = event.target.name
114-
115-
td = TargetData(
116-
identifier=event.target.identifier,
117-
name=target_name,
118-
attributes=target_attributes
119-
)
120-
target_data.append(td)
121-
122138
metric_attributes: List[KeyValue] = [
123139
KeyValue(FEATURE_IDENTIFIER_ATTRIBUTE,
124140
event.flag_identifier),
@@ -140,8 +156,20 @@ def _send_data(self) -> None:
140156
attributes=metric_attributes
141157
)
142158
metrics_data.append(md)
159+
for _, unique_target in self._target_data.items():
160+
target_attributes: List[KeyValue] = []
161+
if not isinstance(unique_target.attributes, Unset):
162+
for key, value in unique_target.attributes.items():
163+
target_attributes.append(KeyValue(key, value))
164+
td = TargetData(
165+
identifier=unique_target.identifier,
166+
name=unique_target.name,
167+
attributes=target_attributes
168+
)
169+
target_data.append(td)
143170
finally:
144171
self._data = {}
172+
self._target_data = {}
145173
self._lock.release()
146174
body: Metrics = Metrics(target_data=target_data,
147175
metrics_data=metrics_data)

featureflags/client.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,37 +118,57 @@ def authenticate(self):
118118
def bool_variation(self, identifier: str, target: Target,
119119
default: bool) -> bool:
120120
variation = self._evaluator.evaluate(identifier, target)
121-
if self._config.enable_analytics:
121+
# Only register metrics if analytics is enabled,
122+
# and sometimes when the SDK starts up we can
123+
# evaluate before the flag is cached which results in
124+
# an empty identifier.
125+
if self._config.enable_analytics and variation.identifier != "":
122126
self._analytics.enqueue(target, identifier, variation)
123127
return variation.bool(default)
124128

125129
def int_variation(self, identifier: str, target: Target,
126130
default: int) -> int:
127131
variation = self._evaluator.evaluate(identifier, target)
128-
if self._config.enable_analytics:
132+
# Only register metrics if analytics is enabled,
133+
# and sometimes when the SDK starts up we can
134+
# evaluate before the flag is cached which results in
135+
# an empty identifier.
136+
if self._config.enable_analytics and variation.identifier != "":
129137
self._analytics.enqueue(target, identifier, variation)
130138
return variation.int(default)
131139

132140
def number_variation(self, identifier: str, target: Target,
133141
default: float) -> float:
134142
variation = self._evaluator.evaluate(
135143
identifier, target)
136-
if self._config.enable_analytics:
144+
# Only register metrics if analytics is enabled,
145+
# and sometimes when the SDK starts up we can
146+
# evaluate before the flag is cached which results in
147+
# an empty identifier.
148+
if self._config.enable_analytics and variation.identifier != "":
137149
self._analytics.enqueue(target, identifier, variation)
138150
return variation.number(default)
139151

140152
def string_variation(self, identifier: str, target: Target,
141153
default: str) -> str:
142154
variation = self._evaluator.evaluate(
143155
identifier, target)
144-
if self._config.enable_analytics:
156+
# Only register metrics if analytics is enabled,
157+
# and sometimes when the SDK starts up we can
158+
# evaluate before the flag is cached which results in
159+
# an empty identifier.
160+
if self._config.enable_analytics and variation.identifier != "":
145161
self._analytics.enqueue(target, identifier, variation)
146162
return variation.string(default)
147163

148164
def json_variation(self, identifier: str, target: Target,
149165
default: Dict[str, Any]) -> Dict[str, Any]:
150166
variation = self._evaluator.evaluate(identifier, target)
151-
if self._config.enable_analytics:
167+
# Only register metrics if analytics is enabled,
168+
# and sometimes when the SDK starts up we can
169+
# evaluate before the flag is cached which results in
170+
# an empty identifier.
171+
if self._config.enable_analytics and variation.identifier != "":
152172
self._analytics.enqueue(target, identifier, variation)
153173
return variation.json(default)
154174

featureflags/evaluations/evaluator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def _check_target_in_segment(self, segments: List[str],
113113

114114
# Should Target be included via segment rules
115115
if segment.rules and self._evaluate_clauses(segment.rules,
116-
target):
116+
target):
117117
log.debug('Target %s included in segment %s via rules\n',
118118
target.name, segment.name)
119119
return True

tests/integration/test_evaluator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ def from_dict(cls, source: Dict[str, Any]) -> 'Feature':
4848
for segment in source["segments"]]
4949
targets = []
5050
if "targets" in source:
51-
targets = [Target.from_dict(target) for target in source["targets"]]
51+
targets = [Target.from_dict(target) for
52+
target in source["targets"]]
5253

5354
tests = [Test.from_dict(test) for test in source["tests"]]
5455

0 commit comments

Comments
 (0)