Skip to content

Commit 776218a

Browse files
add LOG_ALL_EVENTS settings fo filter webhook events (#83)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 7a745ab commit 776218a

File tree

6 files changed

+159
-35
lines changed

6 files changed

+159
-35
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
1818

1919
## [Unreleased]
2020

21+
### Added
22+
23+
- Added `GITHUB_APP["LOG_ALL_EVENTS"]` setting to control webhook event logging. When `False`, only events with registered handlers are stored in the database.
24+
2125
## [0.6.1]
2226

2327
### Fixed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ GITHUB_APP = {
510510
"AUTO_CLEANUP_EVENTS": True,
511511
"CLIENT_ID": "",
512512
"DAYS_TO_KEEP_EVENTS": 7,
513+
"LOG_ALL_EVENTS": True,
513514
"NAME": "",
514515
"PRIVATE_KEY": "",
515516
"WEBHOOK_SECRET": "",
@@ -552,6 +553,24 @@ The GitHub App's client ID. Obtained when registering your GitHub App.
552553
553554
Number of days to retain webhook events before cleanup. Used by both automatic cleanup (when [`AUTO_CLEANUP_EVENTS`](#auto_cleanup_events) is `True`) and the `EventLog.objects.acleanup_events` manager method.
554555
556+
### `LOG_ALL_EVENTS`
557+
558+
> **Optional** | `bool` | Default: `True`
559+
560+
Controls whether all webhook events are stored in the database, or only events that have registered handlers.
561+
562+
When `True` (default), all webhook events sent to your webhook endpoint are stored as `EventLog` entries, providing a complete audit trail. This is useful for debugging and compliance purposes.
563+
564+
When `False`, only events that have registered handlers (via `@router.event()` decorators) are stored. This can significantly reduce database usage for high-traffic GitHub Apps, especially those receiving many events they don't process (e.g., the numerous pull request sub-events like "labeled", "unlabeled", etc.).
565+
566+
Example:
567+
```python
568+
GITHUB_APP = {
569+
# ... other settings ...
570+
"LOG_ALL_EVENTS": False, # Only store events with handlers
571+
}
572+
```
573+
555574
### `NAME`
556575
557576
> 🔴 **Required** | `str`

src/django_github_app/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class AppSettings:
1919
AUTO_CLEANUP_EVENTS: bool = True
2020
CLIENT_ID: str = ""
2121
DAYS_TO_KEEP_EVENTS: int = 7
22+
LOG_ALL_EVENTS: bool = True
2223
NAME: str = ""
2324
PRIVATE_KEY: str = ""
2425
WEBHOOK_SECRET: str = ""

src/django_github_app/views.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,11 @@ def get_github_api(self, installation: Installation | None) -> GitHubAPIType:
5151
installation_id = getattr(installation, "installation_id", None)
5252
return self.github_api_class(requester, installation_id=installation_id)
5353

54-
def get_response(self, event_log: EventLog) -> JsonResponse:
55-
return JsonResponse(
56-
{
57-
"message": "ok",
58-
"event_id": event_log.id,
59-
}
60-
)
54+
def get_response(self, event_log: EventLog | None) -> JsonResponse:
55+
response_data: dict[str, int | str] = {"message": "ok"}
56+
if event_log:
57+
response_data["event_id"] = event_log.id
58+
return JsonResponse(response_data)
6159

6260
@property
6361
def router(self) -> GitHubRouter:
@@ -80,12 +78,17 @@ async def post(self, request: HttpRequest) -> JsonResponse:
8078
if app_settings.AUTO_CLEANUP_EVENTS:
8179
await EventLog.objects.acleanup_events()
8280

83-
event_log = await EventLog.objects.acreate_from_event(event)
84-
installation = await Installation.objects.aget_from_event(event)
81+
found_callbacks = self.router.fetch(event)
8582

86-
async with self.get_github_api(installation) as gh:
87-
await gh.sleep(1)
88-
await self.router.adispatch(event, gh)
83+
event_log = None
84+
if app_settings.LOG_ALL_EVENTS or found_callbacks:
85+
event_log = await EventLog.objects.acreate_from_event(event)
86+
87+
if found_callbacks:
88+
installation = await Installation.objects.aget_from_event(event)
89+
async with self.get_github_api(installation) as gh:
90+
await gh.sleep(1)
91+
await self.router.adispatch(event, gh)
8992

9093
return self.get_response(event_log)
9194

@@ -100,11 +103,16 @@ def post(self, request: HttpRequest) -> JsonResponse: # pragma: no cover
100103
if app_settings.AUTO_CLEANUP_EVENTS:
101104
EventLog.objects.cleanup_events()
102105

103-
event_log = EventLog.objects.create_from_event(event)
104-
installation = Installation.objects.get_from_event(event)
106+
found_callbacks = self.router.fetch(event)
107+
108+
event_log = None
109+
if app_settings.LOG_ALL_EVENTS or found_callbacks:
110+
event_log = EventLog.objects.create_from_event(event)
105111

106-
with self.get_github_api(installation) as gh:
107-
time.sleep(1)
108-
self.router.dispatch(event, gh)
112+
if found_callbacks:
113+
installation = Installation.objects.get_from_event(event)
114+
with self.get_github_api(installation) as gh:
115+
time.sleep(1)
116+
self.router.dispatch(event, gh)
109117

110118
return self.get_response(event_log)

tests/test_conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
("AUTO_CLEANUP_EVENTS", True),
1717
("CLIENT_ID", ""),
1818
("DAYS_TO_KEEP_EVENTS", 7),
19+
("LOG_ALL_EVENTS", True),
1920
("NAME", ""),
2021
("PRIVATE_KEY", ""),
2122
("WEBHOOK_SECRET", ""),

tests/test_views.py

Lines changed: 109 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ def _make_request(
4343
if body is None:
4444
body = {}
4545

46+
body_json = json.dumps(body).encode("UTF-8")
4647
hmac_obj = hmac.new(
4748
secret.encode("UTF-8"),
48-
msg=json.dumps(body).encode("UTF-8"),
49+
msg=body_json,
4950
digestmod="sha256",
5051
)
5152
signature = f"sha256={hmac_obj.hexdigest()}"
@@ -59,7 +60,7 @@ def _make_request(
5960

6061
request = rf.post(
6162
"/webhook/",
62-
data=body,
63+
data=body_json,
6364
content_type="application/json",
6465
**headers,
6566
)
@@ -184,8 +185,9 @@ async def test_csrf_exempt(self, webhook_request):
184185

185186
assert response.status_code != HTTPStatus.FORBIDDEN
186187

187-
async def test_event_log_created(self, webhook_request):
188-
request = webhook_request()
188+
async def test_event_log_created(self, webhook_request, aregister_webhook_event):
189+
aregister_webhook_event("push")
190+
request = webhook_request(event_type="push")
189191
view = AsyncWebhookView()
190192

191193
response = await view.post(request)
@@ -237,15 +239,59 @@ async def test_router_dispatch(self, aregister_webhook_event, webhook_request):
237239
assert isinstance(webhook_data["gh"], AsyncGitHubAPI)
238240

239241
async def test_router_dispatch_unhandled_event(
240-
self, aregister_webhook_event, webhook_request
242+
self, monkeypatch, aregister_webhook_event, override_app_settings
241243
):
242-
aregister_webhook_event("push", should_fail=True)
243-
request = webhook_request(event_type="issues", body={"action": "opened"})
244-
view = AsyncWebhookView()
244+
with override_app_settings(LOG_ALL_EVENTS=False):
245+
aregister_webhook_event("push", should_fail=True)
246+
view = AsyncWebhookView()
245247

246-
response = await view.post(request)
248+
data = {"action": "opened"}
249+
event = sansio.Event(data, event="issues", delivery_id="12345")
247250

248-
assert response.status_code == HTTPStatus.OK
251+
monkeypatch.setattr(view, "get_event", lambda request: event)
252+
253+
response = await view.post(None)
254+
255+
assert response.status_code == HTTPStatus.OK
256+
assert json.loads(response.content) == {"message": "ok"}
257+
258+
async def test_unhandled_event_log_creation_with_log_all(
259+
self, monkeypatch, aregister_webhook_event, override_app_settings
260+
):
261+
with override_app_settings(LOG_ALL_EVENTS=True):
262+
aregister_webhook_event("push", should_fail=True)
263+
view = AsyncWebhookView()
264+
265+
data = {"action": "opened"}
266+
event = sansio.Event(data, event="issues", delivery_id="12345")
267+
268+
monkeypatch.setattr(view, "get_event", lambda request: event)
269+
270+
count_before = await EventLog.objects.acount()
271+
272+
await view.post(None)
273+
274+
count_after = await EventLog.objects.acount()
275+
assert count_after - count_before == 1
276+
277+
async def test_unhandled_event_log_creation_without_log_all(
278+
self, monkeypatch, aregister_webhook_event, override_app_settings
279+
):
280+
with override_app_settings(LOG_ALL_EVENTS=False):
281+
aregister_webhook_event("push", should_fail=True)
282+
view = AsyncWebhookView()
283+
284+
data = {"action": "opened"}
285+
event = sansio.Event(data, event="issues", delivery_id="12345")
286+
287+
monkeypatch.setattr(view, "get_event", lambda request: event)
288+
289+
count_before = await EventLog.objects.acount()
290+
291+
await view.post(None)
292+
293+
count_after = await EventLog.objects.acount()
294+
assert count_after - count_before == 0
249295

250296

251297
class TestSyncWebhookView:
@@ -266,8 +312,9 @@ def test_csrf_exempt(self, webhook_request):
266312

267313
assert response.status_code != HTTPStatus.FORBIDDEN
268314

269-
def test_event_log_created(self, webhook_request):
270-
request = webhook_request()
315+
def test_event_log_created(self, webhook_request, register_webhook_event):
316+
register_webhook_event("push")
317+
request = webhook_request(event_type="push")
271318
view = SyncWebhookView()
272319

273320
response = view.post(request)
@@ -305,12 +352,56 @@ def test_router_dispatch(self, register_webhook_event, webhook_request):
305352
assert isinstance(webhook_data["gh"], SyncGitHubAPI)
306353

307354
def test_router_dispatch_unhandled_event(
308-
self, register_webhook_event, webhook_request
355+
self, monkeypatch, register_webhook_event, override_app_settings
309356
):
310-
register_webhook_event("push", should_fail=True)
311-
request = webhook_request(event_type="issues", body={"action": "opened"})
312-
view = SyncWebhookView()
357+
with override_app_settings(LOG_ALL_EVENTS=False):
358+
register_webhook_event("push", should_fail=True)
359+
view = SyncWebhookView()
313360

314-
response = view.post(request)
361+
data = {"action": "opened"}
362+
event = sansio.Event(data, event="issues", delivery_id="12345")
315363

316-
assert response.status_code == HTTPStatus.OK
364+
monkeypatch.setattr(view, "get_event", lambda request: event)
365+
366+
response = view.post(None)
367+
368+
assert response.status_code == HTTPStatus.OK
369+
assert json.loads(response.content) == {"message": "ok"}
370+
371+
def test_unhandled_event_log_creation_with_log_all(
372+
self, monkeypatch, register_webhook_event, override_app_settings
373+
):
374+
with override_app_settings(LOG_ALL_EVENTS=True):
375+
register_webhook_event("push", should_fail=True)
376+
view = SyncWebhookView()
377+
378+
data = {"action": "opened"}
379+
event = sansio.Event(data, event="issues", delivery_id="12345")
380+
381+
monkeypatch.setattr(view, "get_event", lambda request: event)
382+
383+
count_before = EventLog.objects.count()
384+
385+
view.post(None)
386+
387+
count_after = EventLog.objects.count()
388+
assert count_after - count_before == 1
389+
390+
def test_unhandled_event_log_creation_without_log_all(
391+
self, monkeypatch, register_webhook_event, override_app_settings
392+
):
393+
with override_app_settings(LOG_ALL_EVENTS=False):
394+
register_webhook_event("push", should_fail=True)
395+
view = SyncWebhookView()
396+
397+
data = {"action": "opened"}
398+
event = sansio.Event(data, event="issues", delivery_id="12345")
399+
400+
monkeypatch.setattr(view, "get_event", lambda request: event)
401+
402+
count_before = EventLog.objects.count()
403+
404+
view.post(None)
405+
406+
count_after = EventLog.objects.count()
407+
assert count_after - count_before == 0

0 commit comments

Comments
 (0)