plain.observer 0.11.2__py3-none-any.whl → 0.13.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.
Potentially problematic release.
This version of plain.observer might be problematic. Click here for more details.
- plain/observer/CHANGELOG.md +29 -0
- plain/observer/README.md +23 -0
- plain/observer/admin.py +4 -6
- plain/observer/assets/observer/observer.css +72 -0
- plain/observer/assets/observer/observer.js +69 -0
- plain/observer/default_settings.py +0 -1
- plain/observer/templates/observer/partials/log.html +1 -1
- plain/observer/templates/observer/partials/span.html +7 -3
- plain/observer/templates/observer/trace.html +3 -45
- plain/observer/templates/observer/trace_detail.html +2 -0
- plain/observer/templates/observer/trace_share.html +2 -0
- plain/observer/templates/observer/traces.html +3 -1
- plain/observer/templates/toolbar/observer.html +3 -3
- {plain_observer-0.11.2.dist-info → plain_observer-0.13.0.dist-info}/METADATA +24 -1
- {plain_observer-0.11.2.dist-info → plain_observer-0.13.0.dist-info}/RECORD +17 -15
- {plain_observer-0.11.2.dist-info → plain_observer-0.13.0.dist-info}/WHEEL +0 -0
- {plain_observer-0.11.2.dist-info → plain_observer-0.13.0.dist-info}/licenses/LICENSE +0 -0
plain/observer/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# plain-observer changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.0](https://github.com/dropseed/plain/releases/plain-observer@0.13.0) (2025-10-29)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- Inline JavaScript and CSS extracted to separate asset files for Content Security Policy (CSP) compatibility ([784f3dd](https://github.com/dropseed/plain/commit/784f3dd972))
|
|
8
|
+
- Added CSP nonce support to inline scripts for improved security ([784f3dd](https://github.com/dropseed/plain/commit/784f3dd972))
|
|
9
|
+
- Added comprehensive CSP configuration documentation in README, including required `frame-ancestors 'self'` directive for toolbar panel ([784f3dd](https://github.com/dropseed/plain/commit/784f3dd972))
|
|
10
|
+
- Span and log indentation now uses CSS classes with data attributes instead of inline styles ([784f3dd](https://github.com/dropseed/plain/commit/784f3dd972))
|
|
11
|
+
- Timeline bar positioning now uses CSS custom properties set via JavaScript instead of inline styles ([784f3dd](https://github.com/dropseed/plain/commit/784f3dd972))
|
|
12
|
+
- Copy share URL button now uses data attributes and event delegation instead of inline onclick handlers ([784f3dd](https://github.com/dropseed/plain/commit/784f3dd972))
|
|
13
|
+
- Toolbar iframe now uses HTML attributes instead of inline styles ([784f3dd](https://github.com/dropseed/plain/commit/784f3dd972))
|
|
14
|
+
|
|
15
|
+
### Upgrade instructions
|
|
16
|
+
|
|
17
|
+
- No changes required
|
|
18
|
+
|
|
19
|
+
## [0.12.0](https://github.com/dropseed/plain/releases/plain-observer@0.12.0) (2025-10-24)
|
|
20
|
+
|
|
21
|
+
### What's changed
|
|
22
|
+
|
|
23
|
+
- Admin viewsets now use `presets` instead of `displays` for predefined queryset filters ([0ecc60f](https://github.com/dropseed/plain/commit/0ecc60f19e))
|
|
24
|
+
- Removed `logger` field from Log admin interface for simplified display ([ae43138](https://github.com/dropseed/plain/commit/ae43138863))
|
|
25
|
+
- Removed `/admin/.*` from default ignored URL patterns, allowing admin pages to be traced ([daadf1a](https://github.com/dropseed/plain/commit/daadf1a53d))
|
|
26
|
+
|
|
27
|
+
### Upgrade instructions
|
|
28
|
+
|
|
29
|
+
- If you have custom admin viewsets using the `displays` attribute, rename it to `presets`
|
|
30
|
+
- If you reference the `display` property in custom admin code (e.g., `self.display`), rename it to `self.preset`
|
|
31
|
+
|
|
3
32
|
## [0.11.2](https://github.com/dropseed/plain/releases/plain-observer@0.11.2) (2025-10-20)
|
|
4
33
|
|
|
5
34
|
### What's changed
|
plain/observer/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
**On-page telemetry and observability tools for Plain.**
|
|
4
4
|
|
|
5
5
|
- [Installation](#installation)
|
|
6
|
+
- [Content Security Policy (CSP)](#content-security-policy-csp)
|
|
6
7
|
|
|
7
8
|
## Installation
|
|
8
9
|
|
|
@@ -44,3 +45,25 @@ plain migrate
|
|
|
44
45
|
```
|
|
45
46
|
|
|
46
47
|
After installation, Observer will automatically integrate with your application's toolbar (if using `plain.admin`). You can access the web interface at `/observer/traces/` or use the CLI commands to analyze traces.
|
|
48
|
+
|
|
49
|
+
## Content Security Policy (CSP)
|
|
50
|
+
|
|
51
|
+
If you're using a Content Security Policy (CSP), the Observer toolbar panel requires `frame-ancestors 'self'` to display trace information in an iframe.
|
|
52
|
+
|
|
53
|
+
Without this directive, the toolbar panel will fail to load with a CSP error: `"Refused to frame... because an ancestor violates the following Content Security Policy directive: 'frame-ancestors 'none'"`.
|
|
54
|
+
|
|
55
|
+
Example CSP configuration:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
def DEFAULT_RESPONSE_HEADERS(request):
|
|
59
|
+
nonce = request.csp_nonce
|
|
60
|
+
return {
|
|
61
|
+
"Content-Security-Policy": (
|
|
62
|
+
f"default-src 'self'; "
|
|
63
|
+
f"script-src 'self' 'nonce-{nonce}'; "
|
|
64
|
+
f"style-src 'self' 'nonce-{nonce}'; "
|
|
65
|
+
f"frame-ancestors 'self'; " # Required for Observer toolbar
|
|
66
|
+
# ... other directives
|
|
67
|
+
),
|
|
68
|
+
}
|
|
69
|
+
```
|
plain/observer/admin.py
CHANGED
|
@@ -53,7 +53,7 @@ class SpanViewset(AdminViewset):
|
|
|
53
53
|
]
|
|
54
54
|
queryset_order = ["-id"]
|
|
55
55
|
allow_global_search = False
|
|
56
|
-
|
|
56
|
+
presets = ["Parents only"]
|
|
57
57
|
search_fields = ["name", "span_id", "parent_id"]
|
|
58
58
|
actions = ["Delete"]
|
|
59
59
|
|
|
@@ -76,7 +76,7 @@ class SpanViewset(AdminViewset):
|
|
|
76
76
|
|
|
77
77
|
def get_initial_queryset(self) -> models.QuerySet:
|
|
78
78
|
queryset = super().get_initial_queryset()
|
|
79
|
-
if self.
|
|
79
|
+
if self.preset == "Parents only":
|
|
80
80
|
queryset = queryset.filter(parent_id="")
|
|
81
81
|
return queryset
|
|
82
82
|
|
|
@@ -93,15 +93,14 @@ class LogViewset(AdminViewset):
|
|
|
93
93
|
fields = [
|
|
94
94
|
"timestamp",
|
|
95
95
|
"level",
|
|
96
|
-
"logger",
|
|
97
96
|
"message",
|
|
98
97
|
"trace",
|
|
99
98
|
"span",
|
|
100
99
|
]
|
|
101
100
|
queryset_order = ["-timestamp"]
|
|
102
101
|
allow_global_search = False
|
|
103
|
-
search_fields = ["
|
|
104
|
-
filters = ["level"
|
|
102
|
+
search_fields = ["message", "level"]
|
|
103
|
+
filters = ["level"]
|
|
105
104
|
actions = ["Delete selected", "Delete all"]
|
|
106
105
|
|
|
107
106
|
def perform_action(self, action: str, target_ids: Sequence[int]) -> None:
|
|
@@ -118,7 +117,6 @@ class LogViewset(AdminViewset):
|
|
|
118
117
|
.only(
|
|
119
118
|
"timestamp",
|
|
120
119
|
"level",
|
|
121
|
-
"logger",
|
|
122
120
|
"message",
|
|
123
121
|
"span__span_id",
|
|
124
122
|
"trace__trace_id",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* Custom details arrow animation */
|
|
2
|
+
details[open] summary svg {
|
|
3
|
+
transform: rotate(90deg);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/* Apply padding based on span level - generated for levels 0-19 */
|
|
7
|
+
[data-span-level="0"] {
|
|
8
|
+
padding-left: 0rem;
|
|
9
|
+
}
|
|
10
|
+
[data-span-level="1"] {
|
|
11
|
+
padding-left: 1rem;
|
|
12
|
+
}
|
|
13
|
+
[data-span-level="2"] {
|
|
14
|
+
padding-left: 2rem;
|
|
15
|
+
}
|
|
16
|
+
[data-span-level="3"] {
|
|
17
|
+
padding-left: 3rem;
|
|
18
|
+
}
|
|
19
|
+
[data-span-level="4"] {
|
|
20
|
+
padding-left: 4rem;
|
|
21
|
+
}
|
|
22
|
+
[data-span-level="5"] {
|
|
23
|
+
padding-left: 5rem;
|
|
24
|
+
}
|
|
25
|
+
[data-span-level="6"] {
|
|
26
|
+
padding-left: 6rem;
|
|
27
|
+
}
|
|
28
|
+
[data-span-level="7"] {
|
|
29
|
+
padding-left: 7rem;
|
|
30
|
+
}
|
|
31
|
+
[data-span-level="8"] {
|
|
32
|
+
padding-left: 8rem;
|
|
33
|
+
}
|
|
34
|
+
[data-span-level="9"] {
|
|
35
|
+
padding-left: 9rem;
|
|
36
|
+
}
|
|
37
|
+
[data-span-level="10"] {
|
|
38
|
+
padding-left: 10rem;
|
|
39
|
+
}
|
|
40
|
+
[data-span-level="11"] {
|
|
41
|
+
padding-left: 11rem;
|
|
42
|
+
}
|
|
43
|
+
[data-span-level="12"] {
|
|
44
|
+
padding-left: 12rem;
|
|
45
|
+
}
|
|
46
|
+
[data-span-level="13"] {
|
|
47
|
+
padding-left: 13rem;
|
|
48
|
+
}
|
|
49
|
+
[data-span-level="14"] {
|
|
50
|
+
padding-left: 14rem;
|
|
51
|
+
}
|
|
52
|
+
[data-span-level="15"] {
|
|
53
|
+
padding-left: 15rem;
|
|
54
|
+
}
|
|
55
|
+
[data-span-level="16"] {
|
|
56
|
+
padding-left: 16rem;
|
|
57
|
+
}
|
|
58
|
+
[data-span-level="17"] {
|
|
59
|
+
padding-left: 17rem;
|
|
60
|
+
}
|
|
61
|
+
[data-span-level="18"] {
|
|
62
|
+
padding-left: 18rem;
|
|
63
|
+
}
|
|
64
|
+
[data-span-level="19"] {
|
|
65
|
+
padding-left: 19rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Apply positioning for timeline bars using CSS custom properties */
|
|
69
|
+
[data-span-id] {
|
|
70
|
+
left: var(--start, 0%);
|
|
71
|
+
width: var(--width, 100%);
|
|
72
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Observer JS - CSP-compliant timeline positioning and utilities
|
|
2
|
+
|
|
3
|
+
// Apply CSS custom properties for timeline positioning
|
|
4
|
+
function applyTimelinePositioning() {
|
|
5
|
+
document
|
|
6
|
+
.querySelectorAll("[data-start-percent][data-width-percent]")
|
|
7
|
+
.forEach((el) => {
|
|
8
|
+
const start = el.dataset.startPercent;
|
|
9
|
+
const width = el.dataset.widthPercent;
|
|
10
|
+
el.style.setProperty("--start", `${start}%`);
|
|
11
|
+
el.style.setProperty("--width", `${width}%`);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Apply on page load
|
|
16
|
+
document.addEventListener("DOMContentLoaded", applyTimelinePositioning);
|
|
17
|
+
|
|
18
|
+
// Re-apply after htmx swaps content
|
|
19
|
+
document.addEventListener("htmx:afterSwap", applyTimelinePositioning);
|
|
20
|
+
|
|
21
|
+
// Copy share URL to clipboard with visual feedback
|
|
22
|
+
async function copyShareUrl(button, traceId) {
|
|
23
|
+
try {
|
|
24
|
+
const shareUrl = button.getAttribute("data-share-url");
|
|
25
|
+
|
|
26
|
+
// Copy to clipboard
|
|
27
|
+
await navigator.clipboard.writeText(shareUrl);
|
|
28
|
+
|
|
29
|
+
// Show success feedback on button
|
|
30
|
+
const originalHTML = button.innerHTML;
|
|
31
|
+
button.innerHTML =
|
|
32
|
+
'<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 16 16"><path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/></svg>';
|
|
33
|
+
button.classList.remove("bg-emerald-700", "hover:bg-emerald-600");
|
|
34
|
+
button.classList.add("bg-green-600", "hover:bg-green-700");
|
|
35
|
+
|
|
36
|
+
// Also flash the URL text
|
|
37
|
+
const urlSpan = document.getElementById(`share-url-${traceId}`);
|
|
38
|
+
if (urlSpan) {
|
|
39
|
+
urlSpan.classList.add("text-green-400", "font-bold");
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
urlSpan.classList.remove("text-green-400", "font-bold");
|
|
42
|
+
}, 2000);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
button.innerHTML = originalHTML;
|
|
47
|
+
button.classList.remove("bg-green-600", "hover:bg-green-700");
|
|
48
|
+
button.classList.add("bg-emerald-700", "hover:bg-emerald-600");
|
|
49
|
+
}, 2000);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error("Failed to copy share URL:", error);
|
|
52
|
+
alert("Failed to copy share URL. See console for details.");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Set up event delegation for copy share URL buttons
|
|
57
|
+
function setupCopyShareUrlHandlers() {
|
|
58
|
+
document.addEventListener("click", (event) => {
|
|
59
|
+
const button = event.target.closest("[data-copy-share-url]");
|
|
60
|
+
if (button) {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
const traceId = button.getAttribute("data-trace-id");
|
|
63
|
+
copyShareUrl(button, traceId);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Initialize on page load
|
|
69
|
+
document.addEventListener("DOMContentLoaded", setupCopyShareUrlHandlers);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<div
|
|
1
|
+
<div data-span-level="{{ event.span_level }}" class="border-l border-white/10">
|
|
2
2
|
<div class="ml-px px-2 py-1 text-xs flex items-start space-x-2">
|
|
3
3
|
<div class="w-4 h-4 mr-2 flex items-center justify-center">
|
|
4
4
|
<svg class="w-3 h-3 transform transition-transform" fill="currentColor" viewBox="0 0 20 20">
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
{% set start_percent = (span_start_offset / trace.duration_ms() * 100) if trace.duration_ms() > 0 else 0 %}
|
|
3
3
|
{% set width_percent = (span.duration_ms() / trace.duration_ms() * 100) if trace.duration_ms() > 0 else 0 %}
|
|
4
4
|
|
|
5
|
-
<div
|
|
5
|
+
<div data-span-level="{{ event.span_level }}" class="border-l border-white/20">
|
|
6
6
|
<details class="rounded bg-white/5 min-w-[600px] ml-px">
|
|
7
7
|
<summary class="cursor-pointer hover:bg-white/10 transition-colors px-2 py-1 list-none [&::-webkit-details-marker]:hidden">
|
|
8
8
|
<div class="flex items-center">
|
|
@@ -48,12 +48,16 @@
|
|
|
48
48
|
data-[kind='INTERNAL']:bg-gray-500
|
|
49
49
|
bg-white/30"
|
|
50
50
|
data-kind="{{ span.kind }}"
|
|
51
|
-
|
|
51
|
+
data-span-id="{{ span.span_id }}"
|
|
52
|
+
data-start-percent="{{ start_percent }}"
|
|
53
|
+
data-width-percent="{{ width_percent }}"
|
|
52
54
|
title="{{ span.name }} - {{ span.duration_ms() }}ms">
|
|
53
55
|
</div>
|
|
54
56
|
<div
|
|
55
57
|
class="absolute inset-0 flex items-center justify-start pl-1 text-xs text-white/80 font-medium whitespace-nowrap pointer-events-none"
|
|
56
|
-
|
|
58
|
+
data-span-id="{{ span.span_id }}-label"
|
|
59
|
+
data-start-percent="{{ start_percent }}"
|
|
60
|
+
data-width-percent="{{ width_percent }}">
|
|
57
61
|
{{ "%.2f"|format(span.duration_ms()) }}ms
|
|
58
62
|
</div>
|
|
59
63
|
</div>
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
<div class="mb-2">
|
|
5
5
|
<div class="flex items-center gap-2 bg-emerald-900/20 border border-emerald-700/50 rounded px-2 py-1">
|
|
6
6
|
<span class="text-xs text-emerald-400 font-mono flex-1 truncate" id="share-url-{{ trace.id }}">
|
|
7
|
-
{{ request.
|
|
7
|
+
{{ request.host }}{{ url('observer:trace_shared', trace.share_id) }}
|
|
8
8
|
</span>
|
|
9
9
|
<button
|
|
10
|
-
|
|
10
|
+
data-copy-share-url
|
|
11
|
+
data-trace-id="{{ trace.id }}"
|
|
11
12
|
data-share-url="{{ request.build_absolute_uri(url('observer:trace_shared', trace.share_id)) }}"
|
|
12
13
|
class="px-2 py-0.5 text-xs bg-emerald-700 text-emerald-100 hover:bg-emerald-600 rounded transition-colors flex-shrink-0"
|
|
13
14
|
title="Copy shareable URL">
|
|
@@ -148,46 +149,3 @@
|
|
|
148
149
|
{% endfor %}
|
|
149
150
|
</div>
|
|
150
151
|
</div>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<style>
|
|
154
|
-
/* Custom details arrow animation */
|
|
155
|
-
details[open] summary svg {
|
|
156
|
-
transform: rotate(90deg);
|
|
157
|
-
}
|
|
158
|
-
</style>
|
|
159
|
-
|
|
160
|
-
<script>
|
|
161
|
-
async function copyShareUrl(button, traceId) {
|
|
162
|
-
try {
|
|
163
|
-
const shareUrl = button.getAttribute('data-share-url');
|
|
164
|
-
|
|
165
|
-
// Copy to clipboard
|
|
166
|
-
await navigator.clipboard.writeText(shareUrl);
|
|
167
|
-
|
|
168
|
-
// Show success feedback on button
|
|
169
|
-
const originalHTML = button.innerHTML;
|
|
170
|
-
button.innerHTML = '<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 16 16"><path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/></svg>';
|
|
171
|
-
button.classList.remove('bg-emerald-700', 'hover:bg-emerald-600');
|
|
172
|
-
button.classList.add('bg-green-600', 'hover:bg-green-700');
|
|
173
|
-
|
|
174
|
-
// Also flash the URL text
|
|
175
|
-
const urlSpan = document.getElementById(`share-url-${traceId}`);
|
|
176
|
-
if (urlSpan) {
|
|
177
|
-
urlSpan.classList.add('text-green-400', 'font-bold');
|
|
178
|
-
setTimeout(() => {
|
|
179
|
-
urlSpan.classList.remove('text-green-400', 'font-bold');
|
|
180
|
-
}, 2000);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
setTimeout(() => {
|
|
184
|
-
button.innerHTML = originalHTML;
|
|
185
|
-
button.classList.remove('bg-green-600', 'hover:bg-green-700');
|
|
186
|
-
button.classList.add('bg-emerald-700', 'hover:bg-emerald-600');
|
|
187
|
-
}, 2000);
|
|
188
|
-
} catch (error) {
|
|
189
|
-
console.error('Failed to copy share URL:', error);
|
|
190
|
-
alert('Failed to copy share URL. See console for details.');
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
</script>
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
<title>Trace {{ trace.trace_id }} - Observer</title>
|
|
7
7
|
{% tailwind_css %}
|
|
8
8
|
{% htmx_js %}
|
|
9
|
+
<link rel="stylesheet" href="{{ asset('observer/observer.css') }}">
|
|
10
|
+
<script src="{{ asset('observer/observer.js') }}" defer></script>
|
|
9
11
|
</head>
|
|
10
12
|
<body class="bg-stone-950 text-stone-300 min-h-screen">
|
|
11
13
|
<div class="container mx-auto p-6 max-w-6xl">
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
<title>Shared Trace - {{ trace.trace_id }} - Observer</title>
|
|
7
7
|
{% tailwind_css %}
|
|
8
8
|
{% htmx_js %}
|
|
9
|
+
<link rel="stylesheet" href="{{ asset('observer/observer.css') }}">
|
|
10
|
+
<script src="{{ asset('observer/observer.js') }}" defer></script>
|
|
9
11
|
</head>
|
|
10
12
|
<body class="bg-stone-950 text-stone-300 min-h-screen">
|
|
11
13
|
<div class="container mx-auto p-6 max-w-6xl">
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
<title>Observer Traces</title>
|
|
7
7
|
{% tailwind_css %}
|
|
8
8
|
{% htmx_js %}
|
|
9
|
-
<
|
|
9
|
+
<link rel="stylesheet" href="{{ asset('observer/observer.css') }}">
|
|
10
|
+
<script src="{{ asset('observer/observer.js') }}" defer></script>
|
|
11
|
+
<script nonce="{{ request.csp_nonce }}">
|
|
10
12
|
if (window.self !== window.top) {
|
|
11
13
|
document.addEventListener('DOMContentLoaded', function() {
|
|
12
14
|
document.body.setAttribute('data-iframe', 'true');
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<p>Loading spans...</p>
|
|
4
4
|
</div>
|
|
5
5
|
</div>
|
|
6
|
-
<script>
|
|
6
|
+
<script nonce="{{ request.csp_nonce }}">
|
|
7
7
|
(function() {
|
|
8
8
|
var container = document.getElementById('observer-traces');
|
|
9
9
|
var loaded = false;
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
var iframe = document.createElement('iframe');
|
|
30
30
|
iframe.src = "{{ url('observer:traces') }}";
|
|
31
31
|
iframe.frameBorder = "0";
|
|
32
|
-
iframe.
|
|
33
|
-
iframe.
|
|
32
|
+
iframe.width = "100%";
|
|
33
|
+
iframe.height = "100%";
|
|
34
34
|
container.innerHTML = '';
|
|
35
35
|
container.appendChild(iframe);
|
|
36
36
|
observer.disconnect();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plain.observer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: On-page telemetry and observability tools for Plain.
|
|
5
5
|
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -16,6 +16,7 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
**On-page telemetry and observability tools for Plain.**
|
|
17
17
|
|
|
18
18
|
- [Installation](#installation)
|
|
19
|
+
- [Content Security Policy (CSP)](#content-security-policy-csp)
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
21
22
|
|
|
@@ -57,3 +58,25 @@ plain migrate
|
|
|
57
58
|
```
|
|
58
59
|
|
|
59
60
|
After installation, Observer will automatically integrate with your application's toolbar (if using `plain.admin`). You can access the web interface at `/observer/traces/` or use the CLI commands to analyze traces.
|
|
61
|
+
|
|
62
|
+
## Content Security Policy (CSP)
|
|
63
|
+
|
|
64
|
+
If you're using a Content Security Policy (CSP), the Observer toolbar panel requires `frame-ancestors 'self'` to display trace information in an iframe.
|
|
65
|
+
|
|
66
|
+
Without this directive, the toolbar panel will fail to load with a CSP error: `"Refused to frame... because an ancestor violates the following Content Security Policy directive: 'frame-ancestors 'none'"`.
|
|
67
|
+
|
|
68
|
+
Example CSP configuration:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
def DEFAULT_RESPONSE_HEADERS(request):
|
|
72
|
+
nonce = request.csp_nonce
|
|
73
|
+
return {
|
|
74
|
+
"Content-Security-Policy": (
|
|
75
|
+
f"default-src 'self'; "
|
|
76
|
+
f"script-src 'self' 'nonce-{nonce}'; "
|
|
77
|
+
f"style-src 'self' 'nonce-{nonce}'; "
|
|
78
|
+
f"frame-ancestors 'self'; " # Required for Observer toolbar
|
|
79
|
+
# ... other directives
|
|
80
|
+
),
|
|
81
|
+
}
|
|
82
|
+
```
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
plain/observer/AGENTS.md,sha256=w3vqd2VWGliBQl8ohEBOsHyo1KROoUuK10uDr_C5Xeo,334
|
|
2
|
-
plain/observer/CHANGELOG.md,sha256=
|
|
3
|
-
plain/observer/README.md,sha256=
|
|
2
|
+
plain/observer/CHANGELOG.md,sha256=WEGX1l0PbDgr2T4gZXjo2NMmjVi1Q93TDe4iTX9vPQs,17211
|
|
3
|
+
plain/observer/README.md,sha256=zheQQwkFGDrs5pChC1kzC2x_mpBA8Lh5X97C6khyywA,1870
|
|
4
4
|
plain/observer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
plain/observer/admin.py,sha256=
|
|
5
|
+
plain/observer/admin.py,sha256=u4F9VYMwDkwzfVgMlLUqwaqDjtsf9RIJppzOA2NXWKc,3516
|
|
6
6
|
plain/observer/cli.py,sha256=rzESdG8rYH6ILZ_5jA6oR362kCjTlimgzNqng89s2yg,21131
|
|
7
7
|
plain/observer/config.py,sha256=BJKsKNbfNSYmaxpUAkuR6gDMgNvD9tx19wpo3-1sksA,2542
|
|
8
8
|
plain/observer/core.py,sha256=9AQFe4pwLpz4w64GG7599rNxuTkMN3eyqsqFNehy8A0,5950
|
|
9
|
-
plain/observer/default_settings.py,sha256=
|
|
9
|
+
plain/observer/default_settings.py,sha256=dy90FE2v_YFuqDmc87J1PWP_wlw2_fnC5pSwk_GX_QA,180
|
|
10
10
|
plain/observer/logging.py,sha256=amv5i06ghszoJ192vrZ0zFdnTajazMxei40ltf0kI50,3061
|
|
11
11
|
plain/observer/models.py,sha256=6hyPAjK-Uq24n-2TkN0eHW8xcDIMUprMAFMY6rlLXiI,18365
|
|
12
12
|
plain/observer/otel.py,sha256=QbWoSc2KxDDhGfBQeww_uKBy3DL1SCOomvmxzU_yAVM,16902
|
|
13
13
|
plain/observer/toolbar.py,sha256=btFUZIOQu0dkXOz7dWay79S0eBpadtSdfGyHF0yRVY4,720
|
|
14
14
|
plain/observer/urls.py,sha256=oLJoDjB7YMUx8z-MGql_xXSXkfacVKFp-aHNXaKzs38,376
|
|
15
15
|
plain/observer/views.py,sha256=johyO4MaLvROCexelrPDw0O-mIylzstJS7bzN94NOBg,5424
|
|
16
|
+
plain/observer/assets/observer/observer.css,sha256=4DF_523mc40x33mYF-itTqESoBgwv2JI5MUaByUnUdI,1289
|
|
17
|
+
plain/observer/assets/observer/observer.js,sha256=1CypJqFBjbVn3IcBC9S0cbMVpE7LtoG19o9PljEhM4c,2496
|
|
16
18
|
plain/observer/migrations/0001_initial.py,sha256=HVoSrd5V-IOqD1adADIAzqMH8xMlPwyLOFH6JcGFniI,3312
|
|
17
19
|
plain/observer/migrations/0002_trace_share_created_at_trace_share_id_trace_summary_and_more.py,sha256=lgFqdn66zq7AoCvkC3MIvLXiE_Omup5EEMDSe1jpKOg,1765
|
|
18
20
|
plain/observer/migrations/0003_span_plainobserv_span_id_e7ade3_idx.py,sha256=54G8GXi-PiFbcTU-HrXO14UN9PxhjDCY05TA9HHkkmk,527
|
|
@@ -20,15 +22,15 @@ plain/observer/migrations/0004_trace_app_name_trace_app_version.py,sha256=Qfp62t
|
|
|
20
22
|
plain/observer/migrations/0005_log_log_plainobserv_trace_i_fcfb7d_idx_and_more.py,sha256=ubUAPgN_jKK_o9J3G38MihGgsP_LvBwAzgvc1N-Td9M,2167
|
|
21
23
|
plain/observer/migrations/0006_remove_log_logger.py,sha256=ruuwsDEcilSFTimU52JMiHz9ENxRCmLyheX_71ZXxfo,362
|
|
22
24
|
plain/observer/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
-
plain/observer/templates/observer/trace.html,sha256=
|
|
24
|
-
plain/observer/templates/observer/trace_detail.html,sha256=
|
|
25
|
-
plain/observer/templates/observer/trace_share.html,sha256=
|
|
26
|
-
plain/observer/templates/observer/traces.html,sha256=
|
|
27
|
-
plain/observer/templates/observer/partials/log.html,sha256=
|
|
28
|
-
plain/observer/templates/observer/partials/span.html,sha256=
|
|
29
|
-
plain/observer/templates/toolbar/observer.html,sha256=
|
|
25
|
+
plain/observer/templates/observer/trace.html,sha256=20argqtoo9M1CniIGSjKAuqf4CX0Pxauu5g5IpmVmzc,9826
|
|
26
|
+
plain/observer/templates/observer/trace_detail.html,sha256=9XyNJRWBrGTiEskQeeGdoPspeV7Lpur6s5YYFWw0Gw4,1085
|
|
27
|
+
plain/observer/templates/observer/trace_share.html,sha256=hsAuM6_jYpNCHIzV_A60lzg-OdY2Gg5V8xVYG4d5G6c,667
|
|
28
|
+
plain/observer/templates/observer/traces.html,sha256=cXoj1dATYM_4-xfqlKn1jl136MAkPmD4g-ALeceYvD4,24705
|
|
29
|
+
plain/observer/templates/observer/partials/log.html,sha256=dNIp1Wuu6dLLpenDIiUFcCkpFCQe2u-8Xk8mhrE41x0,952
|
|
30
|
+
plain/observer/templates/observer/partials/span.html,sha256=toBqwwou_oYYjAJDRJ_a72xm0CZPpTqM0-u6A_iWctc,18880
|
|
31
|
+
plain/observer/templates/toolbar/observer.html,sha256=SI1y8KzXRMaI9XDqGxuk6KnjyZwU4yXzPfhWrwD52yM,1385
|
|
30
32
|
plain/observer/templates/toolbar/observer_button.html,sha256=FMBJHKMGqpHWs-3Ei2PBCdVYZ_6vFw6-eKH_i4jQu-Q,1215
|
|
31
|
-
plain_observer-0.
|
|
32
|
-
plain_observer-0.
|
|
33
|
-
plain_observer-0.
|
|
34
|
-
plain_observer-0.
|
|
33
|
+
plain_observer-0.13.0.dist-info/METADATA,sha256=07wRHfuXZh44Wj66IgJhVx9FDuZ37agRV1Ep-bYBeiM,2269
|
|
34
|
+
plain_observer-0.13.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
35
|
+
plain_observer-0.13.0.dist-info/licenses/LICENSE,sha256=YZdq6Pz8ivjs97eSVLRmoGDI1hjEikX6N49DfM0DWio,1500
|
|
36
|
+
plain_observer-0.13.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|