Skip to content

Commit a1e7da7

Browse files
committed
Merge branch 'master' into ivana/populate-tox-step-by-step
2 parents b44f195 + 57e9e51 commit a1e7da7

File tree

7 files changed

+49
-202
lines changed

7 files changed

+49
-202
lines changed

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
# Changelog
22

3+
## 2.20.0
4+
5+
- **New integration:** Add [Typer](https://typer.tiangolo.com/) integration (#3869) by @patrick91
6+
7+
For more information, see the documentation for the [TyperIntegration](https://docs.sentry.io/platforms/python/integrations/typer/).
8+
9+
- **New integration:** Add [Unleash](https://www.getunleash.io/) feature flagging integration (#3888) by @aliu39
10+
11+
For more information, see the documentation for the [UnleashIntegration](https://docs.sentry.io/platforms/python/integrations/unleash/).
12+
13+
- Add custom tracking of feature flag evaluations (#3860) by @aliu39
14+
- Feature Flags: Register LD hook in setup instead of init, and don't check for initialization (#3890) by @aliu39
15+
- Feature Flags: Moved adding of `flags` context into Scope (#3917) by @antonpirker
16+
- Create a separate group for feature flag test suites (#3911) by @sentrivana
17+
- Fix flaky LaunchDarkly tests (#3896) by @aliu39
18+
- Fix LRU cache copying (#3883) by @ffelixg
19+
- Fix cache pollution from mutable reference (#3887) by @cmanallen
20+
- Centralize minimum version checking (#3910) by @sentrivana
21+
- Support SparkIntegration activation after SparkContext created (#3411) by @seyoon-lim
22+
- Preserve ARQ enqueue_job __kwdefaults__ after patching (#3903) by @danmr
23+
- Add Github workflow to comment on issues when a fix was released (#3866) by @antonpirker
24+
- Update test matrix for Sanic (#3904) by @antonpirker
25+
- Rename scripts (#3885) by @sentrivana
26+
- Fix CI (#3878) by @sentrivana
27+
- Treat `potel-base` as release branch in CI (#3912) by @sentrivana
28+
- build(deps): bump actions/create-github-app-token from 1.11.0 to 1.11.1 (#3893) by @dependabot
29+
- build(deps): bump codecov/codecov-action from 5.0.7 to 5.1.1 (#3867) by @dependabot
30+
- build(deps): bump codecov/codecov-action from 5.1.1 to 5.1.2 (#3892) by @dependabot
31+
332
## 2.19.2
433

534
### Various fixes & improvements
@@ -55,6 +84,14 @@
5584

5685
### Various fixes & improvements
5786

87+
- **New integration:** Add [LaunchDarkly](https://launchdarkly.com/) integration (#3648) by @cmanallen
88+
89+
For more information, see the documentation for the [LaunchDarklyIntegration](https://docs.sentry.io/platforms/python/integrations/launchdarkly/).
90+
91+
- **New integration:** Add [OpenFeature](https://openfeature.dev/) feature flagging integration (#3648) by @cmanallen
92+
93+
For more information, see the documentation for the [OpenFeatureIntegration](https://docs.sentry.io/platforms/python/integrations/opoenfeature/).
94+
5895
- Add LaunchDarkly and OpenFeature integration (#3648) by @cmanallen
5996
- Correct typo in a comment (#3726) by @szokeasaurusrex
6097
- End `http.client` span on timeout (#3723) by @Zylphrex

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year)
3232
author = "Sentry Team and Contributors"
3333

34-
release = "2.19.2"
34+
release = "2.20.0"
3535
version = ".".join(release.split(".")[:2]) # The short X.Y version.
3636

3737

sentry_sdk/consts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,4 +581,4 @@ def _get_default_options():
581581
del _get_default_options
582582

583583

584-
VERSION = "2.19.2"
584+
VERSION = "2.20.0"

sentry_sdk/integrations/unleash.py

Lines changed: 0 additions & 16 deletions
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

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def get_file_text(file_name):
2121

2222
setup(
2323
name="sentry-sdk",
24-
version="2.19.2",
24+
version="2.20.0",
2525
author="Sentry Team and Contributors",
2626
author_email="[email protected]",
2727
url="https://github.com/getsentry/sentry-python",

tests/integrations/unleash/test_unleash.py

Lines changed: 7 additions & 149 deletions
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

Lines changed: 2 additions & 34 deletions
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)