Skip to content

Commit 2ee194c

Browse files
authored
feat(flags): remove Unleash get_variant patching code (#3914)
Follow-up to #3888 The original PR patched 2 methods used for evaluating feature flags, `is_enabled` (simple toggle on/off) and `get_variant` (returns a dict of metadata, see https://docs.getunleash.io/reference/sdks/python#getting-a-variant). We want to remove all `get_variant` code since we only support boolean flag evals atm. It seems like the main usecase for variants is reading payloads (non-bool) for A/B/multivariate testing. This could lead to a lot of extraneous flags, so until it is requested and/or we support non-bool values, let's not patch this method.
1 parent 288f69a commit 2ee194c

File tree

3 files changed

+9
-199
lines changed

3 files changed

+9
-199
lines changed

sentry_sdk/integrations/unleash.py

-16
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ def setup_once():
1818
# type: () -> None
1919
# Wrap and patch evaluation methods (instance methods)
2020
old_is_enabled = UnleashClient.is_enabled
21-
old_get_variant = UnleashClient.get_variant
2221

2322
@wraps(old_is_enabled)
2423
def sentry_is_enabled(self, feature, *args, **kwargs):
@@ -32,19 +31,4 @@ def sentry_is_enabled(self, feature, *args, **kwargs):
3231

3332
return enabled
3433

35-
@wraps(old_get_variant)
36-
def sentry_get_variant(self, feature, *args, **kwargs):
37-
# type: (UnleashClient, str, *Any, **Any) -> Any
38-
variant = old_get_variant(self, feature, *args, **kwargs)
39-
enabled = variant.get("enabled", False)
40-
41-
# Payloads are not always used as the feature's value for application logic. They
42-
# may be used for metrics or debugging context instead. Therefore, we treat every
43-
# variant as a boolean toggle, using the `enabled` field.
44-
flags = sentry_sdk.get_current_scope().flags
45-
flags.set(feature, enabled)
46-
47-
return variant
48-
4934
UnleashClient.is_enabled = sentry_is_enabled # type: ignore
50-
UnleashClient.get_variant = sentry_get_variant # type: ignore

tests/integrations/unleash/test_unleash.py

+7-149
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def test_is_enabled(sentry_init, capture_events, uninstall_integration):
1515
uninstall_integration(UnleashIntegration.identifier)
1616

1717
with mock_unleash_client():
18-
client = UnleashClient()
18+
client = UnleashClient() # type: ignore[arg-type]
1919
sentry_init(integrations=[UnleashIntegration()])
2020
client.is_enabled("hello")
2121
client.is_enabled("world")
@@ -34,41 +34,12 @@ def test_is_enabled(sentry_init, capture_events, uninstall_integration):
3434
}
3535

3636

37-
def test_get_variant(sentry_init, capture_events, uninstall_integration):
38-
uninstall_integration(UnleashIntegration.identifier)
39-
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")
49-
50-
events = capture_events()
51-
sentry_sdk.capture_exception(Exception("something wrong!"))
52-
53-
assert len(events) == 1
54-
assert events[0]["contexts"]["flags"] == {
55-
"values": [
56-
{"flag": "no_payload_feature", "result": True},
57-
{"flag": "string_feature", "result": True},
58-
{"flag": "json_feature", "result": True},
59-
{"flag": "csv_feature", "result": True},
60-
{"flag": "number_feature", "result": True},
61-
{"flag": "unknown_feature", "result": False},
62-
]
63-
}
64-
65-
6637
def test_is_enabled_threaded(sentry_init, capture_events, uninstall_integration):
6738
uninstall_integration(UnleashIntegration.identifier)
6839

6940
with mock_unleash_client():
70-
client = UnleashClient()
71-
sentry_init(integrations=[UnleashIntegration()]) # type: ignore
41+
client = UnleashClient() # type: ignore[arg-type]
42+
sentry_init(integrations=[UnleashIntegration()])
7243
events = capture_events()
7344

7445
def task(flag_key):
@@ -112,63 +83,14 @@ def task(flag_key):
11283
}
11384

11485

115-
def test_get_variant_threaded(sentry_init, capture_events, uninstall_integration):
116-
uninstall_integration(UnleashIntegration.identifier)
117-
118-
with mock_unleash_client():
119-
client = UnleashClient()
120-
sentry_init(integrations=[UnleashIntegration()]) # type: ignore
121-
events = capture_events()
122-
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!"))
131-
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"])
137-
138-
# Capture error in original scope
139-
sentry_sdk.set_tag("task_id", "0")
140-
sentry_sdk.capture_exception(Exception("something wrong!"))
141-
142-
assert len(events) == 3
143-
events.sort(key=lambda e: e["tags"]["task_id"])
144-
145-
assert events[0]["contexts"]["flags"] == {
146-
"values": [
147-
{"flag": "hello", "result": False},
148-
]
149-
}
150-
assert events[1]["contexts"]["flags"] == {
151-
"values": [
152-
{"flag": "hello", "result": False},
153-
{"flag": "no_payload_feature", "result": True},
154-
]
155-
}
156-
assert events[2]["contexts"]["flags"] == {
157-
"values": [
158-
{"flag": "hello", "result": False},
159-
{"flag": "other", "result": False},
160-
]
161-
}
162-
163-
16486
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
16587
def test_is_enabled_asyncio(sentry_init, capture_events, uninstall_integration):
16688
asyncio = pytest.importorskip("asyncio")
16789
uninstall_integration(UnleashIntegration.identifier)
16890

16991
with mock_unleash_client():
170-
client = UnleashClient()
171-
sentry_init(integrations=[UnleashIntegration()]) # type: ignore
92+
client = UnleashClient() # type: ignore[arg-type]
93+
sentry_init(integrations=[UnleashIntegration()])
17294
events = capture_events()
17395

17496
async def task(flag_key):
@@ -212,66 +134,12 @@ async def runner():
212134
}
213135

214136

215-
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
216-
def test_get_variant_asyncio(sentry_init, capture_events, uninstall_integration):
217-
asyncio = pytest.importorskip("asyncio")
218-
219-
uninstall_integration(UnleashIntegration.identifier)
220-
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!"))
232-
233-
async def runner():
234-
return asyncio.gather(task("no_payload_feature"), task("other"))
235-
236-
# Capture an eval before we split isolation scopes.
237-
client.get_variant("hello")
238-
239-
asyncio.run(runner())
240-
241-
# Capture error in original scope
242-
sentry_sdk.set_tag("task_id", "0")
243-
sentry_sdk.capture_exception(Exception("something wrong!"))
244-
245-
assert len(events) == 3
246-
events.sort(key=lambda e: e["tags"]["task_id"])
247-
248-
assert events[0]["contexts"]["flags"] == {
249-
"values": [
250-
{"flag": "hello", "result": False},
251-
]
252-
}
253-
assert events[1]["contexts"]["flags"] == {
254-
"values": [
255-
{"flag": "hello", "result": False},
256-
{"flag": "no_payload_feature", "result": True},
257-
]
258-
}
259-
assert events[2]["contexts"]["flags"] == {
260-
"values": [
261-
{"flag": "hello", "result": False},
262-
{"flag": "other", "result": False},
263-
]
264-
}
265-
266-
267137
def test_wraps_original(sentry_init, uninstall_integration):
268138
with mock_unleash_client():
269-
client = UnleashClient()
139+
client = UnleashClient() # type: ignore[arg-type]
270140

271141
mock_is_enabled = mock.Mock(return_value=random() < 0.5)
272-
mock_get_variant = mock.Mock(return_value={"enabled": random() < 0.5})
273142
client.is_enabled = mock_is_enabled
274-
client.get_variant = mock_get_variant
275143

276144
uninstall_integration(UnleashIntegration.identifier)
277145
sentry_init(integrations=[UnleashIntegration()]) # type: ignore
@@ -283,26 +151,16 @@ def test_wraps_original(sentry_init, uninstall_integration):
283151
{"kwarg": 1},
284152
)
285153

286-
res = client.get_variant("test-flag", "arg", kwarg=1)
287-
assert res == mock_get_variant.return_value
288-
assert mock_get_variant.call_args == (
289-
("test-flag", "arg"),
290-
{"kwarg": 1},
291-
)
292-
293154

294155
def test_wrapper_attributes(sentry_init, uninstall_integration):
295156
with mock_unleash_client():
296-
client = UnleashClient() # <- Returns a MockUnleashClient
157+
client = UnleashClient() # type: ignore[arg-type]
297158

298159
original_is_enabled = client.is_enabled
299-
original_get_variant = client.get_variant
300160

301161
uninstall_integration(UnleashIntegration.identifier)
302162
sentry_init(integrations=[UnleashIntegration()]) # type: ignore
303163

304164
# Mock clients methods have not lost their qualified names after decoration.
305165
assert client.is_enabled.__name__ == "is_enabled"
306166
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__

tests/integrations/unleash/testutils.py

+2-34
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ def mock_unleash_client():
88
Temporarily replaces UnleashClient's methods with mock implementations
99
for testing.
1010
11-
This context manager swaps out UnleashClient's __init__, is_enabled,
12-
and get_variant methods with mock versions from MockUnleashClient.
11+
This context manager swaps out UnleashClient's __init__ and is_enabled,
12+
methods with mock versions from MockUnleashClient.
1313
Original methods are restored when exiting the context.
1414
1515
After mocking the client class the integration can be initialized.
@@ -23,17 +23,14 @@ def mock_unleash_client():
2323
"""
2424
old_init = UnleashClient.__init__
2525
old_is_enabled = UnleashClient.is_enabled
26-
old_get_variant = UnleashClient.get_variant
2726

2827
UnleashClient.__init__ = MockUnleashClient.__init__
2928
UnleashClient.is_enabled = MockUnleashClient.is_enabled
30-
UnleashClient.get_variant = MockUnleashClient.get_variant
3129

3230
yield
3331

3432
UnleashClient.__init__ = old_init
3533
UnleashClient.is_enabled = old_is_enabled
36-
UnleashClient.get_variant = old_get_variant
3734

3835

3936
class MockUnleashClient:
@@ -44,34 +41,5 @@ def __init__(self, *a, **kw):
4441
"world": False,
4542
}
4643

47-
self.feature_to_variant = {
48-
"string_feature": {
49-
"name": "variant1",
50-
"enabled": True,
51-
"payload": {"type": "string", "value": "val1"},
52-
},
53-
"json_feature": {
54-
"name": "variant1",
55-
"enabled": True,
56-
"payload": {"type": "json", "value": '{"key1": 0.53}'},
57-
},
58-
"number_feature": {
59-
"name": "variant1",
60-
"enabled": True,
61-
"payload": {"type": "number", "value": "134.5"},
62-
},
63-
"csv_feature": {
64-
"name": "variant1",
65-
"enabled": True,
66-
"payload": {"type": "csv", "value": "abc 123\ncsbq 94"},
67-
},
68-
"no_payload_feature": {"name": "variant1", "enabled": True},
69-
}
70-
71-
self.disabled_variant = {"name": "disabled", "enabled": False}
72-
7344
def is_enabled(self, feature, *a, **kw):
7445
return self.features.get(feature, False)
75-
76-
def get_variant(self, feature, *a, **kw):
77-
return self.feature_to_variant.get(feature, self.disabled_variant)

0 commit comments

Comments
 (0)