Skip to content

Commit 633b3ed

Browse files
committed
Wrap UnleashClient directly
1 parent 800354d commit 633b3ed

File tree

3 files changed

+156
-142
lines changed

3 files changed

+156
-142
lines changed

sentry_sdk/integrations/unleash.py

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,30 @@
11
from functools import wraps
2-
from typing import TYPE_CHECKING
2+
from typing import Any
33

44
import sentry_sdk
55
from sentry_sdk.flag_utils import flag_error_processor
66
from sentry_sdk.integrations import Integration, DidNotEnable
77

8-
if TYPE_CHECKING:
9-
from typing import Any, Optional
10-
11-
try:
12-
from UnleashClient import UnleashClient
13-
except ImportError:
14-
raise DidNotEnable("UnleashClient is not installed")
8+
try:
9+
from UnleashClient import UnleashClient
10+
except ImportError:
11+
raise DidNotEnable("UnleashClient is not installed")
1512

1613

1714
class UnleashIntegration(Integration):
1815
identifier = "unleash"
19-
_unleash_client = None # type: Optional[UnleashClient]
20-
21-
def __init__(self, unleash_client):
22-
# type: (UnleashClient) -> None
23-
self.__class__._unleash_client = unleash_client
2416

2517
@staticmethod
2618
def setup_once():
2719
# type: () -> None
28-
29-
client = UnleashIntegration._unleash_client
30-
if not client:
31-
raise DidNotEnable("Error getting UnleashClient instance")
32-
3320
# Wrap and patch evaluation methods (instance methods)
34-
old_is_enabled = client.is_enabled
35-
old_get_variant = client.get_variant
21+
old_is_enabled = UnleashClient.is_enabled
22+
old_get_variant = UnleashClient.get_variant
3623

3724
@wraps(old_is_enabled)
38-
def sentry_is_enabled(feature, *a, **kw):
39-
# type: (str, *Any, **Any) -> Any
40-
enabled = old_is_enabled(feature, *a, **kw)
25+
def sentry_is_enabled(self, feature, *args, **kwargs):
26+
# type: (UnleashClient, str, *Any, **Any) -> Any
27+
enabled = old_is_enabled(self, feature, *args, **kwargs)
4128

4229
# We have no way of knowing what type of unleash feature this is, so we have to treat
4330
# it as a boolean / toggle feature.
@@ -47,11 +34,10 @@ def sentry_is_enabled(feature, *a, **kw):
4734
return enabled
4835

4936
@wraps(old_get_variant)
50-
def sentry_get_variant(feature, *a, **kw):
51-
# type: (str, *Any, **Any) -> Any
52-
variant = old_get_variant(feature, *a, **kw)
37+
def sentry_get_variant(self, feature, *args, **kwargs):
38+
# type: (UnleashClient, str, *Any, **Any) -> Any
39+
variant = old_get_variant(self, feature, *args, **kwargs)
5340
enabled = variant.get("enabled", False)
54-
# _payload_type = variant.get("payload", {}).get("type")
5541

5642
# Payloads are not always used as the feature's value for application logic. They
5743
# may be used for metrics or debugging context instead. Therefore, we treat every
@@ -60,8 +46,8 @@ def sentry_get_variant(feature, *a, **kw):
6046
flags.set(feature, enabled)
6147
return variant
6248

63-
client.is_enabled = sentry_is_enabled # type: ignore
64-
client.get_variant = sentry_get_variant # type: ignore
49+
UnleashClient.is_enabled = sentry_is_enabled # type: ignore
50+
UnleashClient.get_variant = sentry_get_variant # type: ignore
6551

6652
# Error processor
6753
scope = sentry_sdk.get_current_scope()

tests/integrations/unleash/test_unleash.py

Lines changed: 103 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@
22
import sys
33
from random import random
44
from unittest import mock
5+
from UnleashClient import UnleashClient
56

67
import pytest
78

89
import sentry_sdk
910
from sentry_sdk.integrations.unleash import UnleashIntegration
10-
from tests.integrations.unleash.testutils import MockUnleashClient
11+
from tests.integrations.unleash.testutils import mock_unleash_client
1112

1213

1314
def test_is_enabled(sentry_init, capture_events, uninstall_integration):
14-
client = MockUnleashClient()
1515
uninstall_integration(UnleashIntegration)
16-
sentry_init(integrations=[UnleashIntegration(client)]) # type: ignore
1716

18-
client.is_enabled("hello")
19-
client.is_enabled("world")
20-
client.is_enabled("other")
17+
with mock_unleash_client():
18+
client = UnleashClient()
19+
sentry_init(integrations=[UnleashIntegration()])
20+
client.is_enabled("hello")
21+
client.is_enabled("world")
22+
client.is_enabled("other")
2123

2224
events = capture_events()
2325
sentry_sdk.capture_exception(Exception("something wrong!"))
@@ -33,16 +35,17 @@ def test_is_enabled(sentry_init, capture_events, uninstall_integration):
3335

3436

3537
def test_get_variant(sentry_init, capture_events, uninstall_integration):
36-
client = MockUnleashClient()
3738
uninstall_integration(UnleashIntegration)
38-
sentry_init(integrations=[UnleashIntegration(client)]) # type: ignore
3939

40-
client.get_variant("no_payload_feature")
41-
client.get_variant("string_feature")
42-
client.get_variant("json_feature")
43-
client.get_variant("csv_feature")
44-
client.get_variant("number_feature")
45-
client.get_variant("unknown_feature")
40+
with mock_unleash_client():
41+
client = UnleashClient()
42+
sentry_init(integrations=[UnleashIntegration()]) # type: ignore
43+
client.get_variant("no_payload_feature")
44+
client.get_variant("string_feature")
45+
client.get_variant("json_feature")
46+
client.get_variant("csv_feature")
47+
client.get_variant("number_feature")
48+
client.get_variant("unknown_feature")
4649

4750
events = capture_events()
4851
sentry_sdk.capture_exception(Exception("something wrong!"))
@@ -61,25 +64,27 @@ def test_get_variant(sentry_init, capture_events, uninstall_integration):
6164

6265

6366
def test_is_enabled_threaded(sentry_init, capture_events, uninstall_integration):
64-
client = MockUnleashClient()
6567
uninstall_integration(UnleashIntegration)
66-
sentry_init(integrations=[UnleashIntegration(client)]) # type: ignore
67-
events = capture_events()
6868

69-
def task(flag_key):
70-
# Creates a new isolation scope for the thread.
71-
# This means the evaluations in each task are captured separately.
72-
with sentry_sdk.isolation_scope():
73-
client.is_enabled(flag_key)
74-
# use a tag to identify to identify events later on
75-
sentry_sdk.set_tag("task_id", flag_key)
76-
sentry_sdk.capture_exception(Exception("something wrong!"))
69+
with mock_unleash_client():
70+
client = UnleashClient()
71+
sentry_init(integrations=[UnleashIntegration()]) # type: ignore
72+
events = capture_events()
73+
74+
def task(flag_key):
75+
# Creates a new isolation scope for the thread.
76+
# This means the evaluations in each task are captured separately.
77+
with sentry_sdk.isolation_scope():
78+
client.is_enabled(flag_key)
79+
# use a tag to identify to identify events later on
80+
sentry_sdk.set_tag("task_id", flag_key)
81+
sentry_sdk.capture_exception(Exception("something wrong!"))
7782

78-
# Capture an eval before we split isolation scopes.
79-
client.is_enabled("hello")
83+
# Capture an eval before we split isolation scopes.
84+
client.is_enabled("hello")
8085

81-
with cf.ThreadPoolExecutor(max_workers=2) as pool:
82-
pool.map(task, ["world", "other"])
86+
with cf.ThreadPoolExecutor(max_workers=2) as pool:
87+
pool.map(task, ["world", "other"])
8388

8489
# Capture error in original scope
8590
sentry_sdk.set_tag("task_id", "0")
@@ -108,25 +113,27 @@ def task(flag_key):
108113

109114

110115
def test_get_variant_threaded(sentry_init, capture_events, uninstall_integration):
111-
client = MockUnleashClient()
112116
uninstall_integration(UnleashIntegration)
113-
sentry_init(integrations=[UnleashIntegration(client)]) # type: ignore
114-
events = capture_events()
115117

116-
def task(flag_key):
117-
# Creates a new isolation scope for the thread.
118-
# This means the evaluations in each task are captured separately.
119-
with sentry_sdk.isolation_scope():
120-
client.get_variant(flag_key)
121-
# use a tag to identify to identify events later on
122-
sentry_sdk.set_tag("task_id", flag_key)
123-
sentry_sdk.capture_exception(Exception("something wrong!"))
118+
with mock_unleash_client():
119+
client = UnleashClient()
120+
sentry_init(integrations=[UnleashIntegration()]) # type: ignore
121+
events = capture_events()
124122

125-
# Capture an eval before we split isolation scopes.
126-
client.get_variant("hello")
123+
def task(flag_key):
124+
# Creates a new isolation scope for the thread.
125+
# This means the evaluations in each task are captured separately.
126+
with sentry_sdk.isolation_scope():
127+
client.get_variant(flag_key)
128+
# use a tag to identify to identify events later on
129+
sentry_sdk.set_tag("task_id", flag_key)
130+
sentry_sdk.capture_exception(Exception("something wrong!"))
127131

128-
with cf.ThreadPoolExecutor(max_workers=2) as pool:
129-
pool.map(task, ["no_payload_feature", "other"])
132+
# Capture an eval before we split isolation scopes.
133+
client.get_variant("hello")
134+
135+
with cf.ThreadPoolExecutor(max_workers=2) as pool:
136+
pool.map(task, ["no_payload_feature", "other"])
130137

131138
# Capture error in original scope
132139
sentry_sdk.set_tag("task_id", "0")
@@ -157,26 +164,27 @@ def task(flag_key):
157164
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
158165
def test_is_enabled_asyncio(sentry_init, capture_events, uninstall_integration):
159166
asyncio = pytest.importorskip("asyncio")
160-
161-
client = MockUnleashClient()
162167
uninstall_integration(UnleashIntegration)
163-
sentry_init(integrations=[UnleashIntegration(client)]) # type: ignore
164-
events = capture_events()
165168

166-
async def task(flag_key):
167-
with sentry_sdk.isolation_scope():
168-
client.is_enabled(flag_key)
169-
# use a tag to identify to identify events later on
170-
sentry_sdk.set_tag("task_id", flag_key)
171-
sentry_sdk.capture_exception(Exception("something wrong!"))
169+
with mock_unleash_client():
170+
client = UnleashClient()
171+
sentry_init(integrations=[UnleashIntegration()]) # type: ignore
172+
events = capture_events()
172173

173-
async def runner():
174-
return asyncio.gather(task("world"), task("other"))
174+
async def task(flag_key):
175+
with sentry_sdk.isolation_scope():
176+
client.is_enabled(flag_key)
177+
# use a tag to identify to identify events later on
178+
sentry_sdk.set_tag("task_id", flag_key)
179+
sentry_sdk.capture_exception(Exception("something wrong!"))
175180

176-
# Capture an eval before we split isolation scopes.
177-
client.is_enabled("hello")
181+
async def runner():
182+
return asyncio.gather(task("world"), task("other"))
178183

179-
asyncio.run(runner())
184+
# Capture an eval before we split isolation scopes.
185+
client.is_enabled("hello")
186+
187+
asyncio.run(runner())
180188

181189
# Capture error in original scope
182190
sentry_sdk.set_tag("task_id", "0")
@@ -208,25 +216,27 @@ async def runner():
208216
def test_get_variant_asyncio(sentry_init, capture_events, uninstall_integration):
209217
asyncio = pytest.importorskip("asyncio")
210218

211-
client = MockUnleashClient()
212219
uninstall_integration(UnleashIntegration)
213-
sentry_init(integrations=[UnleashIntegration(client)]) # type: ignore
214-
events = capture_events()
215220

216-
async def task(flag_key):
217-
with sentry_sdk.isolation_scope():
218-
client.get_variant(flag_key)
219-
# use a tag to identify to identify events later on
220-
sentry_sdk.set_tag("task_id", flag_key)
221-
sentry_sdk.capture_exception(Exception("something wrong!"))
221+
with mock_unleash_client():
222+
client = UnleashClient()
223+
sentry_init(integrations=[UnleashIntegration()]) # type: ignore
224+
events = capture_events()
225+
226+
async def task(flag_key):
227+
with sentry_sdk.isolation_scope():
228+
client.get_variant(flag_key)
229+
# use a tag to identify to identify events later on
230+
sentry_sdk.set_tag("task_id", flag_key)
231+
sentry_sdk.capture_exception(Exception("something wrong!"))
222232

223-
async def runner():
224-
return asyncio.gather(task("no_payload_feature"), task("other"))
233+
async def runner():
234+
return asyncio.gather(task("no_payload_feature"), task("other"))
225235

226-
# Capture an eval before we split isolation scopes.
227-
client.get_variant("hello")
236+
# Capture an eval before we split isolation scopes.
237+
client.get_variant("hello")
228238

229-
asyncio.run(runner())
239+
asyncio.run(runner())
230240

231241
# Capture error in original scope
232242
sentry_sdk.set_tag("task_id", "0")
@@ -254,39 +264,17 @@ async def runner():
254264
}
255265

256266

257-
def test_client_isolation(sentry_init, capture_events, uninstall_integration):
258-
"""
259-
If the integration is tracking a single client, evaluations from other clients should not be
260-
captured.
261-
"""
262-
client = MockUnleashClient()
263-
uninstall_integration(UnleashIntegration)
264-
sentry_init(integrations=[UnleashIntegration(client)]) # type: ignore
265-
266-
other_client = MockUnleashClient()
267-
268-
other_client.is_enabled("hello")
269-
other_client.is_enabled("world")
270-
other_client.is_enabled("other")
271-
other_client.get_variant("no_payload_feature")
272-
other_client.get_variant("json_feature")
273-
274-
events = capture_events()
275-
sentry_sdk.capture_exception(Exception("something wrong!"))
276-
277-
assert len(events) == 1
278-
assert events[0]["contexts"]["flags"] == {"values": []}
279-
280-
281267
def test_wraps_original(sentry_init, uninstall_integration):
282-
client = MockUnleashClient()
283-
mock_is_enabled = mock.Mock(return_value=random() < 0.5)
284-
client.is_enabled = mock_is_enabled
285-
mock_get_variant = mock.Mock(return_value={"enabled": random() < 0.5})
286-
client.get_variant = mock_get_variant
268+
with mock_unleash_client():
269+
client = UnleashClient()
287270

288-
uninstall_integration(UnleashIntegration)
289-
sentry_init(integrations=[UnleashIntegration(client)]) # type: ignore
271+
mock_is_enabled = mock.Mock(return_value=random() < 0.5)
272+
mock_get_variant = mock.Mock(return_value={"enabled": random() < 0.5})
273+
client.is_enabled = mock_is_enabled
274+
client.get_variant = mock_get_variant
275+
276+
uninstall_integration(UnleashIntegration)
277+
sentry_init(integrations=[UnleashIntegration()]) # type: ignore
290278

291279
res = client.is_enabled("test-flag", "arg", kwarg=1)
292280
assert res == mock_is_enabled.return_value
@@ -304,15 +292,17 @@ def test_wraps_original(sentry_init, uninstall_integration):
304292

305293

306294
def test_wrapper_attributes(sentry_init, uninstall_integration):
307-
client = MockUnleashClient()
308-
original_is_enabled = client.is_enabled
309-
original_get_variant = client.get_variant
295+
with mock_unleash_client():
296+
client = UnleashClient() # <- Returns a MockUnleashClient
310297

311-
uninstall_integration(UnleashIntegration)
312-
sentry_init(integrations=[UnleashIntegration(client)]) # type: ignore
298+
original_is_enabled = client.is_enabled
299+
original_get_variant = client.get_variant
313300

314-
assert client.is_enabled.__name__ == "is_enabled"
315-
assert client.is_enabled.__qualname__ == original_is_enabled.__qualname__
301+
uninstall_integration(UnleashIntegration)
302+
sentry_init(integrations=[UnleashIntegration()]) # type: ignore
316303

317-
assert client.get_variant.__name__ == "get_variant"
318-
assert client.get_variant.__qualname__ == original_get_variant.__qualname__
304+
# Mock clients methods have not lost their qualified names after decoration.
305+
assert client.is_enabled.__name__ == "is_enabled"
306+
assert client.is_enabled.__qualname__ == original_is_enabled.__qualname__
307+
assert client.get_variant.__name__ == "get_variant"
308+
assert client.get_variant.__qualname__ == original_get_variant.__qualname__

0 commit comments

Comments
 (0)