Skip to content

Commit bed2507

Browse files
committed
Add support for post-delivery callbacks
1 parent e57f70c commit bed2507

File tree

3 files changed

+186
-2
lines changed

3 files changed

+186
-2
lines changed

bugsnag/delivery.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,23 @@
3232
__all__ = ('default_headers', 'Delivery')
3333

3434

35+
def _noop():
36+
pass
37+
38+
39+
def _marshall_post_delivery_callback(post_delivery_callback):
40+
if not callable(post_delivery_callback):
41+
return _noop
42+
43+
def safe_post_delivery_callback():
44+
try:
45+
post_delivery_callback()
46+
except Exception:
47+
pass
48+
49+
return safe_post_delivery_callback
50+
51+
3552
def create_default_delivery():
3653
if requests is not None:
3754
return RequestsDelivery()
@@ -81,6 +98,10 @@ def deliver_sessions(self, config, payload: Any, options=None):
8198
self.deliver(config, payload, options)
8299

83100
def queue_request(self, request: Callable, config, options: Dict):
101+
post_delivery_callback = _marshall_post_delivery_callback(
102+
options.pop('post_delivery_callback', None)
103+
)
104+
84105
if config.asynchronous and options.pop('asynchronous', True):
85106
# if an exception escapes the thread, our threading.excepthook
86107
# will catch it and attempt to deliver it
@@ -91,10 +112,15 @@ def safe_request():
91112
request()
92113
except Exception as e:
93114
config.logger.exception('Notifying Bugsnag failed %s', e)
115+
finally:
116+
post_delivery_callback()
94117

95118
Thread(target=safe_request).start()
96119
else:
97-
request()
120+
try:
121+
request()
122+
finally:
123+
post_delivery_callback()
98124

99125

100126
class UrllibDelivery(Delivery):

tests/test_delivery.py

+134-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pytest
12
import warnings
23
import sys
34

@@ -9,7 +10,7 @@
910
DEFAULT_SESSIONS_ENDPOINT
1011
)
1112

12-
from tests.utils import IntegrationTest
13+
from tests.utils import BrokenDelivery, IntegrationTest, QueueingDelivery
1314

1415

1516
class DeliveryTest(IntegrationTest):
@@ -64,3 +65,135 @@ def test_misconfigured_sessions_endpoint_sends_warning(self):
6465
delivery.deliver_sessions(self.config, '{"apiKey":"aaab"}')
6566
self.assertEqual(1, len(warn))
6667
self.assertEqual(0, len(self.server.events_received))
68+
69+
def test_it_calls_post_delivery_callback(self):
70+
callback_was_called = False
71+
72+
def post_delivery_callback():
73+
nonlocal callback_was_called
74+
callback_was_called = True
75+
76+
self.config.delivery.deliver(
77+
self.config,
78+
'{"legit": 5}',
79+
options={'post_delivery_callback': post_delivery_callback}
80+
)
81+
82+
assert callback_was_called
83+
84+
def test_it_calls_post_delivery_callback_after_success(self):
85+
delivery = QueueingDelivery()
86+
self.config.configure(delivery=delivery)
87+
88+
callback_was_called = False
89+
90+
def post_delivery_callback():
91+
nonlocal callback_was_called
92+
callback_was_called = True
93+
94+
delivery.deliver(
95+
self.config,
96+
'{"legit": 5}',
97+
options={'post_delivery_callback': post_delivery_callback}
98+
)
99+
100+
assert not callback_was_called
101+
102+
delivery.flush_request_queue()
103+
assert callback_was_called
104+
105+
def test_it_calls_post_delivery_callback_on_failure(self):
106+
self.config.configure(delivery=BrokenDelivery())
107+
108+
callback_was_called = False
109+
110+
def post_delivery_callback():
111+
nonlocal callback_was_called
112+
callback_was_called = True
113+
114+
with pytest.raises(Exception):
115+
self.config.delivery.deliver(
116+
self.config,
117+
'{"legit": 6}',
118+
options={'post_delivery_callback': post_delivery_callback}
119+
)
120+
121+
assert callback_was_called
122+
123+
def test_it_calls_post_delivery_callback_for_sessions(self):
124+
callback_was_called = False
125+
126+
def post_delivery_callback():
127+
nonlocal callback_was_called
128+
callback_was_called = True
129+
130+
self.config.delivery.deliver_sessions(
131+
self.config,
132+
'{"legit": 7}',
133+
options={'post_delivery_callback': post_delivery_callback}
134+
)
135+
136+
assert callback_was_called
137+
138+
def test_it_calls_post_delivery_callback_for_sessions_after_success(self):
139+
delivery = QueueingDelivery()
140+
self.config.configure(delivery=delivery)
141+
142+
callback_was_called = False
143+
144+
def post_delivery_callback():
145+
nonlocal callback_was_called
146+
callback_was_called = True
147+
148+
delivery.deliver_sessions(
149+
self.config,
150+
'{"legit": 5}',
151+
options={'post_delivery_callback': post_delivery_callback}
152+
)
153+
154+
assert not callback_was_called
155+
156+
delivery.flush_request_queue()
157+
assert callback_was_called
158+
159+
def test_it_calls_post_delivery_callback_for_sessions_on_failure(self):
160+
self.config.configure(delivery=BrokenDelivery())
161+
162+
callback_was_called = False
163+
164+
def post_delivery_callback():
165+
nonlocal callback_was_called
166+
callback_was_called = True
167+
168+
with pytest.raises(Exception):
169+
self.config.delivery.deliver_sessions(
170+
self.config,
171+
'{"legit": 8}',
172+
options={'post_delivery_callback': post_delivery_callback}
173+
)
174+
175+
assert callback_was_called
176+
177+
def test_it_handles_invalid_post_delivery_callback(self):
178+
self.config.delivery.deliver(
179+
self.config,
180+
'{"legit": 5}',
181+
options={'post_delivery_callback': 'not callable :)'}
182+
)
183+
184+
def test_it_handles_errors_in_post_delivery_callback(self):
185+
callback_was_called = False
186+
187+
def post_delivery_callback():
188+
nonlocal callback_was_called
189+
callback_was_called = True
190+
191+
raise Exception('oh dear')
192+
193+
self.config.delivery.deliver(
194+
self.config,
195+
'{"legit": 5}',
196+
options={'post_delivery_callback': post_delivery_callback}
197+
)
198+
199+
assert callback_was_called

tests/utils.py

+25
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from http.server import SimpleHTTPRequestHandler, HTTPServer
88

99
import bugsnag
10+
from bugsnag.delivery import Delivery
1011

1112

1213
try:
@@ -176,3 +177,27 @@ def sent_session_count(self) -> int:
176177
class ScaryException(Exception):
177178
class NestedScaryException(Exception):
178179
pass
180+
181+
182+
class QueueingDelivery(Delivery):
183+
def __init__(self):
184+
self._queue = []
185+
186+
def deliver(self, config, payload, options):
187+
self._queue.append(options['post_delivery_callback'])
188+
189+
def flush_request_queue(self):
190+
for callback in self._queue:
191+
callback()
192+
193+
self._queue.clear()
194+
195+
196+
class BrokenDelivery(Delivery):
197+
def deliver(self, config, payload, options):
198+
def request():
199+
raise Exception('broken!')
200+
201+
options['asynchronous'] = False
202+
203+
self.queue_request(request, config, options)

0 commit comments

Comments
 (0)