Skip to content

Commit

Permalink
feat(error-capture): Add basic exception autocapture (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
neilkakkar authored Aug 28, 2024
1 parent 16cbd10 commit 24b7b91
Show file tree
Hide file tree
Showing 7 changed files with 976 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.6.0 - 2024-08-28

1. Adds exception autocapture in alpha state. This feature is not yet stable and may change in future versions.

## 3.5.2 - 2024-08-21

1. Guard for None values in local evaluation
Expand Down
6 changes: 6 additions & 0 deletions posthog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
poll_interval = 30 # type: int
disable_geoip = True # type: bool
feature_flags_request_timeout_seconds = 3 # type: int
# Currently alpha, use at your own risk
enable_exception_autocapture = False # type: bool

default_client = None # type: Optional[Client]

Expand Down Expand Up @@ -454,6 +456,10 @@ def _proxy(method, *args, **kwargs):
disabled=disabled,
disable_geoip=disable_geoip,
feature_flags_request_timeout_seconds=feature_flags_request_timeout_seconds,
# TODO: Currently this monitoring begins only when the Client is initialised (which happens when you do something with the SDK)
# This kind of initialisation is very annoying for exception capture. We need to figure out a way around this,
# 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)
enable_exception_autocapture=enable_exception_autocapture,
)

# always set incase user changes it
Expand Down
6 changes: 6 additions & 0 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from six import string_types

from posthog.consumer import Consumer
from posthog.exception_capture import ExceptionCapture
from posthog.feature_flags import InconclusiveMatchError, match_feature_flag_properties
from posthog.poller import Poller
from posthog.request import APIError, batch_post, decide, determine_server_host, get
Expand Down Expand Up @@ -51,6 +52,7 @@ def __init__(
disable_geoip=True,
historical_migration=False,
feature_flags_request_timeout_seconds=3,
enable_exception_autocapture=False,
):
self.queue = queue.Queue(max_queue_size)

Expand All @@ -77,6 +79,7 @@ def __init__(
self.disabled = disabled
self.disable_geoip = disable_geoip
self.historical_migration = historical_migration
self.enable_exception_autocapture = enable_exception_autocapture

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

if self.enable_exception_autocapture:
self.exception_capture = ExceptionCapture(self)

if sync_mode:
self.consumers = None
else:
Expand Down
53 changes: 53 additions & 0 deletions posthog/exception_capture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import logging
import sys
import threading
from typing import TYPE_CHECKING

from posthog.exception_utils import exceptions_from_error_tuple

if TYPE_CHECKING:
from posthog.client import Client


class ExceptionCapture:
# TODO: Add client side rate limiting to prevent spamming the server with exceptions

log = logging.getLogger("posthog")

def __init__(self, client: "Client"):
self.client = client
self.original_excepthook = sys.excepthook
sys.excepthook = self.exception_handler
threading.excepthook = self.thread_exception_handler

def exception_handler(self, exc_type, exc_value, exc_traceback):
# don't affect default behaviour.
self.capture_exception(exc_type, exc_value, exc_traceback)
self.original_excepthook(exc_type, exc_value, exc_traceback)

def thread_exception_handler(self, args):
self.capture_exception(args.exc_type, args.exc_value, args.exc_traceback)

def capture_exception(self, exc_type, exc_value, exc_traceback):
try:
# if hasattr(sys, "ps1"):
# # Disable the excepthook for interactive Python shells
# return

# Format stack trace like sentry
all_exceptions_with_trace = exceptions_from_error_tuple((exc_type, exc_value, exc_traceback))

properties = {
"$exception_type": all_exceptions_with_trace[0].get("type"),
"$exception_message": all_exceptions_with_trace[0].get("value"),
"$exception_list": all_exceptions_with_trace,
# TODO: Can we somehow get distinct_id from context here? Stateless lib makes this much harder? 😅
# '$exception_personURL': f'{self.client.posthog_host}/project/{self.client.token}/person/{self.client.get_distinct_id()}'
}

# TODO: What distinct id should we attach these server-side exceptions to?
# Any heuristic seems prone to errors - how can we know if exception occurred in the context of a user that captured some other event?

self.client.capture("python-exceptions", "$exception", properties=properties)
except Exception as e:
self.log.exception(f"Failed to capture exception: {e}")
Loading

0 comments on commit 24b7b91

Please sign in to comment.