flagsmith 6.1.0__tar.gz → 6.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flagsmith-6.1.0 → flagsmith-6.2.0}/PKG-INFO +1 -1
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/analytics.py +26 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/flagsmith.py +33 -6
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/models.py +3 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/pyproject.toml +1 -1
- {flagsmith-6.1.0 → flagsmith-6.2.0}/LICENSE +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/README.md +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/__init__.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/api/__init__.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/api/types.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/exceptions.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/mappers.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/offline_handlers.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/polling_manager.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/py.typed +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/streaming_manager.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/types.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/utils/__init__.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/utils/datetime.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/utils/identities.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/version.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.0}/flagsmith/webhooks.py +0 -0
|
@@ -83,6 +83,12 @@ class AnalyticsProcessor:
|
|
|
83
83
|
self.flush()
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
class ExposureKey(typing.NamedTuple):
|
|
87
|
+
feature_name: typing.Optional[str]
|
|
88
|
+
identifier: typing.Optional[str]
|
|
89
|
+
value: typing.Optional[str]
|
|
90
|
+
|
|
91
|
+
|
|
86
92
|
@dataclass
|
|
87
93
|
class EventProcessorConfig:
|
|
88
94
|
events_api_url: str = DEFAULT_EVENT_API_URL
|
|
@@ -111,6 +117,7 @@ class EventProcessor:
|
|
|
111
117
|
self._flush_interval_seconds = config.flush_interval_seconds
|
|
112
118
|
|
|
113
119
|
self._buffer: typing.List[typing.Dict[str, typing.Any]] = []
|
|
120
|
+
self._buffered_exposure_keys: typing.Set[ExposureKey] = set()
|
|
114
121
|
self._lock = threading.Lock()
|
|
115
122
|
self._timer: typing.Optional[threading.Timer] = None
|
|
116
123
|
|
|
@@ -159,6 +166,24 @@ class EventProcessor:
|
|
|
159
166
|
) -> None:
|
|
160
167
|
should_flush = False
|
|
161
168
|
with self._lock:
|
|
169
|
+
if event == FLAG_EXPOSURE_EVENT:
|
|
170
|
+
# An exposure is defined by who saw which variant of which
|
|
171
|
+
# feature; equal exposures within one flush interval add no
|
|
172
|
+
# information, so only the first is buffered.
|
|
173
|
+
exposure_key = ExposureKey(
|
|
174
|
+
feature_name=feature_name,
|
|
175
|
+
identifier=identifier,
|
|
176
|
+
value=str(value) if value is not None else None,
|
|
177
|
+
)
|
|
178
|
+
if exposure_key in self._buffered_exposure_keys:
|
|
179
|
+
logger.debug(
|
|
180
|
+
"Skipping duplicate %s event for feature %s, identifier %s",
|
|
181
|
+
FLAG_EXPOSURE_EVENT,
|
|
182
|
+
feature_name,
|
|
183
|
+
identifier,
|
|
184
|
+
)
|
|
185
|
+
return
|
|
186
|
+
self._buffered_exposure_keys.add(exposure_key)
|
|
162
187
|
self._buffer.append(
|
|
163
188
|
{
|
|
164
189
|
"event": event,
|
|
@@ -182,6 +207,7 @@ class EventProcessor:
|
|
|
182
207
|
return
|
|
183
208
|
events = self._buffer
|
|
184
209
|
self._buffer = []
|
|
210
|
+
self._buffered_exposure_keys.clear()
|
|
185
211
|
|
|
186
212
|
payload = json.dumps({"events": events})
|
|
187
213
|
try:
|
|
@@ -368,19 +368,38 @@ class Flagsmith:
|
|
|
368
368
|
"""
|
|
369
369
|
Resolve a flag for an identity and record an exposure event.
|
|
370
370
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
371
|
+
The exposure event's ``value`` is the flag's variant key. It is only
|
|
372
|
+
sent when the flag exists, is enabled and carries a variant; any
|
|
373
|
+
other outcome is logged and skipped to keep experimentation data
|
|
374
|
+
clean. A `DefaultFlag` served via the `default_flag_handler` counts
|
|
375
|
+
as the feature not existing.
|
|
375
376
|
"""
|
|
376
377
|
if not self._event_processor:
|
|
377
378
|
raise ValueError("Events must be enabled to use experiment flags.")
|
|
378
379
|
flag = self.get_identity_flags(identifier, traits).get_flag(feature_name)
|
|
379
|
-
if isinstance(flag, Flag)
|
|
380
|
+
if not isinstance(flag, Flag):
|
|
381
|
+
logger.debug(
|
|
382
|
+
"Not sending %s for feature %s: feature not found.",
|
|
383
|
+
FLAG_EXPOSURE_EVENT,
|
|
384
|
+
feature_name,
|
|
385
|
+
)
|
|
386
|
+
elif not flag.enabled:
|
|
387
|
+
logger.debug(
|
|
388
|
+
"Not sending %s for feature %s: feature is disabled.",
|
|
389
|
+
FLAG_EXPOSURE_EVENT,
|
|
390
|
+
feature_name,
|
|
391
|
+
)
|
|
392
|
+
elif flag.variant is None:
|
|
393
|
+
logger.debug(
|
|
394
|
+
"Not sending %s for feature %s: flag has no variant.",
|
|
395
|
+
FLAG_EXPOSURE_EVENT,
|
|
396
|
+
feature_name,
|
|
397
|
+
)
|
|
398
|
+
else:
|
|
380
399
|
self.track_exposure_event(
|
|
381
400
|
feature_name=feature_name,
|
|
382
401
|
identifier=identifier,
|
|
383
|
-
value=flag.variant
|
|
402
|
+
value=flag.variant,
|
|
384
403
|
traits=traits,
|
|
385
404
|
)
|
|
386
405
|
return flag
|
|
@@ -418,6 +437,14 @@ class Flagsmith:
|
|
|
418
437
|
) -> None:
|
|
419
438
|
if not self._event_processor:
|
|
420
439
|
raise ValueError("Events must be enabled to track exposure events.")
|
|
440
|
+
if not identifier:
|
|
441
|
+
logger.warning(
|
|
442
|
+
"Not sending %s for feature %s: an exposure requires an"
|
|
443
|
+
" identifier to reconcile with conversion events.",
|
|
444
|
+
FLAG_EXPOSURE_EVENT,
|
|
445
|
+
feature_name,
|
|
446
|
+
)
|
|
447
|
+
return
|
|
421
448
|
self._event_processor.track_exposure_event(
|
|
422
449
|
feature_name=feature_name,
|
|
423
450
|
identifier=identifier,
|
|
@@ -52,6 +52,7 @@ class Flag(BaseFlag):
|
|
|
52
52
|
feature_id: int
|
|
53
53
|
feature_name: str
|
|
54
54
|
variant: typing.Optional[str] = None
|
|
55
|
+
reason: typing.Optional[str] = None
|
|
55
56
|
is_default: bool = field(default=False)
|
|
56
57
|
|
|
57
58
|
@classmethod
|
|
@@ -66,6 +67,7 @@ class Flag(BaseFlag):
|
|
|
66
67
|
feature_name=flag_result["name"],
|
|
67
68
|
feature_id=metadata["id"],
|
|
68
69
|
variant=flag_result.get("variant"),
|
|
70
|
+
reason=flag_result["reason"],
|
|
69
71
|
)
|
|
70
72
|
raise ValueError(
|
|
71
73
|
"FlagResult metadata is missing. Cannot create Flag instance. "
|
|
@@ -80,6 +82,7 @@ class Flag(BaseFlag):
|
|
|
80
82
|
feature_name=flag_data["feature"]["name"],
|
|
81
83
|
feature_id=flag_data["feature"]["id"],
|
|
82
84
|
variant=flag_data.get("variant"),
|
|
85
|
+
reason=flag_data.get("reason"),
|
|
83
86
|
)
|
|
84
87
|
|
|
85
88
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|