seitime-frappe/frappe/utils/sentry.py
Akhil Narang 92326d143d
fix(sentry): set scope for background jobs
Signed-off-by: Akhil Narang <me@akhilnarang.dev>
2024-01-16 16:26:48 +05:30

151 lines
4.1 KiB
Python

import os
import sys
from datetime import datetime
import rq
from sentry_sdk import capture_message as sentry_capture_message
from sentry_sdk.hub import Hub
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations.wsgi import _make_wsgi_event_processor
from sentry_sdk.tracing import SOURCE_FOR_STYLE
from sentry_sdk.tracing_utils import record_sql_queries
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
import frappe
import frappe.monitor
from frappe.database.database import Database, EmptyQueryValues
class FrappeIntegration(Integration):
identifier = "frappe"
@staticmethod
def setup_once():
real_connect = Database.connect
real_sql = Database.sql
def sql(self, query, values=None, *args, **kwargs):
hub = Hub.current
if not self._conn:
self.connect()
with record_sql_queries(
hub, self._cursor, query, values, paramstyle="pyformat", executemany=False
):
return real_sql(self, query, values or EmptyQueryValues, *args, **kwargs)
def connect(self):
hub = Hub.current
with capture_internal_exceptions():
hub.add_breadcrumb(message="connect", category="query")
with hub.start_span(op="db", description="connect"):
return real_connect(self)
Database.connect = connect
Database.sql = sql
def set_scope(scope):
if job := rq.get_current_job():
kwargs = job._kwargs
transaction_name = str(kwargs["method"])
context = frappe._dict({"scheduled": False, "wait": 0})
if "run_scheduled_job" in transaction_name:
transaction_name = kwargs.get("kwargs", {}).get("job_type", "")
context.scheduled = True
waitdiff = datetime.utcnow() - job.enqueued_at
context.uuid = job.id
context.wait = waitdiff.total_seconds()
scope.set_extra("job", context)
scope.set_transaction_name(transaction_name)
else:
if frappe.form_dict.cmd:
path = f"/api/method/{frappe.form_dict.cmd}"
else:
path = frappe.request.path
scope.set_transaction_name(
path,
source=SOURCE_FOR_STYLE["endpoint"],
)
scope.set_user({"id": frappe.local.site})
user = getattr(frappe.session, "user", "Unidentified")
scope.set_tag("frappe_user", user)
# Extract `X-Frappe-Request-ID` to store as a separate field if its present
if trace_id := frappe.monitor.get_trace_id():
scope.set_tag("frappe_trace_id", trace_id)
def set_sentry_context():
if not frappe.get_system_settings("enable_telemetry"):
return
hub = Hub.current
with hub.configure_scope() as scope:
set_scope(scope)
def before_send(event, hint):
if event.get("logger", "") == "CSSUTILS":
return None
return event
def capture_exception(message: str | None = None) -> None:
"""
Function to upload exception data to entry
:param message: A message to be sent if we can't find an exception
"""
# Don't report anything if the user hasn't opted-in to telemetry
if not frappe.get_system_settings("enable_telemetry"):
return
try:
hub = Hub.current
with hub.configure_scope() as scope:
if (
os.getenv("ENABLE_SENTRY_DB_MONITORING") is None
or os.getenv("SENTRY_TRACING_SAMPLE_RATE") is None
):
set_scope(scope)
if frappe.request:
evt_processor = _make_wsgi_event_processor(frappe.request.environ, False)
scope.add_event_processor(evt_processor)
if frappe.request.is_json:
scope.set_context("JSON Body", frappe.request.json)
elif frappe.request.form:
scope.set_context("Form Data", frappe.request.form)
if client := hub.client:
exc_info = sys.exc_info()
if any(exc_info):
# Don't report validation errors
if isinstance(exc_info[0], frappe.ValidationError):
return
event, hint = event_from_exception(
exc_info,
client_options=client.options,
mechanism={"type": "wsgi", "handled": False},
)
hub.capture_event(event, hint=hint)
elif message:
sentry_capture_message(message, level="error")
except Exception:
frappe.logger().error("Failed to capture exception", exc_info=True)
pass
def add_bootinfo(bootinfo):
"""Called from hook, sends DSN so client side can setup error monitoring."""
if not frappe.get_system_settings("enable_telemetry"):
return
if sentry_dsn := os.getenv("FRAPPE_SENTRY_DSN"):
bootinfo.sentry_dsn = sentry_dsn