Skip to content

Commit c6a89d6

Browse files
aliu39antonpirkercmanallen
authored
feat(flags): add Unleash feature flagging integration (#3888)
Adds an integration for tracking flag evaluations from [Unleash](https://www.getunleash.io/) customers. Implementation Unleash has no native support for evaluation hooks/listeners, unless the user opts in for each flag. Therefore we decided to patch the `is_enabled` and `get_variant` methods on the `UnleashClient` class. The methods are wrapped and the only side effect is writing to Sentry scope, so users shouldn't see any change in behavior. We patch one `UnleashClient` instance instead of the whole class. The reasons for this are described in - #3895 It's also safer to not modify the unleash import. References - https://develop.sentry.dev/sdk/expected-features/#feature-flags - https://docs.getunleash.io/reference/sdks/python for methods we're patching/wrapping --------- Co-authored-by: Anton Pirker <[email protected]> Co-authored-by: Colton Allen <[email protected]>
1 parent bf65ede commit c6a89d6

File tree

10 files changed

+468
-4
lines changed

10 files changed

+468
-4
lines changed

Diff for: .github/workflows/test-integrations-misc.yml

+8
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ jobs:
7979
run: |
8080
set -x # print commands that are executed
8181
./scripts/runtox.sh "py${{ matrix.python-version }}-typer-latest"
82+
- name: Test unleash latest
83+
run: |
84+
set -x # print commands that are executed
85+
./scripts/runtox.sh "py${{ matrix.python-version }}-unleash-latest"
8286
- name: Generate coverage XML (Python 3.6)
8387
if: ${{ !cancelled() && matrix.python-version == '3.6' }}
8488
run: |
@@ -163,6 +167,10 @@ jobs:
163167
run: |
164168
set -x # print commands that are executed
165169
./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-typer"
170+
- name: Test unleash pinned
171+
run: |
172+
set -x # print commands that are executed
173+
./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-unleash"
166174
- name: Generate coverage XML (Python 3.6)
167175
if: ${{ !cancelled() && matrix.python-version == '3.6' }}
168176
run: |

Diff for: requirements-linting.txt

+1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ pre-commit # local linting
1717
httpcore
1818
openfeature-sdk
1919
launchdarkly-server-sdk
20+
UnleashClient
2021
typer

Diff for: scripts/split_tox_gh_actions/split_tox_gh_actions.py

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
"pure_eval",
134134
"trytond",
135135
"typer",
136+
"unleash",
136137
],
137138
}
138139

Diff for: sentry_sdk/integrations/unleash.py

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from functools import wraps
2+
from typing import Any
3+
4+
import sentry_sdk
5+
from sentry_sdk.flag_utils import flag_error_processor
6+
from sentry_sdk.integrations import Integration, DidNotEnable
7+
8+
try:
9+
from UnleashClient import UnleashClient
10+
except ImportError:
11+
raise DidNotEnable("UnleashClient is not installed")
12+
13+
14+
class UnleashIntegration(Integration):
15+
identifier = "unleash"
16+
17+
@staticmethod
18+
def setup_once():
19+
# type: () -> None
20+
# Wrap and patch evaluation methods (instance methods)
21+
old_is_enabled = UnleashClient.is_enabled
22+
old_get_variant = UnleashClient.get_variant
23+
24+
@wraps(old_is_enabled)
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)
28+
29+
# We have no way of knowing what type of unleash feature this is, so we have to treat
30+
# it as a boolean / toggle feature.
31+
flags = sentry_sdk.get_current_scope().flags
32+
flags.set(feature, enabled)
33+
34+
return enabled
35+
36+
@wraps(old_get_variant)
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)
40+
enabled = variant.get("enabled", False)
41+
42+
# Payloads are not always used as the feature's value for application logic. They
43+
# may be used for metrics or debugging context instead. Therefore, we treat every
44+
# variant as a boolean toggle, using the `enabled` field.
45+
flags = sentry_sdk.get_current_scope().flags
46+
flags.set(feature, enabled)
47+
48+
return variant
49+
50+
UnleashClient.is_enabled = sentry_is_enabled # type: ignore
51+
UnleashClient.get_variant = sentry_get_variant # type: ignore
52+
53+
# Error processor
54+
scope = sentry_sdk.get_current_scope()
55+
scope.add_error_processor(flag_error_processor)

Diff for: setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def get_file_text(file_name):
8080
"starlette": ["starlette>=0.19.1"],
8181
"starlite": ["starlite>=1.48"],
8282
"tornado": ["tornado>=6"],
83+
"unleash": ["UnleashClient>=6.0.1"],
8384
},
8485
entry_points={
8586
"opentelemetry_propagator": [

Diff for: tests/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import pytest
1111
import jsonschema
1212

13+
1314
try:
1415
import gevent
1516
except ImportError:

Diff for: tests/integrations/unleash/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import pytest
2+
3+
pytest.importorskip("UnleashClient")

0 commit comments

Comments
 (0)