Skip to content

Commit 5b335c0

Browse files
authored
Merge branch 'master' into matt/python-rust-tracing-integration
2 parents 4d72832 + dd1117d commit 5b335c0

File tree

19 files changed

+499
-1
lines changed

19 files changed

+499
-1
lines changed

.github/workflows/test-integrations-miscellaneous.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,18 @@ jobs:
4545
- name: Erase coverage
4646
run: |
4747
coverage erase
48+
- name: Test launchdarkly latest
49+
run: |
50+
set -x # print commands that are executed
51+
./scripts/runtox.sh "py${{ matrix.python-version }}-launchdarkly-latest"
4852
- name: Test loguru latest
4953
run: |
5054
set -x # print commands that are executed
5155
./scripts/runtox.sh "py${{ matrix.python-version }}-loguru-latest"
56+
- name: Test openfeature latest
57+
run: |
58+
set -x # print commands that are executed
59+
./scripts/runtox.sh "py${{ matrix.python-version }}-openfeature-latest"
5260
- name: Test opentelemetry latest
5361
run: |
5462
set -x # print commands that are executed
@@ -117,10 +125,18 @@ jobs:
117125
- name: Erase coverage
118126
run: |
119127
coverage erase
128+
- name: Test launchdarkly pinned
129+
run: |
130+
set -x # print commands that are executed
131+
./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-launchdarkly"
120132
- name: Test loguru pinned
121133
run: |
122134
set -x # print commands that are executed
123135
./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-loguru"
136+
- name: Test openfeature pinned
137+
run: |
138+
set -x # print commands that are executed
139+
./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-openfeature"
124140
- name: Test opentelemetry pinned
125141
run: |
126142
set -x # print commands that are executed

mypy.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ ignore_missing_imports = True
7474
ignore_missing_imports = True
7575
[mypy-openai.*]
7676
ignore_missing_imports = True
77+
[mypy-openfeature.*]
78+
ignore_missing_imports = True
7779
[mypy-huggingface_hub.*]
7880
ignore_missing_imports = True
7981
[mypy-arq.*]

requirements-linting.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ flake8-bugbear
1515
pep8-naming
1616
pre-commit # local linting
1717
httpcore
18+
openfeature-sdk
19+
launchdarkly-server-sdk

scripts/split-tox-gh-actions/split-tox-gh-actions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@
125125
"tornado",
126126
],
127127
"Miscellaneous": [
128+
"launchdarkly",
128129
"loguru",
130+
"openfeature",
129131
"opentelemetry",
130132
"potel",
131133
"pure_eval",

sentry_sdk/_lru_cache.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
6363
"""
6464

65+
from copy import copy
66+
6567
SENTINEL = object()
6668

6769

@@ -89,6 +91,13 @@ def __init__(self, max_size):
8991

9092
self.hits = self.misses = 0
9193

94+
def __copy__(self):
95+
cache = LRUCache(self.max_size)
96+
cache.full = self.full
97+
cache.cache = copy(self.cache)
98+
cache.root = copy(self.root)
99+
return cache
100+
92101
def set(self, key, value):
93102
link = self.cache.get(key, SENTINEL)
94103

@@ -154,3 +163,11 @@ def get(self, key, default=None):
154163
self.hits += 1
155164

156165
return link[VALUE]
166+
167+
def get_all(self):
168+
nodes = []
169+
node = self.root[NEXT]
170+
while node is not self.root:
171+
nodes.append((node[KEY], node[VALUE]))
172+
node = node[NEXT]
173+
return nodes

sentry_sdk/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class CompressionAlgo(Enum):
5959
"Experiments",
6060
{
6161
"max_spans": Optional[int],
62+
"max_flags": Optional[int],
6263
"record_sql_params": Optional[bool],
6364
"continuous_profiling_auto_start": Optional[bool],
6465
"continuous_profiling_mode": Optional[ContinuousProfilerMode],

sentry_sdk/flag_utils.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from copy import copy
2+
from typing import TYPE_CHECKING
3+
4+
import sentry_sdk
5+
from sentry_sdk._lru_cache import LRUCache
6+
7+
if TYPE_CHECKING:
8+
from typing import TypedDict, Optional
9+
from sentry_sdk._types import Event, ExcInfo
10+
11+
FlagData = TypedDict("FlagData", {"flag": str, "result": bool})
12+
13+
14+
DEFAULT_FLAG_CAPACITY = 100
15+
16+
17+
class FlagBuffer:
18+
19+
def __init__(self, capacity):
20+
# type: (int) -> None
21+
self.buffer = LRUCache(capacity)
22+
self.capacity = capacity
23+
24+
def clear(self):
25+
# type: () -> None
26+
self.buffer = LRUCache(self.capacity)
27+
28+
def __copy__(self):
29+
# type: () -> FlagBuffer
30+
buffer = FlagBuffer(capacity=self.capacity)
31+
buffer.buffer = copy(self.buffer)
32+
return buffer
33+
34+
def get(self):
35+
# type: () -> list[FlagData]
36+
return [{"flag": key, "result": value} for key, value in self.buffer.get_all()]
37+
38+
def set(self, flag, result):
39+
# type: (str, bool) -> None
40+
self.buffer.set(flag, result)
41+
42+
43+
def flag_error_processor(event, exc_info):
44+
# type: (Event, ExcInfo) -> Optional[Event]
45+
scope = sentry_sdk.get_current_scope()
46+
event["contexts"]["flags"] = {"values": scope.flags.get()}
47+
return event

sentry_sdk/hub.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def current(cls):
101101
rv = _local.get(None)
102102
if rv is None:
103103
with _suppress_hub_deprecation_warning():
104-
# This will raise a deprecation warning; supress it since we already warned above.
104+
# This will raise a deprecation warning; suppress it since we already warned above.
105105
rv = Hub(GLOBAL_HUB)
106106
_local.set(rv)
107107
return rv
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from typing import TYPE_CHECKING
2+
import sentry_sdk
3+
4+
from sentry_sdk.integrations import DidNotEnable, Integration
5+
from sentry_sdk.flag_utils import flag_error_processor
6+
7+
try:
8+
import ldclient
9+
from ldclient.hook import Hook, Metadata
10+
11+
if TYPE_CHECKING:
12+
from ldclient import LDClient
13+
from ldclient.hook import EvaluationSeriesContext
14+
from ldclient.evaluation import EvaluationDetail
15+
16+
from typing import Any
17+
except ImportError:
18+
raise DidNotEnable("LaunchDarkly is not installed")
19+
20+
21+
class LaunchDarklyIntegration(Integration):
22+
identifier = "launchdarkly"
23+
24+
def __init__(self, ld_client=None):
25+
# type: (LDClient | None) -> None
26+
"""
27+
:param client: An initialized LDClient instance. If a client is not provided, this
28+
integration will attempt to use the shared global instance.
29+
"""
30+
try:
31+
client = ld_client or ldclient.get()
32+
except Exception as exc:
33+
raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc))
34+
35+
if not client.is_initialized():
36+
raise DidNotEnable("LaunchDarkly client is not initialized.")
37+
38+
# Register the flag collection hook with the LD client.
39+
client.add_hook(LaunchDarklyHook())
40+
41+
@staticmethod
42+
def setup_once():
43+
# type: () -> None
44+
scope = sentry_sdk.get_current_scope()
45+
scope.add_error_processor(flag_error_processor)
46+
47+
48+
class LaunchDarklyHook(Hook):
49+
50+
@property
51+
def metadata(self):
52+
# type: () -> Metadata
53+
return Metadata(name="sentry-feature-flag-recorder")
54+
55+
def after_evaluation(self, series_context, data, detail):
56+
# type: (EvaluationSeriesContext, dict[Any, Any], EvaluationDetail) -> dict[Any, Any]
57+
if isinstance(detail.value, bool):
58+
flags = sentry_sdk.get_current_scope().flags
59+
flags.set(series_context.key, detail.value)
60+
return data
61+
62+
def before_evaluation(self, series_context, data):
63+
# type: (EvaluationSeriesContext, dict[Any, Any]) -> dict[Any, Any]
64+
return data # No-op.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from typing import TYPE_CHECKING
2+
import sentry_sdk
3+
4+
from sentry_sdk.integrations import DidNotEnable, Integration
5+
from sentry_sdk.flag_utils import flag_error_processor
6+
7+
try:
8+
from openfeature import api
9+
from openfeature.hook import Hook
10+
11+
if TYPE_CHECKING:
12+
from openfeature.flag_evaluation import FlagEvaluationDetails
13+
from openfeature.hook import HookContext, HookHints
14+
except ImportError:
15+
raise DidNotEnable("OpenFeature is not installed")
16+
17+
18+
class OpenFeatureIntegration(Integration):
19+
identifier = "openfeature"
20+
21+
@staticmethod
22+
def setup_once():
23+
# type: () -> None
24+
scope = sentry_sdk.get_current_scope()
25+
scope.add_error_processor(flag_error_processor)
26+
27+
# Register the hook within the global openfeature hooks list.
28+
api.add_hooks(hooks=[OpenFeatureHook()])
29+
30+
31+
class OpenFeatureHook(Hook):
32+
33+
def after(self, hook_context, details, hints):
34+
# type: (HookContext, FlagEvaluationDetails[bool], HookHints) -> None
35+
if isinstance(details.value, bool):
36+
flags = sentry_sdk.get_current_scope().flags
37+
flags.set(details.flag_key, details.value)
38+
39+
def error(self, hook_context, exception, hints):
40+
# type: (HookContext, Exception, HookHints) -> None
41+
if isinstance(hook_context.default_value, bool):
42+
flags = sentry_sdk.get_current_scope().flags
43+
flags.set(hook_context.flag_key, hook_context.default_value)

0 commit comments

Comments
 (0)