flagsmith 6.0.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.0.0 → flagsmith-6.2.0}/PKG-INFO +1 -1
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/analytics.py +37 -2
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/flagsmith.py +55 -15
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/models.py +3 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/pyproject.toml +1 -1
- {flagsmith-6.0.0 → flagsmith-6.2.0}/LICENSE +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/README.md +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/__init__.py +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/api/__init__.py +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/api/types.py +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/exceptions.py +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/mappers.py +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/offline_handlers.py +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/polling_manager.py +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/py.typed +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/streaming_manager.py +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/types.py +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/utils/__init__.py +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/utils/datetime.py +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/utils/identities.py +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/version.py +0 -0
- {flagsmith-6.0.0 → flagsmith-6.2.0}/flagsmith/webhooks.py +0 -0
|
@@ -31,7 +31,11 @@ class AnalyticsProcessor:
|
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
33
|
def __init__(
|
|
34
|
-
self,
|
|
34
|
+
self,
|
|
35
|
+
environment_key: str,
|
|
36
|
+
base_api_url: str,
|
|
37
|
+
timeout: typing.Optional[int] = 3,
|
|
38
|
+
analytics_url: typing.Optional[str] = None,
|
|
35
39
|
):
|
|
36
40
|
"""
|
|
37
41
|
Initialise the AnalyticsProcessor to handle sending analytics on flag usage to
|
|
@@ -41,8 +45,13 @@ class AnalyticsProcessor:
|
|
|
41
45
|
:param base_api_url: base api url to override when using self hosted version
|
|
42
46
|
:param timeout: used to tell requests to stop waiting for a response after a
|
|
43
47
|
given number of seconds
|
|
48
|
+
:param analytics_url: full URL of the flag analytics endpoint, used to override
|
|
49
|
+
the default ``<base_api_url>/analytics/flags/``. Intended for deployments
|
|
50
|
+
where flag evaluation traffic and analytics traffic must go to different
|
|
51
|
+
hosts (for example, evaluating through the Edge Proxy while sending
|
|
52
|
+
analytics to the core API).
|
|
44
53
|
"""
|
|
45
|
-
self.analytics_endpoint = base_api_url + ANALYTICS_ENDPOINT
|
|
54
|
+
self.analytics_endpoint = analytics_url or (base_api_url + ANALYTICS_ENDPOINT)
|
|
46
55
|
self.environment_key = environment_key
|
|
47
56
|
self._last_flushed = datetime.now()
|
|
48
57
|
self.analytics_data: typing.MutableMapping[str, typing.Any] = {}
|
|
@@ -74,6 +83,12 @@ class AnalyticsProcessor:
|
|
|
74
83
|
self.flush()
|
|
75
84
|
|
|
76
85
|
|
|
86
|
+
class ExposureKey(typing.NamedTuple):
|
|
87
|
+
feature_name: typing.Optional[str]
|
|
88
|
+
identifier: typing.Optional[str]
|
|
89
|
+
value: typing.Optional[str]
|
|
90
|
+
|
|
91
|
+
|
|
77
92
|
@dataclass
|
|
78
93
|
class EventProcessorConfig:
|
|
79
94
|
events_api_url: str = DEFAULT_EVENT_API_URL
|
|
@@ -102,6 +117,7 @@ class EventProcessor:
|
|
|
102
117
|
self._flush_interval_seconds = config.flush_interval_seconds
|
|
103
118
|
|
|
104
119
|
self._buffer: typing.List[typing.Dict[str, typing.Any]] = []
|
|
120
|
+
self._buffered_exposure_keys: typing.Set[ExposureKey] = set()
|
|
105
121
|
self._lock = threading.Lock()
|
|
106
122
|
self._timer: typing.Optional[threading.Timer] = None
|
|
107
123
|
|
|
@@ -150,6 +166,24 @@ class EventProcessor:
|
|
|
150
166
|
) -> None:
|
|
151
167
|
should_flush = False
|
|
152
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)
|
|
153
187
|
self._buffer.append(
|
|
154
188
|
{
|
|
155
189
|
"event": event,
|
|
@@ -173,6 +207,7 @@ class EventProcessor:
|
|
|
173
207
|
return
|
|
174
208
|
events = self._buffer
|
|
175
209
|
self._buffer = []
|
|
210
|
+
self._buffered_exposure_keys.clear()
|
|
176
211
|
|
|
177
212
|
payload = json.dumps({"events": events})
|
|
178
213
|
try:
|
|
@@ -70,6 +70,7 @@ class Flagsmith:
|
|
|
70
70
|
environment_key: typing.Optional[str] = None,
|
|
71
71
|
api_url: typing.Optional[str] = None,
|
|
72
72
|
realtime_api_url: typing.Optional[str] = None,
|
|
73
|
+
analytics_url: typing.Optional[str] = None,
|
|
73
74
|
custom_headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
74
75
|
request_timeout_seconds: typing.Optional[int] = 10,
|
|
75
76
|
enable_local_evaluation: bool = False,
|
|
@@ -92,6 +93,11 @@ class Flagsmith:
|
|
|
92
93
|
Required unless offline_mode is True.
|
|
93
94
|
:param api_url: Override the URL of the Flagsmith API to communicate with
|
|
94
95
|
:param realtime_api_url: Override the URL of the Flagsmith real-time API
|
|
96
|
+
:param analytics_url: Override the URL used for flag analytics requests when
|
|
97
|
+
enable_analytics is True. When unset, analytics are posted to
|
|
98
|
+
``<api_url>/analytics/flags/``. Set this when api_url points at a host that
|
|
99
|
+
does not handle analytics (for example, the Edge Proxy) so analytics can be
|
|
100
|
+
sent directly to the core Flagsmith API.
|
|
95
101
|
:param custom_headers: Additional headers to add to requests made to the
|
|
96
102
|
Flagsmith API
|
|
97
103
|
:param request_timeout_seconds: Number of seconds to wait for a request to
|
|
@@ -172,14 +178,12 @@ class Flagsmith:
|
|
|
172
178
|
self.session.proxies.update(proxies or {})
|
|
173
179
|
retries = retries or Retry(total=3, backoff_factor=0.1)
|
|
174
180
|
|
|
175
|
-
api_url = api_url or DEFAULT_API_URL
|
|
176
|
-
self.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
self.
|
|
180
|
-
|
|
181
|
-
if realtime_api_url.endswith("/")
|
|
182
|
-
else f"{realtime_api_url}/"
|
|
181
|
+
self.api_url = self._ensure_trailing_slash(api_url or DEFAULT_API_URL)
|
|
182
|
+
self.realtime_api_url = self._ensure_trailing_slash(
|
|
183
|
+
realtime_api_url or DEFAULT_REALTIME_API_URL
|
|
184
|
+
)
|
|
185
|
+
self.analytics_url = (
|
|
186
|
+
self._ensure_trailing_slash(analytics_url) if analytics_url else None
|
|
183
187
|
)
|
|
184
188
|
|
|
185
189
|
self.request_timeout_seconds = request_timeout_seconds
|
|
@@ -201,6 +205,7 @@ class Flagsmith:
|
|
|
201
205
|
self._initialise_analytics(
|
|
202
206
|
environment_key=environment_key,
|
|
203
207
|
enable_analytics=enable_analytics,
|
|
208
|
+
analytics_url=self.analytics_url,
|
|
204
209
|
)
|
|
205
210
|
self._initialise_events(
|
|
206
211
|
environment_key=environment_key,
|
|
@@ -208,14 +213,22 @@ class Flagsmith:
|
|
|
208
213
|
event_processor_config=event_processor_config,
|
|
209
214
|
)
|
|
210
215
|
|
|
216
|
+
@staticmethod
|
|
217
|
+
def _ensure_trailing_slash(url: str) -> str:
|
|
218
|
+
return url if url.endswith("/") else f"{url}/"
|
|
219
|
+
|
|
211
220
|
def _initialise_analytics(
|
|
212
221
|
self,
|
|
213
222
|
environment_key: str,
|
|
214
223
|
enable_analytics: bool,
|
|
224
|
+
analytics_url: typing.Optional[str] = None,
|
|
215
225
|
) -> None:
|
|
216
226
|
if enable_analytics:
|
|
217
227
|
self._analytics_processor = AnalyticsProcessor(
|
|
218
|
-
environment_key,
|
|
228
|
+
environment_key,
|
|
229
|
+
self.api_url,
|
|
230
|
+
timeout=self.request_timeout_seconds,
|
|
231
|
+
analytics_url=analytics_url,
|
|
219
232
|
)
|
|
220
233
|
|
|
221
234
|
def _initialise_events(
|
|
@@ -355,19 +368,38 @@ class Flagsmith:
|
|
|
355
368
|
"""
|
|
356
369
|
Resolve a flag for an identity and record an exposure event.
|
|
357
370
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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.
|
|
362
376
|
"""
|
|
363
377
|
if not self._event_processor:
|
|
364
378
|
raise ValueError("Events must be enabled to use experiment flags.")
|
|
365
379
|
flag = self.get_identity_flags(identifier, traits).get_flag(feature_name)
|
|
366
|
-
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:
|
|
367
399
|
self.track_exposure_event(
|
|
368
400
|
feature_name=feature_name,
|
|
369
401
|
identifier=identifier,
|
|
370
|
-
value=flag.variant
|
|
402
|
+
value=flag.variant,
|
|
371
403
|
traits=traits,
|
|
372
404
|
)
|
|
373
405
|
return flag
|
|
@@ -405,6 +437,14 @@ class Flagsmith:
|
|
|
405
437
|
) -> None:
|
|
406
438
|
if not self._event_processor:
|
|
407
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
|
|
408
448
|
self._event_processor.track_exposure_event(
|
|
409
449
|
feature_name=feature_name,
|
|
410
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
|