Skip to content

Commit 24b7b91

Browse files
authored
feat(error-capture): Add basic exception autocapture (#128)
1 parent 16cbd10 commit 24b7b91

File tree

7 files changed

+976
-1
lines changed

7 files changed

+976
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.6.0 - 2024-08-28
2+
3+
1. Adds exception autocapture in alpha state. This feature is not yet stable and may change in future versions.
4+
15
## 3.5.2 - 2024-08-21
26

37
1. Guard for None values in local evaluation

posthog/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
poll_interval = 30 # type: int
2020
disable_geoip = True # type: bool
2121
feature_flags_request_timeout_seconds = 3 # type: int
22+
# Currently alpha, use at your own risk
23+
enable_exception_autocapture = False # type: bool
2224

2325
default_client = None # type: Optional[Client]
2426

@@ -454,6 +456,10 @@ def _proxy(method, *args, **kwargs):
454456
disabled=disabled,
455457
disable_geoip=disable_geoip,
456458
feature_flags_request_timeout_seconds=feature_flags_request_timeout_seconds,
459+
# TODO: Currently this monitoring begins only when the Client is initialised (which happens when you do something with the SDK)
460+
# This kind of initialisation is very annoying for exception capture. We need to figure out a way around this,
461+
# or deprecate this proxy option fully (it's already in the process of deprecation, no new clients should be using this method since like 5-6 months)
462+
enable_exception_autocapture=enable_exception_autocapture,
457463
)
458464

459465
# always set incase user changes it

posthog/client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from six import string_types
99

1010
from posthog.consumer import Consumer
11+
from posthog.exception_capture import ExceptionCapture
1112
from posthog.feature_flags import InconclusiveMatchError, match_feature_flag_properties
1213
from posthog.poller import Poller
1314
from posthog.request import APIError, batch_post, decide, determine_server_host, get
@@ -51,6 +52,7 @@ def __init__(
5152
disable_geoip=True,
5253
historical_migration=False,
5354
feature_flags_request_timeout_seconds=3,
55+
enable_exception_autocapture=False,
5456
):
5557
self.queue = queue.Queue(max_queue_size)
5658

@@ -77,6 +79,7 @@ def __init__(
7779
self.disabled = disabled
7880
self.disable_geoip = disable_geoip
7981
self.historical_migration = historical_migration
82+
self.enable_exception_autocapture = enable_exception_autocapture
8083

8184
# personal_api_key: This should be a generated Personal API Key, private
8285
self.personal_api_key = personal_api_key
@@ -88,6 +91,9 @@ def __init__(
8891
else:
8992
self.log.setLevel(logging.WARNING)
9093

94+
if self.enable_exception_autocapture:
95+
self.exception_capture = ExceptionCapture(self)
96+
9197
if sync_mode:
9298
self.consumers = None
9399
else:

posthog/exception_capture.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import logging
2+
import sys
3+
import threading
4+
from typing import TYPE_CHECKING
5+
6+
from posthog.exception_utils import exceptions_from_error_tuple
7+
8+
if TYPE_CHECKING:
9+
from posthog.client import Client
10+
11+
12+
class ExceptionCapture:
13+
# TODO: Add client side rate limiting to prevent spamming the server with exceptions
14+
15+
log = logging.getLogger("posthog")
16+
17+
def __init__(self, client: "Client"):
18+
self.client = client
19+
self.original_excepthook = sys.excepthook
20+
sys.excepthook = self.exception_handler
21+
threading.excepthook = self.thread_exception_handler
22+
23+
def exception_handler(self, exc_type, exc_value, exc_traceback):
24+
# don't affect default behaviour.
25+
self.capture_exception(exc_type, exc_value, exc_traceback)
26+
self.original_excepthook(exc_type, exc_value, exc_traceback)
27+
28+
def thread_exception_handler(self, args):
29+
self.capture_exception(args.exc_type, args.exc_value, args.exc_traceback)
30+
31+
def capture_exception(self, exc_type, exc_value, exc_traceback):
32+
try:
33+
# if hasattr(sys, "ps1"):
34+
# # Disable the excepthook for interactive Python shells
35+
# return
36+
37+
# Format stack trace like sentry
38+
all_exceptions_with_trace = exceptions_from_error_tuple((exc_type, exc_value, exc_traceback))
39+
40+
properties = {
41+
"$exception_type": all_exceptions_with_trace[0].get("type"),
42+
"$exception_message": all_exceptions_with_trace[0].get("value"),
43+
"$exception_list": all_exceptions_with_trace,
44+
# TODO: Can we somehow get distinct_id from context here? Stateless lib makes this much harder? 😅
45+
# '$exception_personURL': f'{self.client.posthog_host}/project/{self.client.token}/person/{self.client.get_distinct_id()}'
46+
}
47+
48+
# TODO: What distinct id should we attach these server-side exceptions to?
49+
# Any heuristic seems prone to errors - how can we know if exception occurred in the context of a user that captured some other event?
50+
51+
self.client.capture("python-exceptions", "$exception", properties=properties)
52+
except Exception as e:
53+
self.log.exception(f"Failed to capture exception: {e}")

0 commit comments

Comments
 (0)