Skip to content

Commit 563063f

Browse files
authored
Release v4.6.0
2 parents 955d088 + 8b023d9 commit 563063f

26 files changed

+1275
-83
lines changed

.github/workflows/python-package.yml

+11-7
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@ on: [ push, pull_request ]
44

55
jobs:
66
test:
7-
# TODO: a GH action update broke the 'ubuntu-latest' image
8-
# when it's fixed, we should switch back
9-
runs-on: ubuntu-20.04
7+
runs-on: ${{ matrix.os }}
108

119
strategy:
1210
fail-fast: false
1311
matrix:
14-
python-version: ['3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11']
12+
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
13+
os: ['ubuntu-latest']
14+
include:
15+
- python-version: '3.5'
16+
os: 'ubuntu-20.04'
17+
- python-version: '3.6'
18+
os: 'ubuntu-20.04'
1519

1620
steps:
17-
- uses: actions/checkout@v2
21+
- uses: actions/checkout@v3
1822

1923
- name: Set up Python ${{ matrix.python-version }}
20-
uses: actions/setup-python@v2
24+
uses: actions/setup-python@v4
2125
with:
2226
python-version: ${{ matrix.python-version }}
2327

@@ -29,7 +33,7 @@ jobs:
2933
run: |
3034
pyversion=${{ matrix.python-version }}
3135
TOXFACTOR=${pyversion//.0-*/}
32-
tox -f py${TOXFACTOR//./} --parallel --quiet
36+
tox -f py${TOXFACTOR//./}
3337
3438
- name: Upload code coverage data
3539
env:

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
Changelog
22
=========
33

4+
## v4.6.0 (2023-09-05)
5+
6+
### Enhancements
7+
8+
* Add support for feature flags & experiments
9+
[#350](https://github.com/bugsnag/bugsnag-python/pull/350)
10+
[#351](https://github.com/bugsnag/bugsnag-python/pull/351)
11+
12+
* Remove use of deprecated `pkg_resources` module
13+
[#362](https://github.com/bugsnag/bugsnag-python/pull/362)
14+
415
## v4.5.0 (2023-07-17)
516

617
### Enhancements

CONTRIBUTING.md

+11-17
Original file line numberDiff line numberDiff line change
@@ -90,31 +90,25 @@ If you're on the core team, you can release Bugsnag as follows:
9090
9191
## Making a release
9292
93-
* Update the version number in setup.py
94-
* Update the CHANGELOG.md, and README.md if necessary
95-
* Commit
93+
* Create branch for the release
9694
9795
```
98-
git commit -am v4.x.x
96+
git checkout -b release/v4.x.x
9997
```
10098
101-
* Tag the release in git
102-
103-
```
104-
git tag v4.x.x
105-
```
106-
107-
* Push to git
99+
* Update the version number in [`setup.py`](./setup.py) and `bugsnag/notifier.py`(./bugsnag/notifier.py)
100+
* Update the CHANGELOG.md and README.md if necessary
101+
* Commit and open a pull request into `master`
102+
* Merge the PR when it's been reviewed
103+
* Create a release on GitHub, tagging the new version `v4.x.x`
104+
* Push the release to PyPI
108105
109106
```
110-
git push origin master && git push --tags
107+
git fetch --tags && git checkout tags/v4.x.x
108+
python setup.py sdist bdist_wheel
109+
twine upload dist/*
111110
```
112111
113-
* Push the release to PyPI
114-
115-
python setup.py sdist bdist_wheel
116-
twine upload dist/*
117-
118112
## Update docs.bugsnag.com
119113
120114
Update the setup guides for Python (and its frameworks) with any new content.

bugsnag/__init__.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88
Breadcrumbs,
99
OnBreadcrumbCallback
1010
)
11+
from bugsnag.feature_flags import FeatureFlag
1112
from bugsnag.legacy import (configuration, configure, configure_request,
1213
add_metadata_tab, clear_request_config, notify,
1314
auto_notify, before_notify, start_session,
1415
auto_notify_exc_info, logger, leave_breadcrumb,
15-
add_on_breadcrumb, remove_on_breadcrumb)
16+
add_on_breadcrumb, remove_on_breadcrumb,
17+
add_feature_flag, add_feature_flags,
18+
clear_feature_flag, clear_feature_flags)
1619

1720
__all__ = ('Client', 'Event', 'Configuration', 'RequestConfiguration',
1821
'configuration', 'configure', 'configure_request',
@@ -21,4 +24,5 @@
2124
'auto_notify_exc_info', 'Notification', 'logger',
2225
'BreadcrumbType', 'Breadcrumb', 'Breadcrumbs',
2326
'OnBreadcrumbCallback', 'leave_breadcrumb', 'add_on_breadcrumb',
24-
'remove_on_breadcrumb')
27+
'remove_on_breadcrumb', 'FeatureFlag', 'add_feature_flag',
28+
'add_feature_flags', 'clear_feature_flag', 'clear_feature_flags')

bugsnag/client.py

+37-4
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
)
1515
from bugsnag.configuration import Configuration, RequestConfiguration
1616
from bugsnag.event import Event
17+
from bugsnag.feature_flags import FeatureFlag
1718
from bugsnag.handlers import BugsnagHandler
1819
from bugsnag.sessiontracker import SessionTracker
1920
from bugsnag.utils import to_rfc3339
21+
from bugsnag.context import ContextLocalState
2022

2123
__all__ = ('Client',)
2224

@@ -33,6 +35,7 @@ def __init__(self, configuration: Optional[Configuration] = None,
3335
self.configuration = configuration or Configuration() # type: Configuration # noqa: E501
3436
self.session_tracker = SessionTracker(self.configuration)
3537
self.configuration.configure(**kwargs)
38+
self._context = ContextLocalState(self)
3639

3740
if install_sys_hook:
3841
self.install_sys_hook()
@@ -78,8 +81,13 @@ def notify(self, exception: BaseException, asynchronous=None, **options):
7881
>>> client.notify(Exception('Example')) # doctest: +SKIP
7982
"""
8083

81-
event = Event(exception, self.configuration,
82-
RequestConfiguration.get_instance(), **options)
84+
event = Event(
85+
exception,
86+
self.configuration,
87+
RequestConfiguration.get_instance(),
88+
**options,
89+
feature_flag_delegate=self._context.feature_flag_delegate
90+
)
8391

8492
self._leave_breadcrumb_for_event(event)
8593
self.deliver(event, asynchronous=asynchronous)
@@ -94,8 +102,13 @@ def notify_exc_info(self, exc_type, exc_value, traceback,
94102

95103
exception = exc_value
96104
options['traceback'] = traceback
97-
event = Event(exception, self.configuration,
98-
RequestConfiguration.get_instance(), **options)
105+
event = Event(
106+
exception,
107+
self.configuration,
108+
RequestConfiguration.get_instance(),
109+
**options,
110+
feature_flag_delegate=self._context.feature_flag_delegate
111+
)
99112

100113
self._leave_breadcrumb_for_event(event)
101114
self.deliver(event, asynchronous=asynchronous)
@@ -213,6 +226,26 @@ def log_handler(
213226
) -> BugsnagHandler:
214227
return BugsnagHandler(client=self, extra_fields=extra_fields)
215228

229+
@property
230+
def feature_flags(self) -> List[FeatureFlag]:
231+
return self._context.feature_flag_delegate.to_list()
232+
233+
def add_feature_flag(
234+
self,
235+
name: Union[str, bytes],
236+
variant: Union[None, str, bytes] = None
237+
) -> None:
238+
self._context.feature_flag_delegate.add(name, variant)
239+
240+
def add_feature_flags(self, feature_flags: List[FeatureFlag]) -> None:
241+
self._context.feature_flag_delegate.merge(feature_flags)
242+
243+
def clear_feature_flag(self, name: Union[str, bytes]) -> None:
244+
self._context.feature_flag_delegate.remove(name)
245+
246+
def clear_feature_flags(self) -> None:
247+
self._context.feature_flag_delegate.clear()
248+
216249
@property
217250
def breadcrumbs(self) -> List[Breadcrumb]:
218251
return self.configuration.breadcrumbs

bugsnag/context.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from weakref import WeakKeyDictionary
2+
from bugsnag.feature_flags import FeatureFlagDelegate
3+
4+
5+
try:
6+
from contextvars import ContextVar # type: ignore
7+
except ImportError:
8+
from bugsnag.utils import ThreadContextVar as ContextVar # type: ignore # noqa: E501
9+
10+
11+
# a top-level context var storing a WeakKeyDictionary of client => state
12+
# the WeakKeyDictionary ensures that when a client object is garbage collected
13+
# its state is discarded as well
14+
_client_contexts = ContextVar('bugsnag-client-context', default=None)
15+
16+
17+
def _raw_get(client, key):
18+
client_context = _client_contexts.get()
19+
20+
if (
21+
client_context is not None and
22+
client in client_context and
23+
key in client_context[client]
24+
):
25+
return client_context[client][key]
26+
27+
return None
28+
29+
30+
def _raw_set(client, key, value):
31+
client_context = _client_contexts.get()
32+
33+
if client_context is None:
34+
client_context = WeakKeyDictionary()
35+
_client_contexts.set(client_context)
36+
37+
if client not in client_context:
38+
client_context[client] = {}
39+
40+
client_context[client][key] = value
41+
42+
43+
def create_new_context():
44+
_client_contexts.set(None)
45+
46+
47+
FEATURE_FLAG_DELEGATE_KEY = 'feature_flag_delegate'
48+
49+
50+
class ContextLocalState:
51+
def __init__(self, client):
52+
self._client = client
53+
54+
@property
55+
def feature_flag_delegate(self) -> FeatureFlagDelegate:
56+
delegate = _raw_get(self._client, FEATURE_FLAG_DELEGATE_KEY)
57+
58+
# create a new delegate if one does not exist already
59+
if delegate is None:
60+
delegate = FeatureFlagDelegate()
61+
_raw_set(self._client, FEATURE_FLAG_DELEGATE_KEY, delegate)
62+
63+
return delegate

bugsnag/django/middleware.py

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import bugsnag
99
import bugsnag.django
10+
from bugsnag.context import create_new_context
1011
from bugsnag.legacy import _auto_leave_breadcrumb
1112
from bugsnag.breadcrumbs import BreadcrumbType
1213
from bugsnag.utils import remove_query_from_url
@@ -20,6 +21,7 @@ def __init__(self, get_response=None):
2021
# pylint: disable-msg=R0201
2122
def process_request(self, request):
2223
bugsnag.configure_request(django_request=request)
24+
create_new_context()
2325

2426
_auto_leave_breadcrumb(
2527
'http request',

bugsnag/event.py

+37-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, Optional, List # noqa
1+
from typing import Any, Dict, Optional, List, Union # noqa
22
import linecache
33
import logging
44
import os
@@ -11,9 +11,14 @@
1111
import bugsnag
1212

1313
from bugsnag.breadcrumbs import Breadcrumb
14-
from bugsnag.utils import fully_qualified_class_name as class_name
15-
from bugsnag.utils import FilterDict, package_version, SanitizingJSONEncoder
14+
from bugsnag.notifier import _NOTIFIER_INFORMATION
15+
from bugsnag.utils import (
16+
fully_qualified_class_name as class_name,
17+
FilterDict,
18+
SanitizingJSONEncoder
19+
)
1620
from bugsnag.error import Error
21+
from bugsnag.feature_flags import FeatureFlag, FeatureFlagDelegate
1722

1823
__all__ = ('Event',)
1924

@@ -31,8 +36,8 @@ class Event:
3136
"""
3237
An occurrence of an exception for delivery to Bugsnag
3338
"""
34-
NOTIFIER_NAME = "Python Bugsnag Notifier"
35-
NOTIFIER_URL = "https://github.com/bugsnag/bugsnag-python"
39+
NOTIFIER_NAME = _NOTIFIER_INFORMATION['name']
40+
NOTIFIER_URL = _NOTIFIER_INFORMATION['url']
3641
PAYLOAD_VERSION = "4.0"
3742
SUPPORTED_SEVERITIES = ["info", "warning", "error"]
3843

@@ -65,6 +70,10 @@ def __init__(self, exception: BaseException, config, request_config,
6570
self._breadcrumbs = [
6671
deepcopy(breadcrumb) for breadcrumb in config.breadcrumbs
6772
]
73+
self._feature_flag_delegate = options.pop(
74+
'feature_flag_delegate',
75+
FeatureFlagDelegate()
76+
).copy()
6877

6978
def get_config(key):
7079
return options.pop(key, getattr(self.config, key))
@@ -237,6 +246,26 @@ def add_tab(self, name, dictionary):
237246

238247
self.metadata[name].update(dictionary)
239248

249+
@property
250+
def feature_flags(self) -> List[FeatureFlag]:
251+
return self._feature_flag_delegate.to_list()
252+
253+
def add_feature_flag(
254+
self,
255+
name: Union[str, bytes],
256+
variant: Union[None, str, bytes] = None
257+
) -> None:
258+
self._feature_flag_delegate.add(name, variant)
259+
260+
def add_feature_flags(self, feature_flags: List[FeatureFlag]) -> None:
261+
self._feature_flag_delegate.merge(feature_flags)
262+
263+
def clear_feature_flag(self, name: Union[str, bytes]) -> None:
264+
self._feature_flag_delegate.remove(name)
265+
266+
def clear_feature_flags(self) -> None:
267+
self._feature_flag_delegate.clear()
268+
240269
def _generate_error_list(
241270
self,
242271
exception: BaseException,
@@ -397,7 +426,6 @@ def _code_for(self, file_name, line, window_size=7):
397426

398427
def _payload(self):
399428
# Fetch the notifier version from the package
400-
notifier_version = package_version("bugsnag") or "unknown"
401429
encoder = SanitizingJSONEncoder(
402430
self.config.logger,
403431
separators=(',', ':'),
@@ -407,11 +435,7 @@ def _payload(self):
407435
# Construct the payload dictionary
408436
return encoder.encode({
409437
"apiKey": self.api_key,
410-
"notifier": {
411-
"name": self.NOTIFIER_NAME,
412-
"url": self.NOTIFIER_URL,
413-
"version": notifier_version,
414-
},
438+
"notifier": _NOTIFIER_INFORMATION,
415439
"events": [{
416440
"severity": self.severity,
417441
"severityReason": self.severity_reason,
@@ -437,6 +461,7 @@ def _payload(self):
437461
"session": self.session,
438462
"breadcrumbs": [
439463
breadcrumb.to_dict() for breadcrumb in self._breadcrumbs
440-
]
464+
],
465+
"featureFlags": self._feature_flag_delegate.to_json()
441466
}]
442467
})

0 commit comments

Comments
 (0)