flagsmith 6.1.0__tar.gz → 6.2.1__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.1}/PKG-INFO +1 -1
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/analytics.py +26 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/flagsmith.py +33 -6
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/mappers.py +1 -1
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/models.py +12 -7
- {flagsmith-6.1.0 → flagsmith-6.2.1}/pyproject.toml +1 -1
- {flagsmith-6.1.0 → flagsmith-6.2.1}/LICENSE +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/README.md +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/__init__.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/api/__init__.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/api/types.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/exceptions.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/offline_handlers.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/polling_manager.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/py.typed +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/streaming_manager.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/types.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/utils/__init__.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/utils/datetime.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/utils/identities.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/flagsmith/version.py +0 -0
- {flagsmith-6.1.0 → flagsmith-6.2.1}/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,
|
|
@@ -177,7 +177,7 @@ def _map_identity_overrides_to_segments(
|
|
|
177
177
|
# Generate a unique key to avoid collisions
|
|
178
178
|
segment_key = str(hash(overrides_key))
|
|
179
179
|
segment_contexts[segment_key] = SegmentContext(
|
|
180
|
-
key=
|
|
180
|
+
key=segment_key,
|
|
181
181
|
name="identity_overrides",
|
|
182
182
|
rules=[
|
|
183
183
|
{
|
|
@@ -17,7 +17,7 @@ from flagsmith.types import (
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
SegmentOverridesIndex = typing.Dict[
|
|
20
|
-
str, typing.
|
|
20
|
+
str, typing.Dict[str, SegmentContext[SegmentMetadata, FeatureMetadata]]
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
|
|
@@ -28,11 +28,16 @@ def build_segment_overrides_index(
|
|
|
28
28
|
|
|
29
29
|
Computed once per environment-document refresh so the lazy eval path
|
|
30
30
|
can walk only the segments actually relevant to a given flag.
|
|
31
|
+
|
|
32
|
+
Each entry keeps the context's own segment keys, so the trimmed
|
|
33
|
+
contexts built by `Flags._resolve_flag` stay a faithful subset of
|
|
34
|
+
the environment. Re-keying them by anything else risks collisions
|
|
35
|
+
silently dropping segments.
|
|
31
36
|
"""
|
|
32
37
|
index: SegmentOverridesIndex = {}
|
|
33
|
-
for segment_context in (context.get("segments") or {}).
|
|
38
|
+
for segment_key, segment_context in (context.get("segments") or {}).items():
|
|
34
39
|
for override in segment_context.get("overrides") or ():
|
|
35
|
-
index.setdefault(override["name"], []
|
|
40
|
+
index.setdefault(override["name"], {})[segment_key] = segment_context
|
|
36
41
|
return index
|
|
37
42
|
|
|
38
43
|
|
|
@@ -52,6 +57,7 @@ class Flag(BaseFlag):
|
|
|
52
57
|
feature_id: int
|
|
53
58
|
feature_name: str
|
|
54
59
|
variant: typing.Optional[str] = None
|
|
60
|
+
reason: typing.Optional[str] = None
|
|
55
61
|
is_default: bool = field(default=False)
|
|
56
62
|
|
|
57
63
|
@classmethod
|
|
@@ -66,6 +72,7 @@ class Flag(BaseFlag):
|
|
|
66
72
|
feature_name=flag_result["name"],
|
|
67
73
|
feature_id=metadata["id"],
|
|
68
74
|
variant=flag_result.get("variant"),
|
|
75
|
+
reason=flag_result["reason"],
|
|
69
76
|
)
|
|
70
77
|
raise ValueError(
|
|
71
78
|
"FlagResult metadata is missing. Cannot create Flag instance. "
|
|
@@ -80,6 +87,7 @@ class Flag(BaseFlag):
|
|
|
80
87
|
feature_name=flag_data["feature"]["name"],
|
|
81
88
|
feature_id=flag_data["feature"]["id"],
|
|
82
89
|
variant=flag_data.get("variant"),
|
|
90
|
+
reason=flag_data.get("reason"),
|
|
83
91
|
)
|
|
84
92
|
|
|
85
93
|
|
|
@@ -251,10 +259,7 @@ class Flags:
|
|
|
251
259
|
trimmed: SDKEvaluationContext = {
|
|
252
260
|
**context,
|
|
253
261
|
"features": {feature_name: context["features"][feature_name]},
|
|
254
|
-
"segments": {
|
|
255
|
-
segment_context["key"]: segment_context
|
|
256
|
-
for segment_context in overrides_index.get(feature_name, ())
|
|
257
|
-
},
|
|
262
|
+
"segments": overrides_index.get(feature_name, {}),
|
|
258
263
|
}
|
|
259
264
|
result = engine.get_evaluation_result(trimmed)
|
|
260
265
|
return Flag.from_evaluation_result(result["flags"][feature_name])
|
|
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
|