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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flagsmith
3
- Version: 6.1.0
3
+ Version: 6.2.0
4
4
  Summary: Flagsmith Python SDK
5
5
  License: BSD3
6
6
  License-File: LICENSE
@@ -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
- Skips the exposure event when the resolved flag is a `DefaultFlag`
372
- (i.e. the feature was not present and was served via the
373
- `default_flag_handler`) or when the feature is disabled, to keep
374
- experimentation data clean.
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) and flag.enabled:
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 if flag.variant is not None else flag.value,
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
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "flagsmith"
3
- version = "6.1.0"
3
+ version = "6.2.0"
4
4
  description = "Flagsmith Python SDK"
5
5
  authors = ["Flagsmith <support@flagsmith.com>"]
6
6
  license = "BSD3"
File without changes
File without changes
File without changes
File without changes