plainx-sentry 0.10.1__tar.gz → 0.11.0__tar.gz
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.
- plainx_sentry-0.11.0/.claude/rules/plain-code.md +33 -0
- plainx_sentry-0.11.0/.claude/rules/plain-models.md +30 -0
- plainx_sentry-0.11.0/.claude/rules/plain-templates.md +27 -0
- plainx_sentry-0.11.0/.claude/rules/plain-test.md +13 -0
- plainx_sentry-0.11.0/.claude/rules/plain.md +101 -0
- plainx_sentry-0.11.0/.claude/skills/plain-install/SKILL.md +26 -0
- plainx_sentry-0.11.0/.claude/skills/plain-upgrade/SKILL.md +35 -0
- plainx_sentry-0.11.0/.github/workflows/release.yml +32 -0
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/PKG-INFO +23 -2
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/README.md +21 -0
- plainx_sentry-0.11.0/plainx/sentry/CHANGELOG.md +24 -0
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/plainx/sentry/config.py +1 -1
- plainx_sentry-0.11.0/plainx/sentry/middleware.py +73 -0
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/plainx/sentry/templates.py +13 -5
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/pyproject.toml +3 -3
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/tests/test_sentry.py +54 -24
- plainx_sentry-0.11.0/uv.lock +739 -0
- plainx_sentry-0.10.1/plainx/sentry/CHANGELOG.md +0 -11
- plainx_sentry-0.10.1/uv.lock +0 -1003
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/.gitignore +0 -0
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/plainx/sentry/__init__.py +0 -0
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/plainx/sentry/default_settings.py +0 -0
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/plainx/sentry/templates/sentry/js.html +0 -0
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/scripts/install +0 -0
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/scripts/release +0 -0
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/scripts/test +0 -0
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/tests/settings.py +0 -0
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/tests/templates/index.html +0 -0
- {plainx_sentry-0.10.1 → plainx_sentry-0.11.0}/tests/urls.py +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Code Quality
|
|
7
|
+
|
|
8
|
+
## Fix Formatting and Linting
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
uv run plain fix [path]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Automatically fixes formatting and linting issues using ruff and biome.
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
|
|
18
|
+
- `--unsafe-fixes` - Apply ruff unsafe fixes
|
|
19
|
+
- `--add-noqa` - Add noqa comments to suppress errors
|
|
20
|
+
|
|
21
|
+
## Check Without Fixing
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
uv run plain code check [path]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Runs ruff, ty (type checking), biome, and annotation coverage checks without auto-fixing.
|
|
28
|
+
|
|
29
|
+
## Code Style
|
|
30
|
+
|
|
31
|
+
- Add `from __future__ import annotations` at the top of Python files
|
|
32
|
+
- Keep imports at the top of the file unless avoiding circular imports
|
|
33
|
+
- Don't include args/returns in docstrings if already type annotated
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Database & Models
|
|
2
|
+
|
|
3
|
+
## Creating Migrations
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
uv run plain makemigrations
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Only write migrations by hand if they are custom data migrations.
|
|
10
|
+
|
|
11
|
+
## Running Migrations
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
uv run plain migrate --backup
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The `--backup` flag creates a database backup before applying migrations.
|
|
18
|
+
|
|
19
|
+
Run `uv run plain docs models --source` for detailed model and migration documentation.
|
|
20
|
+
|
|
21
|
+
## Querying
|
|
22
|
+
|
|
23
|
+
Use `Model.query` to build querysets:
|
|
24
|
+
|
|
25
|
+
- `User.query.all()`
|
|
26
|
+
- `User.query.filter(is_active=True)`
|
|
27
|
+
- `User.query.get(pk=1)`
|
|
28
|
+
- `User.query.exclude(role="admin")`
|
|
29
|
+
|
|
30
|
+
Run `uv run plain docs models --api` for the full query API.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.html"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Templates
|
|
7
|
+
|
|
8
|
+
## Long Lines
|
|
9
|
+
|
|
10
|
+
When an HTML tag has many attributes or a long class list, break it into multiple lines with each attribute indented. The closing `>` goes on its own line.
|
|
11
|
+
|
|
12
|
+
**Good:**
|
|
13
|
+
|
|
14
|
+
```html
|
|
15
|
+
<div
|
|
16
|
+
class="flex items-center justify-between gap-4 rounded-lg border bg-white p-4 shadow-sm"
|
|
17
|
+
data-active="{{ is_active }}"
|
|
18
|
+
>
|
|
19
|
+
...
|
|
20
|
+
</div>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Avoid:**
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<div class="flex items-center justify-between gap-4 rounded-lg border bg-white p-4 shadow-sm" data-active="{{ is_active }}">...</div>
|
|
27
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
uv run plain test [pytest options]
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
- `uv run plain test` - Run all tests
|
|
8
|
+
- `uv run plain test -k test_name` - Filter by test name
|
|
9
|
+
- `uv run plain test --pdb` - Drop into debugger on failure
|
|
10
|
+
- `uv run plain test -x` - Stop on first failure
|
|
11
|
+
- `uv run plain test -v` - Verbose output
|
|
12
|
+
|
|
13
|
+
Use pytest fixtures and conventions. Place tests in `tests/` directory. Use `plain.test.Client` for HTTP request testing.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Plain Framework
|
|
2
|
+
|
|
3
|
+
Plain is a Python web framework.
|
|
4
|
+
|
|
5
|
+
- Always use `uv run` to execute commands — never use bare `python` or `plain` directly.
|
|
6
|
+
- Plain is a Django fork but has different APIs — never assume Django patterns will work.
|
|
7
|
+
- When unsure about an API or something doesn't work, run `uv run plain docs <package>` first. Add `--api` if you need the full API surface.
|
|
8
|
+
- Use the `/plain-install` skill to add new Plain packages.
|
|
9
|
+
- Use the `/plain-upgrade` skill to upgrade Plain packages.
|
|
10
|
+
|
|
11
|
+
## Key Differences from Django
|
|
12
|
+
|
|
13
|
+
Claude's training data contains a lot of Django code. These are the most common patterns that differ in Plain:
|
|
14
|
+
|
|
15
|
+
- **Querysets**: Use `Model.query` not `Model.objects` (e.g., `User.query.filter(is_active=True)`)
|
|
16
|
+
- **Field types**: Import from `plain.models.types` not `plain.models.fields`
|
|
17
|
+
- **Templates**: Plain uses Jinja2, not Django's template engine. Most syntax is similar but filters use `|` with function call syntax (e.g., `{{ name|title }}` works, but custom filters differ)
|
|
18
|
+
- **URLs**: Use `Router` with `urls` list, not Django's `urlpatterns`
|
|
19
|
+
- **Tests**: Use `plain.test.Client`, not `django.test.Client`
|
|
20
|
+
- **Settings**: Use `plain.runtime.settings`, not `django.conf.settings`
|
|
21
|
+
|
|
22
|
+
When in doubt, run `uv run plain docs <package> --api` to check the actual API.
|
|
23
|
+
|
|
24
|
+
## Documentation
|
|
25
|
+
|
|
26
|
+
Run `uv run plain docs --list` to see all official packages (installed and uninstalled) with descriptions.
|
|
27
|
+
Run `uv run plain docs <package>` for markdown documentation (installed packages only).
|
|
28
|
+
Run `uv run plain docs <package> --api` for the symbolicated API surface.
|
|
29
|
+
For uninstalled packages, the CLI shows the install command and an online docs URL.
|
|
30
|
+
|
|
31
|
+
Online docs URL pattern: `https://plainframework.com/docs/<pip-name>/<module/path>/README.md`
|
|
32
|
+
Example: `https://plainframework.com/docs/plain-models/plain/models/README.md`
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
|
|
36
|
+
- `uv run plain docs models` - Models and database docs
|
|
37
|
+
- `uv run plain docs models --api` - Models API surface
|
|
38
|
+
- `uv run plain docs templates` - Jinja2 templates
|
|
39
|
+
- `uv run plain docs assets` - Static assets
|
|
40
|
+
|
|
41
|
+
### All official packages
|
|
42
|
+
|
|
43
|
+
- **plain** — Web framework core
|
|
44
|
+
- **plain-admin** — Backend admin interface
|
|
45
|
+
- **plain-api** — Class-based API views
|
|
46
|
+
- **plain-auth** — User authentication and authorization
|
|
47
|
+
- **plain-cache** — Database-backed cache with optional expiration
|
|
48
|
+
- **plain-code** — Preconfigured code formatting and linting
|
|
49
|
+
- **plain-dev** — Local development server with auto-reload
|
|
50
|
+
- **plain-elements** — HTML template components
|
|
51
|
+
- **plain-email** — Send email
|
|
52
|
+
- **plain-esbuild** — Build JavaScript with esbuild
|
|
53
|
+
- **plain-flags** — Feature flags via database models
|
|
54
|
+
- **plain-htmx** — HTMX integration for templates and views
|
|
55
|
+
- **plain-jobs** — Background jobs with a database-driven queue
|
|
56
|
+
- **plain-loginlink** — Link-based authentication
|
|
57
|
+
- **plain-models** — Model data and store it in a database
|
|
58
|
+
- **plain-oauth** — OAuth provider login
|
|
59
|
+
- **plain-observer** — On-page telemetry and observability
|
|
60
|
+
- **plain-pages** — Serve static pages, markdown, and assets
|
|
61
|
+
- **plain-pageviews** — Client-side pageview tracking
|
|
62
|
+
- **plain-passwords** — Password authentication
|
|
63
|
+
- **plain-pytest** — Test with pytest
|
|
64
|
+
- **plain-redirection** — URL redirection with admin and logging
|
|
65
|
+
- **plain-scan** — Test for production best practices
|
|
66
|
+
- **plain-sessions** — Database-backed sessions
|
|
67
|
+
- **plain-start** — Bootstrap a new project from templates
|
|
68
|
+
- **plain-support** — Support forms for your application
|
|
69
|
+
- **plain-tailwind** — Tailwind CSS without JavaScript or npm
|
|
70
|
+
- **plain-toolbar** — Debug toolbar
|
|
71
|
+
- **plain-tunnel** — Remote access to local dev server
|
|
72
|
+
- **plain-vendor** — Vendor CDN scripts and styles
|
|
73
|
+
|
|
74
|
+
## Shell
|
|
75
|
+
|
|
76
|
+
`uv run plain shell` opens an interactive Python shell with Plain configured and database access.
|
|
77
|
+
|
|
78
|
+
Run a one-off command:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
uv run plain shell -c "from app.users.models import User; print(User.query.count())"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Run a script:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
uv run plain run script.py
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## HTTP Requests
|
|
91
|
+
|
|
92
|
+
Use `uv run plain request` to make test HTTP requests against the dev database.
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
uv run plain request /path
|
|
96
|
+
uv run plain request /path --user 1
|
|
97
|
+
uv run plain request /path --header "Accept: application/json"
|
|
98
|
+
uv run plain request /path --method POST --data '{"key": "value"}'
|
|
99
|
+
uv run plain request /path --no-body # Headers only
|
|
100
|
+
uv run plain request /path --no-headers # Body only
|
|
101
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plain-install
|
|
3
|
+
description: Installs Plain packages and guides through setup steps. Use when adding new packages to a project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Install Plain Packages
|
|
7
|
+
|
|
8
|
+
## 1. Install the package(s)
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
uv run plain install <package-name> [additional-packages...]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 2. Complete setup for each package
|
|
15
|
+
|
|
16
|
+
1. Run `uv run plain docs <package>` and read the installation instructions
|
|
17
|
+
2. If the docs indicate it's a dev tool, move it: `uv remove <package> && uv add <package> --dev`
|
|
18
|
+
3. Complete any code modifications from the installation instructions
|
|
19
|
+
|
|
20
|
+
## Guidelines
|
|
21
|
+
|
|
22
|
+
- DO NOT commit any changes
|
|
23
|
+
- Report back with:
|
|
24
|
+
- Whether setup completed successfully
|
|
25
|
+
- Any manual steps the user needs to complete
|
|
26
|
+
- Any issues or errors encountered
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plain-upgrade
|
|
3
|
+
description: Upgrades Plain packages and applies required migration changes. Use when updating to newer package versions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Upgrade Plain Packages
|
|
7
|
+
|
|
8
|
+
## 1. Run the upgrade
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
uv run plain upgrade [package-names...]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This will show which packages were upgraded (e.g., `plain-models: 0.1.0 -> 0.2.0`).
|
|
15
|
+
|
|
16
|
+
## 2. Apply code changes for each upgraded package
|
|
17
|
+
|
|
18
|
+
For each package that was upgraded:
|
|
19
|
+
|
|
20
|
+
1. Run `uv run plain changelog <package> --from <old-version> --to <new-version>`
|
|
21
|
+
2. Read the "Upgrade instructions" section
|
|
22
|
+
3. If it says "No changes required", skip to next package
|
|
23
|
+
4. Apply any required code changes
|
|
24
|
+
|
|
25
|
+
## 3. Validate
|
|
26
|
+
|
|
27
|
+
1. Run `uv run plain fix` to fix formatting
|
|
28
|
+
2. Run `uv run plain preflight` to validate configuration
|
|
29
|
+
|
|
30
|
+
## Guidelines
|
|
31
|
+
|
|
32
|
+
- Process ALL packages before testing
|
|
33
|
+
- DO NOT commit any changes
|
|
34
|
+
- Keep code changes minimal and focused
|
|
35
|
+
- Report any issues or conflicts encountered
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
release:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v5
|
|
16
|
+
- run: uv python install
|
|
17
|
+
|
|
18
|
+
# https://docs.pypi.org/trusted-publishers/using-a-publisher/
|
|
19
|
+
- name: Mint API token
|
|
20
|
+
id: mint-token
|
|
21
|
+
run: |
|
|
22
|
+
resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
|
|
23
|
+
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi")
|
|
24
|
+
oidc_token=$(jq -r '.value' <<< "${resp}")
|
|
25
|
+
resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}")
|
|
26
|
+
api_token=$(jq -r '.token' <<< "${resp}")
|
|
27
|
+
echo "::add-mask::${api_token}"
|
|
28
|
+
echo "api-token=${api_token}" >> "${GITHUB_OUTPUT}"
|
|
29
|
+
|
|
30
|
+
- run: uv build && uv publish
|
|
31
|
+
env:
|
|
32
|
+
UV_PUBLISH_TOKEN: ${{ steps.mint-token.outputs.api-token }}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plainx-sentry
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Author-email: Dave Gaeddert <dave.gaeddert@gmail.com>
|
|
5
|
-
Requires-Python: >=3.
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
6
|
Requires-Dist: plain-auth>=0.16.0
|
|
7
7
|
Requires-Dist: plain-sessions>=0.27.0
|
|
8
8
|
Requires-Dist: sentry-sdk[opentelemetry]>=2.24.0
|
|
@@ -47,6 +47,27 @@ In Heroku, for example:
|
|
|
47
47
|
heroku config:set SENTRY_DSN=<your-DSN>
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## User and request context
|
|
51
|
+
|
|
52
|
+
To attach user and request context to errors, add the middleware:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# settings.py
|
|
56
|
+
MIDDLEWARE = [
|
|
57
|
+
...
|
|
58
|
+
"plain.sessions.middleware.SessionMiddleware",
|
|
59
|
+
"plainx.sentry.middleware.SentryMiddleware", # After SessionMiddleware
|
|
60
|
+
...
|
|
61
|
+
]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This attaches to errors:
|
|
65
|
+
|
|
66
|
+
- **User context**: ID, email, username
|
|
67
|
+
- **Request context**: URL, method, query string, headers, cookies
|
|
68
|
+
|
|
69
|
+
Email, username, headers, and cookies require `SENTRY_PII_ENABLED=True` (the default).
|
|
70
|
+
|
|
50
71
|
## Configuration
|
|
51
72
|
|
|
52
73
|
[Look at the `default_settings.py` for all available settings.](./plainx/sentry/default_settings.py)
|
|
@@ -37,6 +37,27 @@ In Heroku, for example:
|
|
|
37
37
|
heroku config:set SENTRY_DSN=<your-DSN>
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
## User and request context
|
|
41
|
+
|
|
42
|
+
To attach user and request context to errors, add the middleware:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# settings.py
|
|
46
|
+
MIDDLEWARE = [
|
|
47
|
+
...
|
|
48
|
+
"plain.sessions.middleware.SessionMiddleware",
|
|
49
|
+
"plainx.sentry.middleware.SentryMiddleware", # After SessionMiddleware
|
|
50
|
+
...
|
|
51
|
+
]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This attaches to errors:
|
|
55
|
+
|
|
56
|
+
- **User context**: ID, email, username
|
|
57
|
+
- **Request context**: URL, method, query string, headers, cookies
|
|
58
|
+
|
|
59
|
+
Email, username, headers, and cookies require `SENTRY_PII_ENABLED=True` (the default).
|
|
60
|
+
|
|
40
61
|
## Configuration
|
|
41
62
|
|
|
42
63
|
[Look at the `default_settings.py` for all available settings.](./plainx/sentry/default_settings.py)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# plainx-sentry changelog
|
|
2
|
+
|
|
3
|
+
## [0.11.0](https://github.com/davegaeddert/plainx-sentry/releases/v0.11.0) (2026-02-05)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- Added `SentryMiddleware` for automatic request and user context on Sentry events ([950df07](https://github.com/davegaeddert/plainx-sentry/commit/950df07))
|
|
8
|
+
- Now requires Python 3.13+ ([dbfb0d7](https://github.com/davegaeddert/plainx-sentry/commit/dbfb0d7))
|
|
9
|
+
- Added type annotations throughout the codebase ([dbfb0d7](https://github.com/davegaeddert/plainx-sentry/commit/dbfb0d7))
|
|
10
|
+
|
|
11
|
+
### Upgrade instructions
|
|
12
|
+
|
|
13
|
+
- Ensure you are running Python 3.13 or higher
|
|
14
|
+
- Add `SentryMiddleware` to your middleware stack after `SessionMiddleware` for automatic request/user context
|
|
15
|
+
|
|
16
|
+
## [0.7.0](https://github.com/davegaeddert/plainx-sentry/releases/plainx-sentry@0.7.0) (2025-07-19)
|
|
17
|
+
|
|
18
|
+
### What's changed
|
|
19
|
+
|
|
20
|
+
- Middleware was removed in favor of using the OpenTelemetry integration and new otel instrumentation in Plain.
|
|
21
|
+
|
|
22
|
+
### Upgrade instructions
|
|
23
|
+
|
|
24
|
+
- Remove `SentryMiddleware` and `SentryWorkerMiddleware` from your `app/settings.py`.
|
|
@@ -11,7 +11,7 @@ from sentry_sdk.integrations.opentelemetry import SentryPropagator, SentrySpanPr
|
|
|
11
11
|
class PlainxSentryConfig(PackageConfig):
|
|
12
12
|
label = "plainxsentry"
|
|
13
13
|
|
|
14
|
-
def ready(self):
|
|
14
|
+
def ready(self) -> None:
|
|
15
15
|
if settings.SENTRY_DSN and settings.SENTRY_AUTO_INIT:
|
|
16
16
|
sentry_sdk.init(
|
|
17
17
|
settings.SENTRY_DSN,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import sentry_sdk
|
|
4
|
+
from plain.auth import get_request_user
|
|
5
|
+
from plain.http import HttpMiddleware
|
|
6
|
+
from plain.http.request import Request
|
|
7
|
+
from plain.http.response import Response
|
|
8
|
+
from sentry_sdk.scope import should_send_default_pii
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _build_request_info(request: Request) -> dict[str, Any]:
|
|
12
|
+
"""Build request context dictionary for Sentry events."""
|
|
13
|
+
try:
|
|
14
|
+
url = request.build_absolute_uri()
|
|
15
|
+
except Exception:
|
|
16
|
+
url = request.path
|
|
17
|
+
|
|
18
|
+
request_info = {
|
|
19
|
+
"method": request.method,
|
|
20
|
+
"url": url,
|
|
21
|
+
"query_string": request.query_string,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if should_send_default_pii():
|
|
25
|
+
request_info["headers"] = dict(request.headers)
|
|
26
|
+
request_info["cookies"] = dict(request.cookies)
|
|
27
|
+
|
|
28
|
+
return request_info
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _build_user_info(user: Any) -> dict[str, Any]:
|
|
32
|
+
"""Build user context dictionary for Sentry events."""
|
|
33
|
+
user_info = {"id": str(user.id)}
|
|
34
|
+
|
|
35
|
+
if should_send_default_pii():
|
|
36
|
+
if email := getattr(user, "email", None):
|
|
37
|
+
user_info["email"] = email
|
|
38
|
+
if username := getattr(user, "username", None):
|
|
39
|
+
user_info["username"] = username
|
|
40
|
+
|
|
41
|
+
return user_info
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SentryMiddleware(HttpMiddleware):
|
|
45
|
+
"""
|
|
46
|
+
Middleware that registers a Sentry event processor for request/user context.
|
|
47
|
+
|
|
48
|
+
Add this to your MIDDLEWARE setting after SessionMiddleware:
|
|
49
|
+
|
|
50
|
+
MIDDLEWARE = [
|
|
51
|
+
...
|
|
52
|
+
"plain.sessions.middleware.SessionMiddleware",
|
|
53
|
+
"plainx.sentry.middleware.SentryMiddleware",
|
|
54
|
+
...
|
|
55
|
+
]
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def process_request(self, request: Request) -> Response:
|
|
59
|
+
def event_processor(
|
|
60
|
+
event: dict[str, Any], hint: dict[str, Any]
|
|
61
|
+
) -> dict[str, Any]:
|
|
62
|
+
if "request" not in event:
|
|
63
|
+
event["request"] = _build_request_info(request)
|
|
64
|
+
|
|
65
|
+
if "user" not in event:
|
|
66
|
+
user = get_request_user(request)
|
|
67
|
+
if user:
|
|
68
|
+
event["user"] = _build_user_info(user)
|
|
69
|
+
|
|
70
|
+
return event
|
|
71
|
+
|
|
72
|
+
sentry_sdk.get_current_scope().add_event_processor(event_processor)
|
|
73
|
+
return self.get_response(request)
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
1
3
|
import sentry_sdk
|
|
4
|
+
from jinja2.runtime import Context
|
|
2
5
|
from plain.auth import get_request_user
|
|
3
6
|
from plain.runtime import settings
|
|
4
7
|
from plain.templates import register_template_extension
|
|
@@ -10,7 +13,9 @@ class SentryJSExtension(InclusionTagExtension):
|
|
|
10
13
|
tags = {"sentry_js"}
|
|
11
14
|
template_name = "sentry/js.html"
|
|
12
15
|
|
|
13
|
-
def get_context(
|
|
16
|
+
def get_context(
|
|
17
|
+
self, context: Context, *args: Any, **kwargs: Any
|
|
18
|
+
) -> Context | dict[str, Any]:
|
|
14
19
|
if not settings.SENTRY_DSN:
|
|
15
20
|
return {}
|
|
16
21
|
|
|
@@ -45,7 +50,10 @@ class SentryJSExtension(InclusionTagExtension):
|
|
|
45
50
|
class SentryFeedbackExtension(SentryJSExtension):
|
|
46
51
|
tags = {"sentry_feedback"}
|
|
47
52
|
|
|
48
|
-
def get_context(
|
|
49
|
-
context
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
def get_context(
|
|
54
|
+
self, context: Context, *args: Any, **kwargs: Any
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
parent_result = super().get_context(context, *args, **kwargs)
|
|
57
|
+
result: dict[str, Any] = dict(parent_result)
|
|
58
|
+
result["sentry_dialog_event_id"] = sentry_sdk.last_event_id()
|
|
59
|
+
return result
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "plainx-sentry"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.11.0"
|
|
4
4
|
description = ""
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
7
7
|
{ name = "Dave Gaeddert", email = "dave.gaeddert@gmail.com" }
|
|
8
8
|
]
|
|
9
|
-
requires-python = ">=3.
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"plain-auth>=0.16.0",
|
|
12
12
|
"plain-sessions>=0.27.0",
|
|
@@ -26,7 +26,7 @@ packages = ["plainx"]
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
[tool.plain.code.biome]
|
|
29
|
-
|
|
29
|
+
enabled = false
|
|
30
30
|
[build-system]
|
|
31
31
|
requires = ["hatchling"]
|
|
32
32
|
build-backend = "hatchling.build"
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
1
3
|
import sentry_sdk
|
|
4
|
+
from jinja2 import Environment
|
|
5
|
+
from jinja2.runtime import Context
|
|
2
6
|
from plain.auth import get_user_model
|
|
3
7
|
from plain.auth.requests import set_request_user
|
|
4
8
|
from plain.views import TemplateView
|
|
5
9
|
|
|
6
|
-
from plainx.sentry.
|
|
10
|
+
from plainx.sentry.templates import SentryJSExtension
|
|
7
11
|
|
|
8
12
|
SENTRY_TEST_DSN = "https://publickey@1.ingest.sentry.io/1"
|
|
9
13
|
|
|
@@ -38,14 +42,21 @@ def test_sentry_tag(settings, rf):
|
|
|
38
42
|
def test_sentry_pii_enabled(settings, rf):
|
|
39
43
|
settings.SENTRY_DSN = SENTRY_TEST_DSN
|
|
40
44
|
settings.SENTRY_PII_ENABLED = True
|
|
45
|
+
settings.SENTRY_RELEASE = None
|
|
46
|
+
settings.SENTRY_ENVIRONMENT = "production"
|
|
41
47
|
|
|
42
48
|
request = rf.get("/")
|
|
43
|
-
|
|
49
|
+
request.csp_nonce = "test-nonce"
|
|
50
|
+
set_request_user(
|
|
51
|
+
request, get_user_model()(id=1, email="test@example.com", username="test")
|
|
52
|
+
)
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
extension = SentryJSExtension(cast(Environment, None))
|
|
55
|
+
result = extension.get_context(cast(Context, {"request": request}))
|
|
56
|
+
|
|
57
|
+
assert result == {
|
|
47
58
|
"sentry_public_key": "publickey",
|
|
48
|
-
"
|
|
59
|
+
"csp_nonce": "test-nonce",
|
|
49
60
|
"sentry_init": {
|
|
50
61
|
"release": None,
|
|
51
62
|
"environment": "production",
|
|
@@ -64,13 +75,18 @@ def test_sentry_pii_enabled(settings, rf):
|
|
|
64
75
|
def test_sentry_pii_enabled_without_user(settings, rf):
|
|
65
76
|
settings.SENTRY_DSN = SENTRY_TEST_DSN
|
|
66
77
|
settings.SENTRY_PII_ENABLED = True
|
|
78
|
+
settings.SENTRY_RELEASE = None
|
|
79
|
+
settings.SENTRY_ENVIRONMENT = "production"
|
|
67
80
|
|
|
68
81
|
request = rf.get("/")
|
|
82
|
+
request.csp_nonce = "test-nonce"
|
|
83
|
+
|
|
84
|
+
extension = SentryJSExtension(cast(Environment, None))
|
|
85
|
+
result = extension.get_context(cast(Context, {"request": request}))
|
|
69
86
|
|
|
70
|
-
assert
|
|
71
|
-
"sentry_js_enabled": True,
|
|
87
|
+
assert result == {
|
|
72
88
|
"sentry_public_key": "publickey",
|
|
73
|
-
"
|
|
89
|
+
"csp_nonce": "test-nonce",
|
|
74
90
|
"sentry_init": {
|
|
75
91
|
"release": None,
|
|
76
92
|
"environment": "production",
|
|
@@ -82,18 +98,30 @@ def test_sentry_pii_enabled_without_user(settings, rf):
|
|
|
82
98
|
def test_sentry_pii_disabled(settings, rf):
|
|
83
99
|
settings.SENTRY_DSN = SENTRY_TEST_DSN
|
|
84
100
|
settings.SENTRY_PII_ENABLED = False
|
|
101
|
+
settings.SENTRY_RELEASE = None
|
|
102
|
+
settings.SENTRY_ENVIRONMENT = "production"
|
|
85
103
|
|
|
86
104
|
request = rf.get("/")
|
|
87
|
-
|
|
105
|
+
request.csp_nonce = "test-nonce"
|
|
106
|
+
set_request_user(
|
|
107
|
+
request, get_user_model()(id=1, email="test@example.com", username="test")
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
extension = SentryJSExtension(cast(Environment, None))
|
|
111
|
+
result = extension.get_context(cast(Context, {"request": request}))
|
|
88
112
|
|
|
89
|
-
assert
|
|
90
|
-
"sentry_js_enabled": True,
|
|
113
|
+
assert result == {
|
|
91
114
|
"sentry_public_key": "publickey",
|
|
92
|
-
"
|
|
115
|
+
"csp_nonce": "test-nonce",
|
|
93
116
|
"sentry_init": {
|
|
94
117
|
"release": None,
|
|
95
118
|
"environment": "production",
|
|
96
119
|
"sendDefaultPii": False,
|
|
120
|
+
"initialScope": {
|
|
121
|
+
"user": {
|
|
122
|
+
"id": 1,
|
|
123
|
+
}
|
|
124
|
+
},
|
|
97
125
|
},
|
|
98
126
|
}
|
|
99
127
|
|
|
@@ -105,16 +133,26 @@ def test_sentry_release_env(settings, rf):
|
|
|
105
133
|
settings.SENTRY_ENVIRONMENT = "production"
|
|
106
134
|
|
|
107
135
|
request = rf.get("/")
|
|
108
|
-
|
|
136
|
+
request.csp_nonce = "test-nonce"
|
|
137
|
+
set_request_user(
|
|
138
|
+
request, get_user_model()(id=1, email="test@example.com", username="test")
|
|
139
|
+
)
|
|
109
140
|
|
|
110
|
-
|
|
111
|
-
|
|
141
|
+
extension = SentryJSExtension(cast(Environment, None))
|
|
142
|
+
result = extension.get_context(cast(Context, {"request": request}))
|
|
143
|
+
|
|
144
|
+
assert result == {
|
|
112
145
|
"sentry_public_key": "publickey",
|
|
113
|
-
"
|
|
146
|
+
"csp_nonce": "test-nonce",
|
|
114
147
|
"sentry_init": {
|
|
115
148
|
"release": "v1",
|
|
116
149
|
"environment": "production",
|
|
117
150
|
"sendDefaultPii": False,
|
|
151
|
+
"initialScope": {
|
|
152
|
+
"user": {
|
|
153
|
+
"id": 1,
|
|
154
|
+
}
|
|
155
|
+
},
|
|
118
156
|
},
|
|
119
157
|
}
|
|
120
158
|
|
|
@@ -128,11 +166,3 @@ def test_sentry_feedback_middleware(settings, client):
|
|
|
128
166
|
|
|
129
167
|
assert response.status_code == 500
|
|
130
168
|
assert b"Sentry.onLoad" in response.content
|
|
131
|
-
|
|
132
|
-
# # # Have to test the middleware directly
|
|
133
|
-
# middleware = SentryFeedbackMiddleware(get_response=lambda request: response)
|
|
134
|
-
# response = middleware(response.request)
|
|
135
|
-
|
|
136
|
-
# assert response.status_code == 500
|
|
137
|
-
# assert b"Sentry.onLoad" in response.content
|
|
138
|
-
# assert b"Sentry.showReportDialog" in response.content
|