Skip to content

Commit

Permalink
Add feature flag API to Client
Browse files Browse the repository at this point in the history
  • Loading branch information
imjoehaines committed Aug 9, 2023
1 parent 899b916 commit bf2a5ef
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 7 deletions.
3 changes: 2 additions & 1 deletion bugsnag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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,
Expand All @@ -21,4 +22,4 @@
'auto_notify_exc_info', 'Notification', 'logger',
'BreadcrumbType', 'Breadcrumb', 'Breadcrumbs',
'OnBreadcrumbCallback', 'leave_breadcrumb', 'add_on_breadcrumb',
'remove_on_breadcrumb')
'remove_on_breadcrumb', 'FeatureFlag')
63 changes: 59 additions & 4 deletions bugsnag/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,25 @@
)
from bugsnag.configuration import Configuration, RequestConfiguration
from bugsnag.event import Event
from bugsnag.feature_flags import FeatureFlag, FeatureFlagDelegate
from bugsnag.handlers import BugsnagHandler
from bugsnag.sessiontracker import SessionTracker
from bugsnag.utils import to_rfc3339

__all__ = ('Client',)


try:
from contextvars import ContextVar
_feature_flag_delegate_context_var = ContextVar(
'bugsnag-client-feature-flag-delegate',
default=None
) # type: ContextVar[Optional[FeatureFlagDelegate]]
except ImportError:
from bugsnag.utils import ThreadContextVar
_feature_flag_delegate_context_var = ThreadContextVar('bugsnag-client-feature-flag-delegate', default=None) # type: ignore # noqa: E501


class Client:
"""
A Bugsnag monitoring and reporting client.
Expand Down Expand Up @@ -78,8 +90,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._feature_flag_delegate
)

self._leave_breadcrumb_for_event(event)
self.deliver(event, asynchronous=asynchronous)
Expand All @@ -94,8 +111,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._feature_flag_delegate
)

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

@property
def _feature_flag_delegate(self) -> FeatureFlagDelegate:
try:
feature_flag_delegate = _feature_flag_delegate_context_var.get()
except LookupError:
feature_flag_delegate = None

if feature_flag_delegate is None:
feature_flag_delegate = FeatureFlagDelegate()
_feature_flag_delegate_context_var.set(feature_flag_delegate)

return feature_flag_delegate

@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()

@property
def breadcrumbs(self) -> List[Breadcrumb]:
return self.configuration.breadcrumbs
Expand Down
5 changes: 4 additions & 1 deletion bugsnag/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ def __init__(self, exception: BaseException, config, request_config,
self._breadcrumbs = [
deepcopy(breadcrumb) for breadcrumb in config.breadcrumbs
]
self._feature_flag_delegate = FeatureFlagDelegate()
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
111 changes: 110 additions & 1 deletion tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
Client,
Configuration,
BreadcrumbType,
Breadcrumb
Breadcrumb,
FeatureFlag
)

import bugsnag.legacy as legacy
Expand All @@ -35,6 +36,8 @@ def setUp(self):
asynchronous=False,
install_sys_hook=False)

self.client.clear_feature_flags()

# Initialisation

def test_init_no_configuration(self):
Expand Down Expand Up @@ -1512,6 +1515,112 @@ def test_deleting_skip_bugsnag_attr_allows_notify(self):

assert self.sent_report_count == 1

def test_feature_flags_can_be_added_individually(self):
self.client.add_feature_flag('one')
self.client.add_feature_flag('two', 'a')
self.client.add_feature_flag('three', None)

assert self.client.feature_flags == [
FeatureFlag('one'),
FeatureFlag('two', 'a'),
FeatureFlag('three')
]

def test_feature_flags_can_be_added_in_bulk(self):
self.client.add_feature_flags([
FeatureFlag('a', '1'),
FeatureFlag('b'),
FeatureFlag('c', '3')
])

assert self.client.feature_flags == [
FeatureFlag('a', '1'),
FeatureFlag('b'),
FeatureFlag('c', '3')
]

def test_feature_flags_can_be_removed_individually(self):
self.client.add_feature_flags([
FeatureFlag('a', '1'),
FeatureFlag('b'),
FeatureFlag('c', '3')
])

self.client.clear_feature_flag('b')

assert self.client.feature_flags == [
FeatureFlag('a', '1'),
FeatureFlag('c', '3')
]

def test_feature_flags_can_be_cleared(self):
self.client.add_feature_flags([
FeatureFlag('a', '1'),
FeatureFlag('b'),
FeatureFlag('c', '3')
])

self.client.clear_feature_flags()

assert self.client.feature_flags == []

def test_feature_flags_are_included_in_payload(self):
self.client.add_feature_flags([
FeatureFlag('a', '1'),
FeatureFlag('b'),
FeatureFlag('c', '3')
])

self.client.notify(Exception('abc'))

assert self.sent_report_count == 1

payload = self.server.received[0]['json_body']
feature_flags = payload['events'][0]['featureFlags']

assert feature_flags == [
{'name': 'a', 'variant': '1'},
{'name': 'b'},
{'name': 'c', 'variant': '3'}
]

def test_mutating_client_feature_flags_does_not_affect_event(self):
self.client.add_feature_flags([
FeatureFlag('a', '1'),
FeatureFlag('b'),
FeatureFlag('c', '3')
])

def on_error(event):
# adding a flag to the event directly should affect the payload
event.add_feature_flag('d')

# adding a flag to the client should not affect the payload as the
# event has already been created
self.client.add_feature_flag('e')

self.client.configuration.middleware.before_notify(on_error)
self.client.notify(Exception('abc'))

assert self.sent_report_count == 1

payload = self.server.received[0]['json_body']
feature_flags = payload['events'][0]['featureFlags']

assert feature_flags == [
{'name': 'a', 'variant': '1'},
{'name': 'b'},
{'name': 'c', 'variant': '3'},
{'name': 'd'}
]

assert self.client.feature_flags == [
FeatureFlag('a', '1'),
FeatureFlag('b'),
FeatureFlag('c', '3'),
FeatureFlag('e')
]


@pytest.mark.parametrize("metadata,type", [
(1234, 'int'),
Expand Down

0 comments on commit bf2a5ef

Please sign in to comment.