Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v4.6.0 #363

Merged
merged 29 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
93a0b6d
Add FeatureFlag class
imjoehaines Jul 20, 2023
8f7f9b6
Add FeatureFlagDelegate
imjoehaines Jul 20, 2023
eb54292
Implement __eq__ for FeatureFlag
imjoehaines Jul 21, 2023
9754c5b
Take advantage of __eq__ in feature flag tests
imjoehaines Jul 21, 2023
899b916
Add feature flag API to Event
imjoehaines Jul 21, 2023
a642efb
Add feature flag API to Client
imjoehaines Jul 25, 2023
f9a062e
Add feature flag API to legacy.py
imjoehaines Aug 9, 2023
f40ea8e
Don't run tox in parallel
imjoehaines Aug 10, 2023
13c9f97
Bump GH actions versions
imjoehaines Aug 9, 2023
95603e0
Run on ubuntu-latest
imjoehaines Aug 9, 2023
429fe42
Add tests for feature flags in Bottle
imjoehaines Aug 11, 2023
48c287a
Fix last_event_request returning the first event
imjoehaines Aug 10, 2023
90b9ee7
Add sent_report_count to AsyncIntegrationTest
imjoehaines Aug 10, 2023
cde7b7c
Add tests for feature flags in ASGI apps
imjoehaines Aug 10, 2023
e544364
Add context module for per-client per-request data
imjoehaines Aug 25, 2023
f26fc82
Use new context module for feature flags
imjoehaines Aug 25, 2023
7d8ae2b
Simplify ContextLocalState API
imjoehaines Aug 25, 2023
1b76644
Create a new context for each WSGI request
imjoehaines Aug 30, 2023
8c328f9
Create a new context for each Tornado request
imjoehaines Sep 1, 2023
f379b10
Create a new context for each Django request
imjoehaines Sep 4, 2023
ede062b
Implement __repr__ for FeatureFlag
imjoehaines Sep 4, 2023
e8aeed5
Fix feature flag to_dict key
imjoehaines Sep 4, 2023
a5dcd5b
Fix asyncio event loop deprecation warning in test
imjoehaines Sep 4, 2023
0d26241
Remove use of deprecated pkg_resources module
imjoehaines Sep 4, 2023
7c758d1
Update release instructions
imjoehaines Sep 4, 2023
2eebc50
Update changelog
imjoehaines Sep 5, 2023
6696f3b
Add feature flags to changelog
imjoehaines Sep 5, 2023
71176aa
Add date to changelog
imjoehaines Sep 5, 2023
8b023d9
Bump version to v4.6.0
imjoehaines Sep 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading