Skip to content

Commit

Permalink
Merge branch 'master' into ivana/populate-tox
Browse files Browse the repository at this point in the history
  • Loading branch information
sentrivana authored Dec 19, 2024
2 parents 653fcde + fe4b88b commit 610de68
Show file tree
Hide file tree
Showing 7 changed files with 408 additions and 43 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/release-comment-issues.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: "Automation: Notify issues for release"
on:
release:
types:
- published
workflow_dispatch:
inputs:
version:
description: Which version to notify issues for
required: false

# This workflow is triggered when a release is published
jobs:
release-comment-issues:
runs-on: ubuntu-20.04
name: Notify issues
steps:
- name: Get version
id: get_version
run: echo "version=${{ github.event.inputs.version || github.event.release.tag_name }}" >> $GITHUB_OUTPUT

- name: Comment on linked issues that are mentioned in release
if: |
steps.get_version.outputs.version != ''
&& !contains(steps.get_version.outputs.version, 'a')
&& !contains(steps.get_version.outputs.version, 'b')
&& !contains(steps.get_version.outputs.version, 'rc')
uses: getsentry/release-comment-issues-gh-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
version: ${{ steps.get_version.outputs.version }}
44 changes: 44 additions & 0 deletions sentry_sdk/integrations/featureflags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from sentry_sdk.flag_utils import flag_error_processor

import sentry_sdk
from sentry_sdk.integrations import Integration


class FeatureFlagsIntegration(Integration):
"""
Sentry integration for capturing feature flags on error events. To manually buffer flag data,
call `integrations.featureflags.add_feature_flag`. We recommend you do this on each flag
evaluation.
See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags)
for more information.
@example
```
import sentry_sdk
from sentry_sdk.integrations.featureflags import FeatureFlagsIntegration, add_feature_flag
sentry_sdk.init(dsn="my_dsn", integrations=[FeatureFlagsIntegration()]);
add_feature_flag('my-flag', true);
sentry_sdk.capture_exception(Exception('broke')); // 'my-flag' should be captured on this Sentry event.
```
"""

identifier = "featureflags"

@staticmethod
def setup_once():
# type: () -> None
scope = sentry_sdk.get_current_scope()
scope.add_error_processor(flag_error_processor)


def add_feature_flag(flag, result):
# type: (str, bool) -> None
"""
Records a flag and its value to be sent on subsequent error events by FeatureFlagsIntegration.
We recommend you do this on flag evaluations. Flags are buffered per Sentry scope.
"""
flags = sentry_sdk.get_current_scope().flags
flags.set(flag, result)
11 changes: 11 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,17 @@ def reset_integrations():
_installed_integrations.clear()


@pytest.fixture
def uninstall_integration():
"""Use to force the next call to sentry_init to re-install/setup an integration."""

def inner(identifier):
_processed_integrations.discard(identifier)
_installed_integrations.discard(identifier)

return inner


@pytest.fixture
def sentry_init(request):
def inner(*a, **kw):
Expand Down
Empty file.
133 changes: 133 additions & 0 deletions tests/integrations/featureflags/test_featureflags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import concurrent.futures as cf
import sys

import pytest

import sentry_sdk
from sentry_sdk.integrations.featureflags import (
FeatureFlagsIntegration,
add_feature_flag,
)


def test_featureflags_integration(sentry_init, capture_events, uninstall_integration):
uninstall_integration(FeatureFlagsIntegration.identifier)
sentry_init(integrations=[FeatureFlagsIntegration()])

add_feature_flag("hello", False)
add_feature_flag("world", True)
add_feature_flag("other", False)

events = capture_events()
sentry_sdk.capture_exception(Exception("something wrong!"))

assert len(events) == 1
assert events[0]["contexts"]["flags"] == {
"values": [
{"flag": "hello", "result": False},
{"flag": "world", "result": True},
{"flag": "other", "result": False},
]
}


def test_featureflags_integration_threaded(
sentry_init, capture_events, uninstall_integration
):
uninstall_integration(FeatureFlagsIntegration.identifier)
sentry_init(integrations=[FeatureFlagsIntegration()])
events = capture_events()

# Capture an eval before we split isolation scopes.
add_feature_flag("hello", False)

def task(flag_key):
# Creates a new isolation scope for the thread.
# This means the evaluations in each task are captured separately.
with sentry_sdk.isolation_scope():
add_feature_flag(flag_key, False)
# use a tag to identify to identify events later on
sentry_sdk.set_tag("task_id", flag_key)
sentry_sdk.capture_exception(Exception("something wrong!"))

# Run tasks in separate threads
with cf.ThreadPoolExecutor(max_workers=2) as pool:
pool.map(task, ["world", "other"])

# Capture error in original scope
sentry_sdk.set_tag("task_id", "0")
sentry_sdk.capture_exception(Exception("something wrong!"))

assert len(events) == 3
events.sort(key=lambda e: e["tags"]["task_id"])

assert events[0]["contexts"]["flags"] == {
"values": [
{"flag": "hello", "result": False},
]
}
assert events[1]["contexts"]["flags"] == {
"values": [
{"flag": "hello", "result": False},
{"flag": "other", "result": False},
]
}
assert events[2]["contexts"]["flags"] == {
"values": [
{"flag": "hello", "result": False},
{"flag": "world", "result": False},
]
}


@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
def test_featureflags_integration_asyncio(
sentry_init, capture_events, uninstall_integration
):
asyncio = pytest.importorskip("asyncio")

uninstall_integration(FeatureFlagsIntegration.identifier)
sentry_init(integrations=[FeatureFlagsIntegration()])
events = capture_events()

# Capture an eval before we split isolation scopes.
add_feature_flag("hello", False)

async def task(flag_key):
# Creates a new isolation scope for the thread.
# This means the evaluations in each task are captured separately.
with sentry_sdk.isolation_scope():
add_feature_flag(flag_key, False)
# use a tag to identify to identify events later on
sentry_sdk.set_tag("task_id", flag_key)
sentry_sdk.capture_exception(Exception("something wrong!"))

async def runner():
return asyncio.gather(task("world"), task("other"))

asyncio.run(runner())

# Capture error in original scope
sentry_sdk.set_tag("task_id", "0")
sentry_sdk.capture_exception(Exception("something wrong!"))

assert len(events) == 3
events.sort(key=lambda e: e["tags"]["task_id"])

assert events[0]["contexts"]["flags"] == {
"values": [
{"flag": "hello", "result": False},
]
}
assert events[1]["contexts"]["flags"] == {
"values": [
{"flag": "hello", "result": False},
{"flag": "other", "result": False},
]
}
assert events[2]["contexts"]["flags"] == {
"values": [
{"flag": "hello", "result": False},
{"flag": "world", "result": False},
]
}
Loading

0 comments on commit 610de68

Please sign in to comment.