feature_flags.py 2.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. import copy
  2. import sentry_sdk
  3. from sentry_sdk._lru_cache import LRUCache
  4. from sentry_sdk.tracing import Span
  5. from threading import Lock
  6. from typing import TYPE_CHECKING, Any
  7. if TYPE_CHECKING:
  8. from typing import TypedDict
  9. FlagData = TypedDict("FlagData", {"flag": str, "result": bool})
  10. DEFAULT_FLAG_CAPACITY = 100
  11. class FlagBuffer:
  12. def __init__(self, capacity: int) -> None:
  13. self.capacity = capacity
  14. self.lock = Lock()
  15. # Buffer is private. The name is mangled to discourage use. If you use this attribute
  16. # directly you're on your own!
  17. self.__buffer = LRUCache(capacity)
  18. def clear(self) -> None:
  19. self.__buffer = LRUCache(self.capacity)
  20. def __deepcopy__(self, memo: "dict[int, Any]") -> "FlagBuffer":
  21. with self.lock:
  22. buffer = FlagBuffer(self.capacity)
  23. buffer.__buffer = copy.deepcopy(self.__buffer, memo)
  24. return buffer
  25. def get(self) -> "list[FlagData]":
  26. with self.lock:
  27. return [
  28. {"flag": key, "result": value} for key, value in self.__buffer.get_all()
  29. ]
  30. def set(self, flag: str, result: bool) -> None:
  31. if isinstance(result, FlagBuffer):
  32. # If someone were to insert `self` into `self` this would create a circular dependency
  33. # on the lock. This is of course a deadlock. However, this is far outside the expected
  34. # usage of this class. We guard against it here for completeness and to document this
  35. # expected failure mode.
  36. raise ValueError(
  37. "FlagBuffer instances can not be inserted into the dictionary."
  38. )
  39. with self.lock:
  40. self.__buffer.set(flag, result)
  41. def add_feature_flag(flag: str, result: bool) -> None:
  42. """
  43. Records a flag and its value to be sent on subsequent error events.
  44. We recommend you do this on flag evaluations. Flags are buffered per Sentry scope.
  45. """
  46. flags = sentry_sdk.get_isolation_scope().flags
  47. flags.set(flag, result)
  48. span = sentry_sdk.get_current_span()
  49. if span and isinstance(span, Span):
  50. span.set_flag(f"flag.evaluation.{flag}", result)