Skip to content

Commit 610de68

Browse files
authored
Merge branch 'master' into ivana/populate-tox
2 parents 653fcde + fe4b88b commit 610de68

File tree

7 files changed

+408
-43
lines changed

7 files changed

+408
-43
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: "Automation: Notify issues for release"
2+
on:
3+
release:
4+
types:
5+
- published
6+
workflow_dispatch:
7+
inputs:
8+
version:
9+
description: Which version to notify issues for
10+
required: false
11+
12+
# This workflow is triggered when a release is published
13+
jobs:
14+
release-comment-issues:
15+
runs-on: ubuntu-20.04
16+
name: Notify issues
17+
steps:
18+
- name: Get version
19+
id: get_version
20+
run: echo "version=${{ github.event.inputs.version || github.event.release.tag_name }}" >> $GITHUB_OUTPUT
21+
22+
- name: Comment on linked issues that are mentioned in release
23+
if: |
24+
steps.get_version.outputs.version != ''
25+
&& !contains(steps.get_version.outputs.version, 'a')
26+
&& !contains(steps.get_version.outputs.version, 'b')
27+
&& !contains(steps.get_version.outputs.version, 'rc')
28+
uses: getsentry/release-comment-issues-gh-action@v1
29+
with:
30+
github_token: ${{ secrets.GITHUB_TOKEN }}
31+
version: ${{ steps.get_version.outputs.version }}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from sentry_sdk.flag_utils import flag_error_processor
2+
3+
import sentry_sdk
4+
from sentry_sdk.integrations import Integration
5+
6+
7+
class FeatureFlagsIntegration(Integration):
8+
"""
9+
Sentry integration for capturing feature flags on error events. To manually buffer flag data,
10+
call `integrations.featureflags.add_feature_flag`. We recommend you do this on each flag
11+
evaluation.
12+
13+
See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags)
14+
for more information.
15+
16+
@example
17+
```
18+
import sentry_sdk
19+
from sentry_sdk.integrations.featureflags import FeatureFlagsIntegration, add_feature_flag
20+
21+
sentry_sdk.init(dsn="my_dsn", integrations=[FeatureFlagsIntegration()]);
22+
23+
add_feature_flag('my-flag', true);
24+
sentry_sdk.capture_exception(Exception('broke')); // 'my-flag' should be captured on this Sentry event.
25+
```
26+
"""
27+
28+
identifier = "featureflags"
29+
30+
@staticmethod
31+
def setup_once():
32+
# type: () -> None
33+
scope = sentry_sdk.get_current_scope()
34+
scope.add_error_processor(flag_error_processor)
35+
36+
37+
def add_feature_flag(flag, result):
38+
# type: (str, bool) -> None
39+
"""
40+
Records a flag and its value to be sent on subsequent error events by FeatureFlagsIntegration.
41+
We recommend you do this on flag evaluations. Flags are buffered per Sentry scope.
42+
"""
43+
flags = sentry_sdk.get_current_scope().flags
44+
flags.set(flag, result)

tests/conftest.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,17 @@ def reset_integrations():
184184
_installed_integrations.clear()
185185

186186

187+
@pytest.fixture
188+
def uninstall_integration():
189+
"""Use to force the next call to sentry_init to re-install/setup an integration."""
190+
191+
def inner(identifier):
192+
_processed_integrations.discard(identifier)
193+
_installed_integrations.discard(identifier)
194+
195+
return inner
196+
197+
187198
@pytest.fixture
188199
def sentry_init(request):
189200
def inner(*a, **kw):

tests/integrations/featureflags/__init__.py

Whitespace-only changes.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import concurrent.futures as cf
2+
import sys
3+
4+
import pytest
5+
6+
import sentry_sdk
7+
from sentry_sdk.integrations.featureflags import (
8+
FeatureFlagsIntegration,
9+
add_feature_flag,
10+
)
11+
12+
13+
def test_featureflags_integration(sentry_init, capture_events, uninstall_integration):
14+
uninstall_integration(FeatureFlagsIntegration.identifier)
15+
sentry_init(integrations=[FeatureFlagsIntegration()])
16+
17+
add_feature_flag("hello", False)
18+
add_feature_flag("world", True)
19+
add_feature_flag("other", False)
20+
21+
events = capture_events()
22+
sentry_sdk.capture_exception(Exception("something wrong!"))
23+
24+
assert len(events) == 1
25+
assert events[0]["contexts"]["flags"] == {
26+
"values": [
27+
{"flag": "hello", "result": False},
28+
{"flag": "world", "result": True},
29+
{"flag": "other", "result": False},
30+
]
31+
}
32+
33+
34+
def test_featureflags_integration_threaded(
35+
sentry_init, capture_events, uninstall_integration
36+
):
37+
uninstall_integration(FeatureFlagsIntegration.identifier)
38+
sentry_init(integrations=[FeatureFlagsIntegration()])
39+
events = capture_events()
40+
41+
# Capture an eval before we split isolation scopes.
42+
add_feature_flag("hello", False)
43+
44+
def task(flag_key):
45+
# Creates a new isolation scope for the thread.
46+
# This means the evaluations in each task are captured separately.
47+
with sentry_sdk.isolation_scope():
48+
add_feature_flag(flag_key, False)
49+
# use a tag to identify to identify events later on
50+
sentry_sdk.set_tag("task_id", flag_key)
51+
sentry_sdk.capture_exception(Exception("something wrong!"))
52+
53+
# Run tasks in separate threads
54+
with cf.ThreadPoolExecutor(max_workers=2) as pool:
55+
pool.map(task, ["world", "other"])
56+
57+
# Capture error in original scope
58+
sentry_sdk.set_tag("task_id", "0")
59+
sentry_sdk.capture_exception(Exception("something wrong!"))
60+
61+
assert len(events) == 3
62+
events.sort(key=lambda e: e["tags"]["task_id"])
63+
64+
assert events[0]["contexts"]["flags"] == {
65+
"values": [
66+
{"flag": "hello", "result": False},
67+
]
68+
}
69+
assert events[1]["contexts"]["flags"] == {
70+
"values": [
71+
{"flag": "hello", "result": False},
72+
{"flag": "other", "result": False},
73+
]
74+
}
75+
assert events[2]["contexts"]["flags"] == {
76+
"values": [
77+
{"flag": "hello", "result": False},
78+
{"flag": "world", "result": False},
79+
]
80+
}
81+
82+
83+
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
84+
def test_featureflags_integration_asyncio(
85+
sentry_init, capture_events, uninstall_integration
86+
):
87+
asyncio = pytest.importorskip("asyncio")
88+
89+
uninstall_integration(FeatureFlagsIntegration.identifier)
90+
sentry_init(integrations=[FeatureFlagsIntegration()])
91+
events = capture_events()
92+
93+
# Capture an eval before we split isolation scopes.
94+
add_feature_flag("hello", False)
95+
96+
async def task(flag_key):
97+
# Creates a new isolation scope for the thread.
98+
# This means the evaluations in each task are captured separately.
99+
with sentry_sdk.isolation_scope():
100+
add_feature_flag(flag_key, False)
101+
# use a tag to identify to identify events later on
102+
sentry_sdk.set_tag("task_id", flag_key)
103+
sentry_sdk.capture_exception(Exception("something wrong!"))
104+
105+
async def runner():
106+
return asyncio.gather(task("world"), task("other"))
107+
108+
asyncio.run(runner())
109+
110+
# Capture error in original scope
111+
sentry_sdk.set_tag("task_id", "0")
112+
sentry_sdk.capture_exception(Exception("something wrong!"))
113+
114+
assert len(events) == 3
115+
events.sort(key=lambda e: e["tags"]["task_id"])
116+
117+
assert events[0]["contexts"]["flags"] == {
118+
"values": [
119+
{"flag": "hello", "result": False},
120+
]
121+
}
122+
assert events[1]["contexts"]["flags"] == {
123+
"values": [
124+
{"flag": "hello", "result": False},
125+
{"flag": "other", "result": False},
126+
]
127+
}
128+
assert events[2]["contexts"]["flags"] == {
129+
"values": [
130+
{"flag": "hello", "result": False},
131+
{"flag": "world", "result": False},
132+
]
133+
}

0 commit comments

Comments
 (0)