JoviDeCroock/pracht/skills/add-observability/SKILL.md
add-observability
Wire Sentry or OpenTelemetry into pracht server boundaries (loaders, middleware, API routes), client-side Web Vitals reporting, and a capability audit sink that records every capability dispatch with transport, outcome, latency, and verified identity. Use for "add observability", "wire Sentry", "set up tracing", "add OpenTelemetry", "monitor Web Vitals", "track errors", "log capability calls", or "are agents calling my app".
- Source repository stars
- 94
- Declared platforms
- 0
- Static risk flags
- 2
- Last source update
- 2026-08-28
- Source checked
- 2026-08-28
Decision brief
What it does: where it fits
Four layers, each opt-in:
Not for
- Tasks that require unconfirmed production actions or broad system permissions.
- Environments where the pinned source and install steps cannot be inspected.
Compatibility matrix
Platform support, with evidence labels
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
Inspect first. Install second.
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/JoviDeCroock/pracht --skill "skills/add-observability"Inspect the Agent Skill "add-observability" from https://github.com/JoviDeCroock/pracht/blob/43a8e8dcccb137abcc7c78d4a238edc01829e5a9/skills/add-observability/SKILL.md at commit 43a8e8dcccb137abcc7c78d4a238edc01829e5a9. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
What the source asks the agent to do
- 01
Step 1: Pick the stack
The skill below shows Sentry and OTel patterns. Custom beacon is mentioned but trivial.
Sentry — easiest end-to-end (errors + traces + Web Vitals).OpenTelemetry + your backend (Honeycomb, Grafana, Datadog, Jaeger).Custom beacon — minimal fetch('/api/telemetry') setup, no SaaS. - 02
Step 2: Server error tracking
The pattern below uses @sentry/node and works on the Node adapter only — see the caveat box below for Cloudflare and Vercel Edge before installing anything.
The pattern below uses @sentry/node and works on the Node adapter only — see the caveat box below for Cloudflare and Vercel Edge before installing anything.Create src/server/observability.ts:Add a global middleware that calls initObservability() once (from inside the handler, never at module scope) and wraps the downstream call: - 03
Step 3: Loader/API tracing
For each loader and API handler, wrap the body in a span.
For each loader and API handler, wrap the body in a span.Auto-injection is out of scope; provide a snippet, recommend wrapping the 5-10 slowest loaders (cross-reference with audit-bundles perf hotspots). - 04
Step 4: Web Vitals on the client
Create src/client/vitals.ts — export a function, no module-level side effects:
Create src/client/vitals.ts — export a function, no module-level side effects:Do NOT import this statically from a shell: shells render on the server too, so module-level onCLS(...) calls would execute during SSR. The primary pattern is a lazy import() inside an effect, guarded by useIsHydrated (…This keeps web-vitals out of the critical bundle (lazy chunk) and only starts observers after hydration has fully settled. - 05
Step 5: Beacon endpoint
For Sentry users, Sentry's browser SDK can capture Web Vitals natively — prefer that over a custom beacon if you've gone the Sentry route.
For Sentry users, Sentry's browser SDK can capture Web Vitals natively — prefer that over a custom beacon if you've gone the Sentry route.
Permission review
Static risk signals and limitations
Network access
The documentation includes network, browsing, or remote request actions.
**Custom beacon** — minimal `fetch('/api/telemetry')` setup, no SaaS.Runs scripts
The documentation asks the agent to run terminal commands or scripts.
pnpm add @sentry/node # Node adapter onlyNetwork access
The documentation includes network, browsing, or remote request actions.
middleware and `fetch` them to Sentry's store/envelope endpoint (or anyRuns scripts
The documentation asks the agent to run terminal commands or scripts.
pnpm add @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/auto-instrumentations-nodeEvidence record
Why each signal appears
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 91/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 94 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
Provenance and original SKILL.md
- Repository
- JoviDeCroock/pracht
- Skill path
- skills/add-observability/SKILL.md
- Commit
- 43a8e8dcccb137abcc7c78d4a238edc01829e5a9
- License
- MIT
- Collected
- 2026-08-28
- Default branch
- main
View the original SKILL.md
Pracht Add Observability
Four layers, each opt-in:
- Server error tracking — capture loader/middleware/API exceptions.
- Request tracing — span per request with child spans per loader/db call.
- Web Vitals (LCP/CLS/INP/FCP/TTFB) — client-side, posted to a beacon endpoint.
- Agent traffic — one structured audit event per capability dispatch. Only relevant when the app registers capabilities; skip it otherwise.
MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
inspect_agents/inspect_routes/inspect_api/inspect_build/doctor/
verify/generate_* tools over shelling out. pracht inspect needs the pracht
plugin in the vite config.
Step 1: Pick the stack
Use AskUserQuestion:
- Sentry — easiest end-to-end (errors + traces + Web Vitals).
- OpenTelemetry + your backend (Honeycomb, Grafana, Datadog, Jaeger).
- Custom beacon — minimal
fetch('/api/telemetry')setup, no SaaS.
The skill below shows Sentry and OTel patterns. Custom beacon is mentioned but trivial.
Step 2: Server error tracking
Sentry path (Node adapter)
The pattern below uses @sentry/node and works on the Node adapter only
— see the caveat box below for Cloudflare and Vercel Edge before installing
anything.
pnpm add @sentry/node # Node adapter only
Create src/server/observability.ts:
import { serverEnv } from "@pracht/core/env/server";
import * as Sentry from "@sentry/node";
let initialized = false;
export function initObservability() {
if (initialized) return;
initialized = true;
// serverEnv is read INSIDE this function, not at module scope — module-level
// env reads break on runtimes where env arrives per request (docs/ENV.md).
Sentry.init({
dsn: serverEnv.SENTRY_DSN,
tracesSampleRate: Number(serverEnv.SENTRY_TRACES_SAMPLE_RATE ?? 0.1),
environment: serverEnv.NODE_ENV,
});
}
Add a global middleware that calls initObservability() once (from inside
the handler, never at module scope) and wraps the downstream call:
// src/middleware/observability.ts
import type { MiddlewareFn } from "@pracht/core";
import * as Sentry from "@sentry/node";
import { initObservability } from "../server/observability";
export const middleware: MiddlewareFn = async ({ request, route }, next) => {
initObservability();
return Sentry.startSpan(
{
name: `${request.method} ${route.path}`,
op: "http.server",
},
() => next(),
);
};
Cloudflare / Vercel Edge caveat — be honest here.
@sentry/cloudflarerequires wrapping the worker's fetch handler withwithSentry(), but pracht's Cloudflare adapter owns that handler — there is no user hook to wrap it today, so the middleware-init pattern above cannot work with@sentry/cloudflare, and@sentry/nodedoes not run on Workers at all. Do not scaffold a pattern that can't work. Options on Cloudflare:
- Plain fetch-based event forwarding: catch errors in a wrap-around middleware and
fetchthem to Sentry's store/envelope endpoint (or any HTTP sink) yourself. Read the DSN viaserverEnvinside the middleware.- Wait for pracht to expose a handler-wrap hook for the adapter, then use Sentry's Cloudflare SDK properly.
The same applies to
@sentry/vercel-edge: verify how the init hooks into the runtime before installing; if it needs to own the handler, fall back to option 1. (This mirrors the OTel-edge honesty note below.)
Pracht middleware is wrap-around: await next() invokes the rest of the
request and resolves to the final Response, so the span naturally covers
the loader/handler and ends when they finish.
Register it in defineApp({ middleware: { observability: "./..." } }) (the
top-level middleware field is a registry keyed by name — not an ordered
chain). To actually wrap requests, place "observability" first in every
chain that should cover them:
defineApp({
middleware: { observability: "./middleware/observability.ts", auth: "./middleware/auth.ts" },
api: { middleware: ["observability"] }, // all API routes
routes: [
group({ middleware: ["observability"] }, [ // all pages
group({ middleware: ["auth"] }, [ /* protected routes */ ]),
]),
],
});
Ordering lives in these middleware: [...] arrays — always place
observability first so it spans the rest of the chain.
OpenTelemetry path
pnpm add @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/auto-instrumentations-node
Create a SDK init module that runs at server entry — for Node, use the
--require ./otel.cjs flag; for Cloudflare/Vercel edge, OTel is more limited
(use HTTP exporter directly). Surface this trade-off; don't pretend OTel
edge is plug-and-play.
Step 3: Loader/API tracing
For each loader and API handler, wrap the body in a span.
import * as Sentry from "@sentry/node";
export async function loader({ request }) {
return Sentry.startSpan({ name: "loader: dashboard", op: "function" }, async () => {
return { /* ... */ };
});
}
Auto-injection is out of scope; provide a snippet, recommend wrapping the 5-10
slowest loaders (cross-reference with audit-bundles perf hotspots).
Step 4: Web Vitals on the client
pnpm add web-vitals
Create src/client/vitals.ts — export a function, no module-level
side effects:
import { onCLS, onINP, onLCP, onFCP, onTTFB, type Metric } from "web-vitals";
function send(metric: Metric) {
navigator.sendBeacon?.(
"/api/telemetry/vitals",
JSON.stringify({ name: metric.name, value: metric.value, id: metric.id, path: location.pathname }),
);
}
export function reportVitals() {
onCLS(send);
onINP(send);
onLCP(send);
onFCP(send);
onTTFB(send);
}
Do NOT import this statically from a shell: shells render on the server
too, so module-level onCLS(...) calls would execute during SSR. The primary
pattern is a lazy import() inside an effect, guarded by useIsHydrated
(exported from @pracht/core), placed in a shell or top-level component:
import { useIsHydrated } from "@pracht/core";
import { useEffect } from "preact/hooks";
export function Vitals() {
const hydrated = useIsHydrated();
useEffect(() => {
if (!hydrated) return;
void import("../client/vitals").then((m) => m.reportVitals());
}, [hydrated]);
return null;
}
This keeps web-vitals out of the critical bundle (lazy chunk) and only
starts observers after hydration has fully settled.
Step 5: Beacon endpoint
// src/api/telemetry/vitals.ts
import type { ApiRouteArgs } from "@pracht/core";
export async function POST({ request }: ApiRouteArgs) {
const body = await request.text();
// Forward to your destination (Sentry, Honeycomb, custom store).
// Keep body small; do not block on the upstream.
console.log("vitals", body);
return new Response(null, { status: 204 });
}
For Sentry users, Sentry's browser SDK can capture Web Vitals natively — prefer that over a custom beacon if you've gone the Sentry route.
Step 6: Agent traffic (capability apps only)
Skip when pracht inspect agents --json reports an empty capabilities list.
That only means there are no capability operations to audit; llms.txt, MCP,
or Web Bot Auth may still expose other agent-facing surfaces. When capabilities
do exist, every dispatch already emits a structured CapabilityAuditEvent —
nothing is instrumented per capability, a sink just has to be registered.
import { addCapabilityAuditListener } from "@pracht/core/server";
const stopAuditLog = addCapabilityAuditListener("audit-log", (event) => {
console.log(
JSON.stringify({
msg: "capability",
at: new Date().toISOString(),
capability: event.capability,
effect: event.effect,
transport: event.transport, // "http" | "webmcp" | "mcp" | "server"
via: event.via, // causal transport for nested invokeCapability()
outcome: event.outcome, // "ok" or the envelope error code
status: event.status,
durationMs: Math.round(event.durationMs),
agent: event.agent?.agentDomain ?? event.agent?.keyId ?? null,
}),
);
});
if (import.meta.hot) {
import.meta.hot.dispose(stopAuditLog);
}
The OTel version records a counter and a histogram keyed on
capability/transport/outcome, and backdates a span with
startTime: Date.now() - event.durationMs (the dispatch has already
finished when the sink runs). The full snippet is on the agent-trust docs page.
Import the module from an eagerly loaded server module. The adapter's configured
createContextFrom module is one portable option: add import "./audit.ts"
there so the generated entry registers the sink before request handling. A
custom server entry can import it directly. Do not rely on an unrelated route,
API route, middleware, or src/server/ registry module; those modules are lazy
and can miss earlier capability calls.
Key properties to state when scaffolding this:
- Sinks are invoked synchronously, so keep work before the callback returns or
reaches its first
awaitcheap. A returned promise is never awaited, and a synchronous throw is swallowed (first failure per sink reported viaconsole.warn, naming it), so a broken exporter cannot fail the call. - Always pass a stable name as the first argument. Registering the same
name again replaces that sink, which is what keeps a module-scope call safe
under dev HMR: Vite re-executes importers on every save, so an unkeyed
registration would add a fresh closure per keystroke and deliver every event
N times. Never compute the name. Register the returned unsubscribe with
import.meta.hot.dispose()too, so removing the module or renaming the sink cannot leave the old name active until the dev server restarts. setCapabilityAuditHook()is a single slot — a second call replaces the first. UseaddCapabilityAuditListener()whenever more than one sink exists; it returns an unsubscribe handle that removes only its own registration.- Warning suppression is per named registration, so differently named sinks still report independently when they reuse one callback.
- Delivery snapshots the registered sinks before callbacks run. Adding or replacing a sink from inside a callback takes effect on the next dispatch, so the current event is never delivered twice to one name.
- On Cloudflare Workers, a batching exporter must flush within the request or
be handed the execution context by app code
(
context.executionContext.waitUntil(exporter.flush())). Pracht does not callctx.waitUntil()for a sink. - Audit events cover dispatch only. A cross-origin 403, an unknown-capability 404, and an unknown MCP tool name all return before dispatch and emit nothing, so do not build a reconnaissance alert on these events — use the HTTP access log for that.
In dev the same events are already collected: the Agents section of
/_pracht shows recent dispatches with transport, via, verified identity,
outcome, and duration, and /_pracht.json exposes all of them under
agentTraffic. The page counts verified identities, MCP, and MCP-caused
composition as agent-attributed; shows top-level unsigned HTTP, HTTP-caused
composition, and client-declared WebMCP markers separately as unverified client
dispatches; and hides only invokeCapability() work with no served-request
provenance behind a first-party toggle, so the panel's visible count can be
lower than the sink's.
Counts and empty-state conclusions only cover the retained window when older
events have been dropped. Use it to confirm the sink sees what the panel sees
before wiring a paid backend. Adapter-owned dev servers do not register this
middleware: on Cloudflare workerd, /_pracht and /_pracht.json return 404.
Validate the sink from its own output there; a missing panel is not a failed
audit hook.
Step 7: Sampling and PII
- Set
SENTRY_TRACES_SAMPLE_RATEto a small number (0.05–0.10) in production. - Scrub auth headers and cookies from breadcrumbs:
Sentry.init({ beforeSend(event) { delete event.request?.headers?.cookie; return event; } }); - Never send loader return values verbatim — they often contain user data.
Step 8: Verify
- Trigger a deliberate error in dev and confirm it lands in Sentry/OTel.
- Open a route, check the Web Vitals beacon fires (Network tab).
- If an audit sink was added: call a capability in dev and confirm the event
reaches both the sink and, when the adapter exposes it, the Agents panel at
/_pracht. On Cloudflare's adapter-owned dev server, validate the sink output directly because the panel does not exist. - Confirm
pnpm testandpnpm e2estill pass. - Run
pracht typegenif any routes were added (the beacon API route does not affect page-route types, but re-run when in doubt). - Run
pracht verify --jsonand confirm no failures.
Rules
- Confirm adapter compatibility before installing the SDK package
(Sentry has separate packages per runtime), and never scaffold a pattern
the runtime can't actually run — see the Cloudflare/Vercel-edge caveat in
Step 2. Read
SENTRY_DSNand friends viaserverEnvinside functions, neverprocess.envat module scope. - Top-level
middlewareindefineAppis a name→path registry, not an ordered chain. Place"observability"first in everygroup({ middleware: [...] })and inapi.middlewareso it wraps the rest. - Web Vitals only matter for SSR/SSG/ISG routes that hydrate; SPA-only routes still benefit but the values reflect the post-bootstrap state.
- Sample traces (≤ 10%) in production; full sampling in dev.
- Never send raw cookies, auth headers, or full loader payloads to a third-party SaaS. The same applies to audit events: log the capability name, effect, transport, outcome, and agent domain — not the capability's input, which is application data.
- Keep the synchronous part of an audit sink cheap: no CPU-heavy work or synchronous network call. Returned promises are fire-and-forget; the runtime does not await them or catch their rejections.
$ARGUMENTS
Frequently asked questions
What to verify before installation and use
What does the add-observability source document cover?
Four layers, each opt-in:
How do I install add-observability?
The source record exposes this install command: npx skills add https://github.com/JoviDeCroock/pracht --skill "skills/add-observability". Inspect the command and pinned source before running it.
Which permission-related actions were detected?
Static rules flagged network, exec-script in the source; the page lists the matching lines and excerpts.
Alternatives
Compare before choosing
garrytan/gbrain
bulk-ingestion
End-to-end discipline for turning any large data source (audio libraries, email takeouts, document corpora, chat exports, API dumps) into brain pages at scale. The lifecycle spine: SCHEMA → ACCESS → TRIAL → EVALUATE → IMPROVE → CODIFY → TEST → SKILLIFY → BULK → MONITOR. State is tracked in a durable JSON manifest (see MANIFEST-PATTERN.md) so any crash, session boundary, or subagent fan-out resumes from ground truth instead of memory.
alirezarezvani/claude-skills
app-store-optimization
App Store Optimization (ASO) toolkit for researching keywords, analyzing competitor rankings, generating metadata suggestions, and improving app visibility on Apple App Store and Google Play Store. Use when the user asks about ASO, app store rankings, app metadata, app titles and descriptions, app store listings, app visibility, or mobile app marketing on iOS or Android. Supports keyword research and scoring, competitor keyword analysis, metadata optimization, A/B test planning, launch checklist
dotnet/skills
migrate-vstest-to-mtp
Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP). Use when user asks to "migrate to MTP", "switch from VSTest", "enable Microsoft.Testing.Platform", "use MTP runner", set OutputType=Exe only for test projects in Directory.Build.props, or mentions EnableMSTestRunner, EnableNUnitRunner, or UseMicrosoftTestingPlatformRunner. USE FOR: MTP behavioral differences vs VSTest (exit code 8, zero tests discovered, --ignore-exit-code, TESTINGPLATFORM_EXITCODE_IGNORE); centralizing
oaustegard/claude-skills
featuring
Generate hierarchical _FEATURES.md files that describe what a codebase DOES from a user/consumer perspective, anchored to source symbols via tree-sitting. Supports large complex codebases through feature-driven decomposition into sub-feature files. Uses a multi-pass synthesis: orientation → detail → overview rewrite. Use when someone says "what does this do", "document features", "feature inventory", "_FEATURES.md", or needs to understand a codebase's purpose before modifying it. Complements tre