Skip to content

Commit 3eb82bb

Browse files
add LOG_ALL_EVENTS settings fo filter webhook events
1 parent 7a745ab commit 3eb82bb

File tree

6 files changed

+193
-39
lines changed

6 files changed

+193
-39
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: 143 additions & 22 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,10 @@ 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+
# Register a handler for the webhook event
190+
aregister_webhook_event("push")
191+
request = webhook_request(event_type="push")
189192
view = AsyncWebhookView()
190193

191194
response = await view.post(request)
@@ -237,15 +240,73 @@ async def test_router_dispatch(self, aregister_webhook_event, webhook_request):
237240
assert isinstance(webhook_data["gh"], AsyncGitHubAPI)
238241

239242
async def test_router_dispatch_unhandled_event(
240-
self, aregister_webhook_event, webhook_request
243+
self, monkeypatch, aregister_webhook_event, override_app_settings
241244
):
242-
aregister_webhook_event("push", should_fail=True)
243-
request = webhook_request(event_type="issues", body={"action": "opened"})
244-
view = AsyncWebhookView()
245-
246-
response = await view.post(request)
247-
248-
assert response.status_code == HTTPStatus.OK
245+
with override_app_settings(LOG_ALL_EVENTS=False):
246+
aregister_webhook_event("push", should_fail=True)
247+
view = AsyncWebhookView()
248+
249+
# Create event directly, bypassing webhook validation
250+
data = {"action": "opened"}
251+
event = sansio.Event(data, event="issues", delivery_id="12345")
252+
253+
# Mock the get_event method to return our event
254+
monkeypatch.setattr(view, "get_event", lambda request: event)
255+
256+
response = await view.post(None)
257+
258+
assert response.status_code == HTTPStatus.OK
259+
assert json.loads(response.content) == {"message": "ok"}
260+
261+
async def test_unhandled_event_log_creation_with_log_all(
262+
self, monkeypatch, aregister_webhook_event, override_app_settings
263+
):
264+
with override_app_settings(LOG_ALL_EVENTS=True):
265+
# Register handler for "push" but send "issues" event
266+
aregister_webhook_event("push", should_fail=True)
267+
view = AsyncWebhookView()
268+
269+
# Create event directly, bypassing webhook validation
270+
data = {"action": "opened"}
271+
event = sansio.Event(data, event="issues", delivery_id="12345")
272+
273+
# Mock the get_event method to return our event
274+
monkeypatch.setattr(view, "get_event", lambda request: event)
275+
276+
# Get count before
277+
count_before = await EventLog.objects.acount()
278+
279+
# Process request (with a dummy request)
280+
await view.post(None)
281+
282+
# Verify event log is created when LOG_ALL_EVENTS=True
283+
count_after = await EventLog.objects.acount()
284+
assert count_after - count_before == 1
285+
286+
async def test_unhandled_event_log_creation_without_log_all(
287+
self, monkeypatch, aregister_webhook_event, override_app_settings
288+
):
289+
with override_app_settings(LOG_ALL_EVENTS=False):
290+
# Register handler for "push" but send "issues" event
291+
aregister_webhook_event("push", should_fail=True)
292+
view = AsyncWebhookView()
293+
294+
# Create event directly, bypassing webhook validation
295+
data = {"action": "opened"}
296+
event = sansio.Event(data, event="issues", delivery_id="12345")
297+
298+
# Mock the get_event method to return our event
299+
monkeypatch.setattr(view, "get_event", lambda request: event)
300+
301+
# Get count before
302+
count_before = await EventLog.objects.acount()
303+
304+
# Process request (with a dummy request)
305+
await view.post(None)
306+
307+
# Verify event log is not created when LOG_ALL_EVENTS=False
308+
count_after = await EventLog.objects.acount()
309+
assert count_after - count_before == 0
249310

250311

251312
class TestSyncWebhookView:
@@ -266,8 +327,10 @@ def test_csrf_exempt(self, webhook_request):
266327

267328
assert response.status_code != HTTPStatus.FORBIDDEN
268329

269-
def test_event_log_created(self, webhook_request):
270-
request = webhook_request()
330+
def test_event_log_created(self, webhook_request, register_webhook_event):
331+
# Register a handler for the webhook event
332+
register_webhook_event("push")
333+
request = webhook_request(event_type="push")
271334
view = SyncWebhookView()
272335

273336
response = view.post(request)
@@ -305,12 +368,70 @@ def test_router_dispatch(self, register_webhook_event, webhook_request):
305368
assert isinstance(webhook_data["gh"], SyncGitHubAPI)
306369

307370
def test_router_dispatch_unhandled_event(
308-
self, register_webhook_event, webhook_request
371+
self, monkeypatch, register_webhook_event, override_app_settings
309372
):
310-
register_webhook_event("push", should_fail=True)
311-
request = webhook_request(event_type="issues", body={"action": "opened"})
312-
view = SyncWebhookView()
313-
314-
response = view.post(request)
315-
316-
assert response.status_code == HTTPStatus.OK
373+
with override_app_settings(LOG_ALL_EVENTS=False):
374+
register_webhook_event("push", should_fail=True)
375+
view = SyncWebhookView()
376+
377+
# Create event directly, bypassing webhook validation
378+
data = {"action": "opened"}
379+
event = sansio.Event(data, event="issues", delivery_id="12345")
380+
381+
# Mock the get_event method to return our event
382+
monkeypatch.setattr(view, "get_event", lambda request: event)
383+
384+
response = view.post(None)
385+
386+
assert response.status_code == HTTPStatus.OK
387+
assert json.loads(response.content) == {"message": "ok"}
388+
389+
def test_unhandled_event_log_creation_with_log_all(
390+
self, monkeypatch, register_webhook_event, override_app_settings
391+
):
392+
with override_app_settings(LOG_ALL_EVENTS=True):
393+
# Register handler for "push" but send "issues" event
394+
register_webhook_event("push", should_fail=True)
395+
view = SyncWebhookView()
396+
397+
# Create event directly, bypassing webhook validation
398+
data = {"action": "opened"}
399+
event = sansio.Event(data, event="issues", delivery_id="12345")
400+
401+
# Mock the get_event method to return our event
402+
monkeypatch.setattr(view, "get_event", lambda request: event)
403+
404+
# Get count before
405+
count_before = EventLog.objects.count()
406+
407+
# Process request (with a dummy request)
408+
view.post(None)
409+
410+
# Verify event log is created when LOG_ALL_EVENTS=True
411+
count_after = EventLog.objects.count()
412+
assert count_after - count_before == 1
413+
414+
def test_unhandled_event_log_creation_without_log_all(
415+
self, monkeypatch, register_webhook_event, override_app_settings
416+
):
417+
with override_app_settings(LOG_ALL_EVENTS=False):
418+
# Register handler for "push" but send "issues" event
419+
register_webhook_event("push", should_fail=True)
420+
view = SyncWebhookView()
421+
422+
# Create event directly, bypassing webhook validation
423+
data = {"action": "opened"}
424+
event = sansio.Event(data, event="issues", delivery_id="12345")
425+
426+
# Mock the get_event method to return our event
427+
monkeypatch.setattr(view, "get_event", lambda request: event)
428+
429+
# Get count before
430+
count_before = EventLog.objects.count()
431+
432+
# Process request (with a dummy request)
433+
view.post(None)
434+
435+
# Verify event log is not created when LOG_ALL_EVENTS=False
436+
count_after = EventLog.objects.count()
437+
assert count_after - count_before == 0

0 commit comments

Comments
 (0)