Skip to content

Commit

Permalink
Release v4.6.0
Browse files Browse the repository at this point in the history
  • Loading branch information
imjoehaines authored Sep 5, 2023
2 parents 955d088 + 8b023d9 commit 563063f
Show file tree
Hide file tree
Showing 26 changed files with 1,275 additions and 83 deletions.
18 changes: 11 additions & 7 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@ on: [ push, pull_request ]

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

strategy:
fail-fast: false
matrix:
python-version: ['3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11']
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
os: ['ubuntu-latest']
include:
- python-version: '3.5'
os: 'ubuntu-20.04'
- python-version: '3.6'
os: 'ubuntu-20.04'

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

Expand All @@ -29,7 +33,7 @@ jobs:
run: |
pyversion=${{ matrix.python-version }}
TOXFACTOR=${pyversion//.0-*/}
tox -f py${TOXFACTOR//./} --parallel --quiet
tox -f py${TOXFACTOR//./}
- name: Upload code coverage data
env:
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Changelog
=========

## v4.6.0 (2023-09-05)

### Enhancements

* Add support for feature flags & experiments
[#350](https://github.com/bugsnag/bugsnag-python/pull/350)
[#351](https://github.com/bugsnag/bugsnag-python/pull/351)

* Remove use of deprecated `pkg_resources` module
[#362](https://github.com/bugsnag/bugsnag-python/pull/362)

## v4.5.0 (2023-07-17)

### Enhancements
Expand Down
28 changes: 11 additions & 17 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,31 +90,25 @@ If you're on the core team, you can release Bugsnag as follows:
## Making a release
* Update the version number in setup.py
* Update the CHANGELOG.md, and README.md if necessary
* Commit
* Create branch for the release
```
git commit -am v4.x.x
git checkout -b release/v4.x.x
```
* Tag the release in git
```
git tag v4.x.x
```
* Push to git
* Update the version number in [`setup.py`](./setup.py) and `bugsnag/notifier.py`(./bugsnag/notifier.py)
* Update the CHANGELOG.md and README.md if necessary
* Commit and open a pull request into `master`
* Merge the PR when it's been reviewed
* Create a release on GitHub, tagging the new version `v4.x.x`
* Push the release to PyPI
```
git push origin master && git push --tags
git fetch --tags && git checkout tags/v4.x.x
python setup.py sdist bdist_wheel
twine upload dist/*
```
* Push the release to PyPI
python setup.py sdist bdist_wheel
twine upload dist/*
## Update docs.bugsnag.com
Update the setup guides for Python (and its frameworks) with any new content.
Expand Down
8 changes: 6 additions & 2 deletions bugsnag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
Breadcrumbs,
OnBreadcrumbCallback
)
from bugsnag.feature_flags import FeatureFlag
from bugsnag.legacy import (configuration, configure, configure_request,
add_metadata_tab, clear_request_config, notify,
auto_notify, before_notify, start_session,
auto_notify_exc_info, logger, leave_breadcrumb,
add_on_breadcrumb, remove_on_breadcrumb)
add_on_breadcrumb, remove_on_breadcrumb,
add_feature_flag, add_feature_flags,
clear_feature_flag, clear_feature_flags)

__all__ = ('Client', 'Event', 'Configuration', 'RequestConfiguration',
'configuration', 'configure', 'configure_request',
Expand All @@ -21,4 +24,5 @@
'auto_notify_exc_info', 'Notification', 'logger',
'BreadcrumbType', 'Breadcrumb', 'Breadcrumbs',
'OnBreadcrumbCallback', 'leave_breadcrumb', 'add_on_breadcrumb',
'remove_on_breadcrumb')
'remove_on_breadcrumb', 'FeatureFlag', 'add_feature_flag',
'add_feature_flags', 'clear_feature_flag', 'clear_feature_flags')
41 changes: 37 additions & 4 deletions bugsnag/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
)
from bugsnag.configuration import Configuration, RequestConfiguration
from bugsnag.event import Event
from bugsnag.feature_flags import FeatureFlag
from bugsnag.handlers import BugsnagHandler
from bugsnag.sessiontracker import SessionTracker
from bugsnag.utils import to_rfc3339
from bugsnag.context import ContextLocalState

__all__ = ('Client',)

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

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

event = Event(exception, self.configuration,
RequestConfiguration.get_instance(), **options)
event = Event(
exception,
self.configuration,
RequestConfiguration.get_instance(),
**options,
feature_flag_delegate=self._context.feature_flag_delegate
)

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

exception = exc_value
options['traceback'] = traceback
event = Event(exception, self.configuration,
RequestConfiguration.get_instance(), **options)
event = Event(
exception,
self.configuration,
RequestConfiguration.get_instance(),
**options,
feature_flag_delegate=self._context.feature_flag_delegate
)

self._leave_breadcrumb_for_event(event)
self.deliver(event, asynchronous=asynchronous)
Expand Down Expand Up @@ -213,6 +226,26 @@ def log_handler(
) -> BugsnagHandler:
return BugsnagHandler(client=self, extra_fields=extra_fields)

@property
def feature_flags(self) -> List[FeatureFlag]:
return self._context.feature_flag_delegate.to_list()

def add_feature_flag(
self,
name: Union[str, bytes],
variant: Union[None, str, bytes] = None
) -> None:
self._context.feature_flag_delegate.add(name, variant)

def add_feature_flags(self, feature_flags: List[FeatureFlag]) -> None:
self._context.feature_flag_delegate.merge(feature_flags)

def clear_feature_flag(self, name: Union[str, bytes]) -> None:
self._context.feature_flag_delegate.remove(name)

def clear_feature_flags(self) -> None:
self._context.feature_flag_delegate.clear()

@property
def breadcrumbs(self) -> List[Breadcrumb]:
return self.configuration.breadcrumbs
Expand Down
63 changes: 63 additions & 0 deletions bugsnag/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from weakref import WeakKeyDictionary
from bugsnag.feature_flags import FeatureFlagDelegate


try:
from contextvars import ContextVar # type: ignore
except ImportError:
from bugsnag.utils import ThreadContextVar as ContextVar # type: ignore # noqa: E501


# a top-level context var storing a WeakKeyDictionary of client => state
# the WeakKeyDictionary ensures that when a client object is garbage collected
# its state is discarded as well
_client_contexts = ContextVar('bugsnag-client-context', default=None)


def _raw_get(client, key):
client_context = _client_contexts.get()

if (
client_context is not None and
client in client_context and
key in client_context[client]
):
return client_context[client][key]

return None


def _raw_set(client, key, value):
client_context = _client_contexts.get()

if client_context is None:
client_context = WeakKeyDictionary()
_client_contexts.set(client_context)

if client not in client_context:
client_context[client] = {}

client_context[client][key] = value


def create_new_context():
_client_contexts.set(None)


FEATURE_FLAG_DELEGATE_KEY = 'feature_flag_delegate'


class ContextLocalState:
def __init__(self, client):
self._client = client

@property
def feature_flag_delegate(self) -> FeatureFlagDelegate:
delegate = _raw_get(self._client, FEATURE_FLAG_DELEGATE_KEY)

# create a new delegate if one does not exist already
if delegate is None:
delegate = FeatureFlagDelegate()
_raw_set(self._client, FEATURE_FLAG_DELEGATE_KEY, delegate)

return delegate
2 changes: 2 additions & 0 deletions bugsnag/django/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import bugsnag
import bugsnag.django
from bugsnag.context import create_new_context
from bugsnag.legacy import _auto_leave_breadcrumb
from bugsnag.breadcrumbs import BreadcrumbType
from bugsnag.utils import remove_query_from_url
Expand All @@ -20,6 +21,7 @@ def __init__(self, get_response=None):
# pylint: disable-msg=R0201
def process_request(self, request):
bugsnag.configure_request(django_request=request)
create_new_context()

_auto_leave_breadcrumb(
'http request',
Expand Down
49 changes: 37 additions & 12 deletions bugsnag/event.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict, Optional, List # noqa
from typing import Any, Dict, Optional, List, Union # noqa
import linecache
import logging
import os
Expand All @@ -11,9 +11,14 @@
import bugsnag

from bugsnag.breadcrumbs import Breadcrumb
from bugsnag.utils import fully_qualified_class_name as class_name
from bugsnag.utils import FilterDict, package_version, SanitizingJSONEncoder
from bugsnag.notifier import _NOTIFIER_INFORMATION
from bugsnag.utils import (
fully_qualified_class_name as class_name,
FilterDict,
SanitizingJSONEncoder
)
from bugsnag.error import Error
from bugsnag.feature_flags import FeatureFlag, FeatureFlagDelegate

__all__ = ('Event',)

Expand All @@ -31,8 +36,8 @@ class Event:
"""
An occurrence of an exception for delivery to Bugsnag
"""
NOTIFIER_NAME = "Python Bugsnag Notifier"
NOTIFIER_URL = "https://github.com/bugsnag/bugsnag-python"
NOTIFIER_NAME = _NOTIFIER_INFORMATION['name']
NOTIFIER_URL = _NOTIFIER_INFORMATION['url']
PAYLOAD_VERSION = "4.0"
SUPPORTED_SEVERITIES = ["info", "warning", "error"]

Expand Down Expand Up @@ -65,6 +70,10 @@ def __init__(self, exception: BaseException, config, request_config,
self._breadcrumbs = [
deepcopy(breadcrumb) for breadcrumb in config.breadcrumbs
]
self._feature_flag_delegate = options.pop(
'feature_flag_delegate',
FeatureFlagDelegate()
).copy()

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

self.metadata[name].update(dictionary)

@property
def feature_flags(self) -> List[FeatureFlag]:
return self._feature_flag_delegate.to_list()

def add_feature_flag(
self,
name: Union[str, bytes],
variant: Union[None, str, bytes] = None
) -> None:
self._feature_flag_delegate.add(name, variant)

def add_feature_flags(self, feature_flags: List[FeatureFlag]) -> None:
self._feature_flag_delegate.merge(feature_flags)

def clear_feature_flag(self, name: Union[str, bytes]) -> None:
self._feature_flag_delegate.remove(name)

def clear_feature_flags(self) -> None:
self._feature_flag_delegate.clear()

def _generate_error_list(
self,
exception: BaseException,
Expand Down Expand Up @@ -397,7 +426,6 @@ def _code_for(self, file_name, line, window_size=7):

def _payload(self):
# Fetch the notifier version from the package
notifier_version = package_version("bugsnag") or "unknown"
encoder = SanitizingJSONEncoder(
self.config.logger,
separators=(',', ':'),
Expand All @@ -407,11 +435,7 @@ def _payload(self):
# Construct the payload dictionary
return encoder.encode({
"apiKey": self.api_key,
"notifier": {
"name": self.NOTIFIER_NAME,
"url": self.NOTIFIER_URL,
"version": notifier_version,
},
"notifier": _NOTIFIER_INFORMATION,
"events": [{
"severity": self.severity,
"severityReason": self.severity_reason,
Expand All @@ -437,6 +461,7 @@ def _payload(self):
"session": self.session,
"breadcrumbs": [
breadcrumb.to_dict() for breadcrumb in self._breadcrumbs
]
],
"featureFlags": self._feature_flag_delegate.to_json()
}]
})
Loading

0 comments on commit 563063f

Please sign in to comment.