plainx-sentry 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from .middleware import SentryMiddleware, SentryWorkerMiddleware
2
+
3
+ __all__ = ["SentryMiddleware", "SentryWorkerMiddleware"]
@@ -0,0 +1,20 @@
1
+ import sentry_sdk
2
+ from plain.packages import PackageConfig
3
+ from plain.runtime import settings
4
+
5
+
6
+ class PlainxSentryConfig(PackageConfig):
7
+ name = "plainx.sentry"
8
+ label = "plainxsentry"
9
+
10
+ def ready(self):
11
+ if settings.SENTRY_DSN and settings.SENTRY_AUTO_INIT:
12
+ sentry_sdk.init(
13
+ settings.SENTRY_DSN,
14
+ release=settings.SENTRY_RELEASE,
15
+ environment=settings.SENTRY_ENVIRONMENT,
16
+ send_default_pii=settings.SENTRY_PII_ENABLED,
17
+ traces_sample_rate=settings.SENTRY_TRACES_SAMPLE_RATE,
18
+ profiles_sample_rate=settings.SENTRY_PROFILES_SAMPLE_RATE,
19
+ **settings.SENTRY_INIT_KWARGS,
20
+ )
@@ -0,0 +1,16 @@
1
+ from os import environ
2
+
3
+ SENTRY_AUTO_INIT: bool = True
4
+ SENTRY_INIT_KWARGS: dict = {}
5
+ SENTRY_PII_ENABLED: bool = True
6
+
7
+ # Re-use the standard Sentry env vars
8
+ SENTRY_DSN: str = environ.get("SENTRY_DSN", "")
9
+ SENTRY_RELEASE: str = environ.get("SENTRY_RELEASE", "")
10
+ SENTRY_ENVIRONMENT: str = environ.get("SENTRY_ENVIRONMENT", "production")
11
+
12
+ # These aren't built in as Sentry env vars?
13
+ SENTRY_TRACES_SAMPLE_RATE: float = float(environ.get("SENTRY_TRACES_SAMPLE_RATE", 0.0))
14
+ SENTRY_PROFILES_SAMPLE_RATE: float = float(
15
+ environ.get("SENTRY_PROFILES_SAMPLE_RATE", 0.0)
16
+ )
plainx/sentry/jinja.py ADDED
@@ -0,0 +1,59 @@
1
+ import sentry_sdk
2
+ from plain.runtime import settings
3
+ from plain.templates.jinja.extensions import InclusionTagExtension
4
+
5
+
6
+ class SentryJSExtension(InclusionTagExtension):
7
+ tags = {"sentry_js"}
8
+ template_name = "sentry/js.html"
9
+
10
+ def get_context(self, context, *args, **kwargs):
11
+ if not settings.SENTRY_DSN:
12
+ return {}
13
+
14
+ sentry_public_key = settings.SENTRY_DSN.split("//")[1].split("@")[0]
15
+
16
+ sentry_context = {
17
+ "sentry_public_key": sentry_public_key,
18
+ "sentry_init": {
19
+ "release": settings.SENTRY_RELEASE,
20
+ "environment": settings.SENTRY_ENVIRONMENT,
21
+ "sendDefaultPii": bool(settings.SENTRY_PII_ENABLED),
22
+ },
23
+ }
24
+
25
+ if "request" in context:
26
+ # Use request.user by default (avoids accidental "user" variable confusion)
27
+ user = getattr(context["request"], "user", None)
28
+ else:
29
+ # Get user directly if no request (like in server error context)
30
+ user = context.get("user", None)
31
+
32
+ if user:
33
+ sentry_context["sentry_init"]["initialScope"] = {"user": {"id": user.id}}
34
+ if settings.SENTRY_PII_ENABLED:
35
+ if email := getattr(user, "email", None):
36
+ sentry_context["sentry_init"]["initialScope"]["user"][
37
+ "email"
38
+ ] = email
39
+ if username := getattr(user, "username", None):
40
+ sentry_context["sentry_init"]["initialScope"]["user"][
41
+ "username"
42
+ ] = username
43
+
44
+ return sentry_context
45
+
46
+
47
+ class SentryFeedbackExtension(SentryJSExtension):
48
+ tags = {"sentry_feedback"}
49
+
50
+ def get_context(self, context, *args, **kwargs):
51
+ context = super().get_context(context, *args, **kwargs)
52
+ context["sentry_dialog_event_id"] = sentry_sdk.last_event_id()
53
+ return context
54
+
55
+
56
+ extensions = [
57
+ SentryJSExtension,
58
+ SentryFeedbackExtension,
59
+ ]
@@ -0,0 +1,146 @@
1
+ import sentry_sdk
2
+ from plain.runtime import settings
3
+ from sentry_sdk.tracing import TRANSACTION_SOURCE_TASK
4
+ from sentry_sdk.utils import capture_internal_exceptions
5
+
6
+ try:
7
+ from plain.models.db import connection
8
+ except ImportError:
9
+ connection = None
10
+
11
+ import logging
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def trace_db(execute, sql, params, many, context):
17
+ with sentry_sdk.start_span(op="db", description=sql) as span:
18
+ # Mostly borrowed from the Sentry Django integration...
19
+ data = {
20
+ "db.params": params,
21
+ "db.executemany": many,
22
+ "db.system": connection.vendor,
23
+ "db.name": connection.settings_dict.get("NAME"),
24
+ "db.user": connection.settings_dict.get("USER"),
25
+ "server.address": connection.settings_dict.get("HOST"),
26
+ "server.port": connection.settings_dict.get("PORT"),
27
+ }
28
+
29
+ sentry_sdk.add_breadcrumb(message=sql, category="query", data=data)
30
+
31
+ for k, v in data.items():
32
+ span.set_data(k, v)
33
+
34
+ result = execute(sql, params, many, context)
35
+
36
+ return result
37
+
38
+
39
+ class SentryMiddleware:
40
+ def __init__(self, get_response):
41
+ self.get_response = get_response
42
+
43
+ def __call__(self, request):
44
+ def event_processor(event, hint):
45
+ # request gets attached directly to an event,
46
+ # not necessarily in the "context"
47
+ request_info = event.setdefault("request", {})
48
+ request_info["url"] = request.build_absolute_uri()
49
+ request_info["method"] = request.method
50
+ request_info["query_string"] = request.META.get("QUERY_STRING", "")
51
+ # Headers and env need some PII filtering, ideally,
52
+ # among other filters... similar for GET/POST data?
53
+ # request_info["headers"] = dict(request.headers)
54
+ try:
55
+ request_info["data"] = request.body.decode("utf-8")
56
+ except Exception:
57
+ pass
58
+
59
+ if user := getattr(request, "user", None):
60
+ event["user"] = {"id": str(user.pk)}
61
+ if settings.SENTRY_PII_ENABLED:
62
+ if email := getattr(user, "email", None):
63
+ event["user"]["email"] = email
64
+ if username := getattr(user, "username", None):
65
+ event["user"]["username"] = username
66
+
67
+ return event
68
+
69
+ with sentry_sdk.configure_scope() as scope:
70
+ # Reset the scope (and breadcrumbs) for each request
71
+ scope.clear()
72
+ scope.add_event_processor(event_processor)
73
+
74
+ # Sentry's Django integration patches the WSGIHandler.
75
+ # We could make our own WSGIHandler and patch it or call it directly from gunicorn,
76
+ # but putting our middleware at the top of MIDDLEWARE is pretty close and easier.
77
+ with sentry_sdk.start_transaction(
78
+ op="http.server", name=request.path_info
79
+ ) as transaction:
80
+ if connection:
81
+ # Also get spans for db queries
82
+ with connection.execute_wrapper(trace_db):
83
+ response = self.get_response(request)
84
+ else:
85
+ # No db presumably
86
+ response = self.get_response(request)
87
+
88
+ if resolver_match := getattr(request, "resolver_match", None):
89
+ # Rename the transaction using a pattern,
90
+ # and attach other url/views tags we can use to filter
91
+ transaction.name = f"route:{resolver_match.route}"
92
+ transaction.set_tag("url_namespace", resolver_match.namespace)
93
+ transaction.set_tag("url_name", resolver_match.url_name)
94
+ transaction.set_tag("view_name", resolver_match.view_name)
95
+ transaction.set_tag("view_class", resolver_match._func_path)
96
+ # Don't need to filter on this, but do want the context to view
97
+ transaction.set_context(
98
+ "url_params",
99
+ {
100
+ "args": resolver_match.args,
101
+ "kwargs": resolver_match.kwargs,
102
+ },
103
+ )
104
+
105
+ transaction.set_http_status(response.status_code)
106
+
107
+ return response
108
+
109
+
110
+ class SentryWorkerMiddleware:
111
+ def __init__(self, run_job):
112
+ self.run_job = run_job
113
+
114
+ def __call__(self, job):
115
+ def event_processor(event, hint):
116
+ with capture_internal_exceptions():
117
+ # Attach it directly to any events
118
+ extra = event.setdefault("extra", {})
119
+ extra["plain.worker"] = {"job": job.as_json()}
120
+ return event
121
+
122
+ with sentry_sdk.configure_scope() as scope:
123
+ # Reset the scope (and breadcrumbs) for each job
124
+ scope.clear()
125
+ scope.add_event_processor(event_processor)
126
+
127
+ with sentry_sdk.start_transaction(
128
+ op="plain.worker.job",
129
+ name=f"job:{job.job_class}",
130
+ source=TRANSACTION_SOURCE_TASK,
131
+ ) as transaction:
132
+ if connection:
133
+ # Also get spans for db queries
134
+ with connection.execute_wrapper(trace_db):
135
+ job_result = self.run_job(job)
136
+ else:
137
+ # No db presumably
138
+ job_result = self.run_job(job)
139
+
140
+ with capture_internal_exceptions():
141
+ # Don't need to filter on this, but do want the context to view
142
+ transaction.set_context("job", job.as_json())
143
+
144
+ transaction.set_status("ok")
145
+
146
+ return job_result
@@ -0,0 +1,13 @@
1
+ {% if sentry_public_key|default("") -%}
2
+ <script src="https://js.sentry-cdn.com/{{ sentry_public_key }}.min.js" crossorigin="anonymous"></script>
3
+ {{ sentry_init|json_script("sentry_init") }}
4
+ <script>
5
+ var sentryInit = JSON.parse(document.getElementById("sentry_init").textContent);
6
+ Sentry.onLoad(function() {
7
+ Sentry.init(sentryInit);
8
+ });
9
+ {% if sentry_dialog_event_id|default("") -%}
10
+ Sentry.showReportDialog({ eventId: "{{ sentry_dialog_event_id }}" });
11
+ {%- endif %}
12
+ </script>
13
+ {%- endif %}
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.1
2
+ Name: plainx-sentry
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: Dave Gaeddert
6
+ Author-email: dave.gaeddert@dropseed.dev
7
+ Requires-Python: >=3.11,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Requires-Dist: sentry-sdk (>=1.8.0,<2.0.0)
@@ -0,0 +1,9 @@
1
+ plainx/sentry/__init__.py,sha256=Klz3mH_itM2RXzJ_sWWJRIoZcS0jCRxWrhVVIPsG5FY,123
2
+ plainx/sentry/config.py,sha256=pxvKwy6V7aYmZLe2tA7DiQF84R1ovvBMR0rE0CVFT1Y,716
3
+ plainx/sentry/default_settings.py,sha256=DNX6OQ0-BqfjyWuszW7JnZhiDn0tGtS3Fa2W7VbTIF8,564
4
+ plainx/sentry/jinja.py,sha256=hI6g65GQpaP_-LesTiXINeH7YyNdOYRJUSMQ1pnjQfA,2003
5
+ plainx/sentry/middleware.py,sha256=tw2ZMBRfH7eze0ZDqxo-meLSOaD2lVUSBYC99z5izZs,5597
6
+ plainx/sentry/templates/sentry/js.html,sha256=aOjWAwuUaecGbC-5yZWP1aaGyYAcM-GK0dru-4VOYoE,491
7
+ plainx_sentry-0.1.0.dist-info/METADATA,sha256=sNoU_R76H5uYQVKLhG0HrxEib-xPAPOa58Ie5Y1HQNI,352
8
+ plainx_sentry-0.1.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
9
+ plainx_sentry-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.8.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any