diff --git a/.gitignore b/.gitignore
index 6b5cefa..d26f520 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,5 +19,7 @@ go-zero/go-zero
db.sqlite3
gorm.db
+**/docs/superpowers/
+
# Misc
.DS_Store
diff --git a/README.md b/README.md
index 91a840b..1b1dc94 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,7 @@ project.
- [sentry-go](sentry-go) — errors and tracing with sentry-go.
- [sentry-python](sentry-python) — nested exception chains with sentry-python.
- [sentry-browser](sentry-browser) — events with @sentry/browser.
+- [sentry-react](sentry-react) — errors, breadcrumbs, and tracing with @sentry/react in a todo app.
### Logs, Collector, and demos
diff --git a/sentry-react/.env.example b/sentry-react/.env.example
new file mode 100644
index 0000000..2e79909
--- /dev/null
+++ b/sentry-react/.env.example
@@ -0,0 +1,12 @@
+# Copy this file to `.env` and paste your Uptrace Sentry DSN.
+#
+# Get it from Uptrace: open your project, go to "DSN", switch to the
+# "Sentry" tab, and copy the DSN. It looks like:
+#
+# http://@/
+VITE_SENTRY_DSN=
+
+# Optional: base URL of your Uptrace UI (its site URL), used to turn the current
+# trace badge into a clickable link to that trace. Without it the badge still
+# shows the trace id.
+VITE_UPTRACE_URL=
diff --git a/sentry-react/.gitignore b/sentry-react/.gitignore
new file mode 100644
index 0000000..63862ee
--- /dev/null
+++ b/sentry-react/.gitignore
@@ -0,0 +1,8 @@
+node_modules
+dist
+.env
+*.local
+*.tsbuildinfo
+test-results
+playwright-report
+blob-report
diff --git a/sentry-react/AGENTS.md b/sentry-react/AGENTS.md
new file mode 100644
index 0000000..692437f
--- /dev/null
+++ b/sentry-react/AGENTS.md
@@ -0,0 +1,82 @@
+# React + Sentry example
+
+Rules here apply to `examples/sentry-react`, one of the `uptrace/examples` projects:
+small, runnable apps that show how to send data to Uptrace. This one shows how
+to integrate the Sentry SDK (`@sentry/react`) with a React frontend reporting to
+Uptrace, and that is the one thing it teaches. Keep it minimal: an example is
+read more than it is run, so every extra feature, file, or dependency is noise
+that hides the integration. Add something only when it makes the integration
+clearer, not the app richer. The `README.md` is the primary deliverable; when
+behavior changes, update it in the same change.
+
+## Core Rules
+
+- The only Uptrace-specific code is `Sentry.init()` in `src/instrument.ts`;
+ `src/telemetry.ts` holds the rest of the Sentry SDK usage. `instrument.ts` is
+ imported first in `src/main.tsx` (before React) so instrumentation is in place
+ before the app renders. `instrument.ts` also installs a reporting transport
+ wrapper (`src/delivery.ts`) that observes send outcomes for the UI — it wraps
+ the standard fetch transport and does not change delivery, so it is not an
+ Uptrace-specific adapter.
+- The DSN comes from `VITE_SENTRY_DSN`; never hardcode a DSN. `.env` is
+ gitignored — keep `.env.example` as the template and document it in the README.
+- Uptrace ingests the standard Sentry protocol, so there is no Uptrace exporter
+ or adapter. If you reach for one, you are doing it wrong.
+- Never reach for `Sentry.logger.*` / `enableLogs`. They emit `log` envelope items,
+ and Uptrace's Sentry ingest handles only `transaction`, `event` and
+ `client_report` — anything else is skipped, so the data is silently dropped (the
+ SDK still reports a successful send). A `logger.*` Logs panel was added and
+ removed once already; do not re-add it. `Sentry.captureMessage()` is the path
+ that works: Uptrace stores a Sentry event carrying a `message` as a log
+ (`event_name=log`, the level normalized into `log_severity`).
+- The app is a small routed single-page app: Vite + React + TypeScript with
+ React Router v7, state held in memory (`useState`). There is no separate
+ backend — the `/api/ok|slow|fail` endpoints are Vite dev-server middleware
+ (`vite.config.ts`), so they exist under `npm run dev` but not in a static
+ `vite preview` build. No UI framework.
+- Navigation is traced by `Sentry.wrapReactRouterRouting` (`src/main.tsx`),
+ which names each navigation trace by its route pattern (`/products/:id`,
+ `/categories/*`), not the concrete URL. Keep that: it is a deliberate
+ low-cardinality demonstration, and the concrete URL still rides on the root span
+ as `url.full`.
+- Each control fires an explicit Sentry signal and leaves a breadcrumb; keep both
+ going for new controls. The signals are: Todos (a `created todo` span on add, a
+ back-dated `completed todo` span on Done), HTTP (`/api/ok|slow|fail` → a
+ `GET /api/…` span nested under the page root, wrapping the SDK's auto-instrumented
+ `http.client` span; Fail also captures an error), and Errors (one exception per
+ button — the home panel uses fixed types, each sub-route its own named errors).
+ The not-found route auto-reports a `PageNotFound` error when shown. Document any
+ new control in the README.
+- Hard rule: never call `Sentry.startNewTrace()` — the trace comes only from the
+ browser-tracing integration (a pageload trace on load, a navigation trace on each
+ route change). Every signal attaches to the current trace **nested under the
+ page's root span** (via `startSpan`/`startInactiveSpan` with `parentSpan` +
+ `forceTransaction`, wired in `telemetry.ts`), because Uptrace renders only one
+ root span per trace — sibling roots would be dropped from the trace tree.
+- TypeScript with `strict` on. JS/TS comments use `//` line comments, including
+ comments for exported types and functions. Comment only what the code cannot say.
+- Order code top-down, as the Go backend does: module doc, imports, package-level
+ consts, then the entry points, then their callees below them — a helper goes under
+ the function that calls it, never above. A const or type used by exactly one
+ function sits directly above that function. Read a file top to bottom and the call
+ stack unfolds in order.
+- The UI is four files grouped by what they do — `shell.tsx` (frame), `inspector.tsx`
+ (bottom readout), `panels.tsx` (the demo controls), `pages.tsx` (the routes) — not
+ one file per component. An example is read top to bottom; keep it that way.
+- Plain CSS only. Design tokens (OKLCH colors, radius, easing) live in `:root` in
+ `src/index.css`; component styles in `src/App.css`. No Tailwind, no component
+ library, no CSS-in-JS. Keep the dependency list short.
+
+## Commands
+
+- `npm install`
+- `npm run dev` — local dev server.
+- `npm run build` — type-check (`tsc -b`) and production build.
+- `npm test` — two Playwright specs, guarding only what is Uptrace-specific: single-
+ root trace nesting, and route-pattern trace naming. They read the intercepted Sentry
+ envelopes, so no credentials and no running Uptrace are needed: `playwright.config.ts`
+ gives the test dev server a dummy DSN and ignores your `.env`, keeping them green on
+ a fresh clone. Keep it that way — the SDK is inert without a DSN (no spans, no trace
+ ids, no envelopes), so a suite that reads them must supply one itself. The app must
+ not carry test hooks: assert on what it sends, never on globals it exposes for tests.
+ Run `npm run build` and `npm test` when changing behavior, and click through the app.
diff --git a/sentry-react/README.md b/sentry-react/README.md
new file mode 100644
index 0000000..46cb1be
--- /dev/null
+++ b/sentry-react/README.md
@@ -0,0 +1,126 @@
+# React + Sentry → Uptrace
+
+A minimal React (Vite + TypeScript) app instrumented with the
+[`@sentry/react`](https://docs.sentry.io/platforms/javascript/guides/react/)
+SDK. Uptrace speaks the Sentry ingest protocol, so the SDK sends data to Uptrace
+**without any extra exporter** — you point the SDK's DSN at your Uptrace project.
+
+Every control fires one explicit Sentry signal and shows what it sent in an
+in-page inspector, so you can watch each signal type and then find it in Uptrace.
+
+- **Todos** — add a todo (a `created todo` span) and mark it **Done** (a
+ `completed todo` span whose duration is how long the todo was open).
+- **HTTP** — **OK / Slow (~5s) / Fail (500)** fetch a dev endpoint. Each request is
+ a `GET /api/…` span on the current trace, wrapping the `http.client` span the SDK
+ instruments fetch with (Fail also captures an error).
+- **Errors** — one button per type (`Error`, `TypeError`, `RangeError`,
+ `SyncError`); each is a distinct, findable issue.
+- **Routes** — a side nav lists the app's routes (Products, Categories, News,
+ Settings, Redirect, Not found). Each navigation mints a new trace named by its
+ route **pattern** (`/products/:id`, `/categories/*`), so Uptrace groups
+ navigations by route rather than by concrete URL. An unmatched path renders
+ NotFound, which reports itself as a `PageNotFound` error.
+- A **trace badge** shows the current trace id (and a link to it if you set
+ `VITE_UPTRACE_URL`); a bottom **inspector** shows what the last control sent,
+ whether it reached Uptrace (its **delivery status**), and (when
+ `VITE_UPTRACE_URL` is set) links straight to it in Uptrace — to the exact span
+ for a custom span (`/traces//`), or to the trace for other
+ signals.
+
+> Session Replay is intentionally not included: Uptrace's Sentry ingest does not
+> confirm replay support, and this example only demonstrates signals you can find
+> in Uptrace (errors, spans).
+
+## How it works
+
+The Sentry SDK builds its request URLs from the DSN
+(`http://@/` → `POST /api//envelope/`),
+which is exactly what Uptrace exposes. So instrumentation is a standard
+`Sentry.init({ dsn })` — see [`src/instrument.ts`](src/instrument.ts). The app
+never starts a trace itself: the React Router tracing integration opens a
+pageload trace on load and a navigation trace on each route change, and every
+span, request, and error attaches to the current trace.
+
+The **HTTP** panel calls `/api/ok`, `/api/slow`, and `/api/fail`, served by a
+small Vite dev-server middleware (see [`vite.config.ts`](vite.config.ts)) so
+there is **no separate backend**. These endpoints exist under `npm run dev`; a
+static `vite preview` build does not include them.
+
+### One trace, one tree
+
+Every signal on a page (custom spans, errors, HTTP) is nested under that
+page's pageload/navigation root span, so opening the trace in Uptrace shows one
+tree with everything in it. Uptrace stores one root span per trace, so signals are
+attached as children rather than as separate roots. Each interaction is therefore
+sent as its own Sentry transaction — you will see multiple transaction envelopes
+for one page in the Network tab; in Uptrace they appear as one nested tree.
+
+## Prerequisites
+
+- [Node.js](https://nodejs.org) 20 or newer.
+- A running Uptrace and a project to send data to:
+ - **Self-hosted:** [Uptrace get-started guide](https://uptrace.dev/get-started).
+ The Sentry ingest host is usually `localhost:14318`.
+ - **Uptrace Cloud:** create a project at .
+
+## 1. Get your Uptrace Sentry DSN
+
+Open your project in Uptrace → **Project → Data Source Name** → **Sentry** tab →
+copy the DSN. It looks like `http://project2_secret_token@localhost:14318/2`.
+
+## 2. Configure and run
+
+```bash
+# from this directory: examples/sentry-react
+cp .env.example .env
+# edit .env: paste your DSN into VITE_SENTRY_DSN
+# optionally set VITE_UPTRACE_URL to your Uptrace UI for clickable trace links
+
+npm install
+npm run dev
+```
+
+Open the URL Vite prints (default ).
+
+## 3. Generate and find data
+
+Use the panels, watching the inspector and delivery-status line. Then in Uptrace:
+
+- **Errors** — the four error buttons (each a distinct type).
+- **Traces / spans** — your todo spans (`created todo` / `completed todo`), the
+ request spans (`GET /api/slow` is visibly long, with the SDK's `http.client` span
+ nested inside it), and the pageload/navigation traces. Navigate between routes and
+ watch the trace id change.
+
+If nothing shows up, check that `VITE_SENTRY_DSN` is set (the app warns in the
+console if not) and that the DSN host matches your Uptrace ingest address.
+Restart `npm run dev` after editing `.env`. The delivery-status line distinguishes
+"can't reach Uptrace" (host unreachable) from "Uptrace rejected the data" (bad
+DSN key/project).
+
+## Tests
+
+```bash
+npm test
+```
+
+Two specs, covering only what is Uptrace-specific: every signal nests under one
+root span per trace, and each navigation mints a trace named by its route pattern.
+They intercept the Sentry envelopes in the browser and read them, so no Sentry
+credentials and no running Uptrace are needed — `playwright.config.ts` gives the
+test dev server a dummy DSN (the SDK is inert without one) and ignores your `.env`.
+
+## Project layout
+
+| File | Purpose |
+| --- | --- |
+| `src/instrument.ts` | `Sentry.init()` — the only Uptrace-specific wiring — plus React Router tracing. |
+| `src/main.tsx` | Imports instrumentation first; sets up the router and `Sentry.ErrorBoundary`. |
+| `src/telemetry.ts` | Every Sentry SDK call: breadcrumbs, custom spans, errors, requests, trace helpers. |
+| `src/signals.ts` | Framework-free "last signal sent" store the inspector renders. |
+| `src/pages.tsx` | The routes: home console, `/products/:id`, `/categories/*`, not-found, and the shared route body. |
+| `src/panels.tsx` | The three demo controls: todos, HTTP, errors. |
+| `src/shell.tsx` | Persistent frame: side nav, trace badge, routed content, inspector. |
+| `src/inspector.tsx` | Bottom bar: what the last control sent, and whether it reached Uptrace. |
+| `src/delivery.ts` | Observe and show whether envelopes reach Uptrace. |
+| `vite.config.ts` | Dev-server middleware for `/api/ok|slow|fail`. |
diff --git a/sentry-react/index.html b/sentry-react/index.html
new file mode 100644
index 0000000..84b7eb6
--- /dev/null
+++ b/sentry-react/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sentry-react/package-lock.json b/sentry-react/package-lock.json
new file mode 100644
index 0000000..512956b
--- /dev/null
+++ b/sentry-react/package-lock.json
@@ -0,0 +1,2037 @@
+{
+ "name": "uptrace-sentry-react-example",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "uptrace-sentry-react-example",
+ "version": "0.1.0",
+ "dependencies": {
+ "@sentry/react": "^10.57.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.18.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.56.0",
+ "@types/react": "^19.2.0",
+ "@types/react-dom": "^19.2.0",
+ "@vitejs/plugin-react": "^5.0.0",
+ "typescript": "^5.7.0",
+ "vite": "^6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
+ "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.61.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
+ "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
+ "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
+ "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
+ "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
+ "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
+ "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
+ "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
+ "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
+ "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
+ "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
+ "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
+ "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
+ "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
+ "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
+ "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
+ "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
+ "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
+ "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
+ "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
+ "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
+ "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
+ "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
+ "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
+ "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@sentry/browser": {
+ "version": "10.62.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.62.0.tgz",
+ "integrity": "sha512-uJi0yPssB3Nt/cZ8/S8opW42gaM59/6IyNtPFYD7C0ciudi/nIo5QMVpCYBBI3jnKFOIQLlsMT4pDlOLuxxNuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/browser-utils": "10.62.0",
+ "@sentry/core": "10.62.0",
+ "@sentry/feedback": "10.62.0",
+ "@sentry/replay": "10.62.0",
+ "@sentry/replay-canvas": "10.62.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/browser-utils": {
+ "version": "10.62.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser-utils/-/browser-utils-10.62.0.tgz",
+ "integrity": "sha512-mS9HVVuWIdye9o0xUGFmzNOBqktF4n5kugrF8NCOYYDrr5ZV8Cx7BlquHQn5UpCeViVhZtcDlEm4iOK7++Px7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "10.62.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/core": {
+ "version": "10.62.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.62.0.tgz",
+ "integrity": "sha512-tV69fMg2sS5DUFmQSnS7Jd5qJAp0izxwcsvBVz2ieTM9VMRi99IfOSYW9UYr3p1yfuksk41kefN5PEbeedUE+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/feedback": {
+ "version": "10.62.0",
+ "resolved": "https://registry.npmjs.org/@sentry/feedback/-/feedback-10.62.0.tgz",
+ "integrity": "sha512-d0BVjJVny6qpBgGJgWL0fbcoQHjtD3z3R8EK/KzTS3RO92JX5n3A536n5D/rh0gZFgcIwiUzBXegmyPOSQn9ng==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "10.62.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/react": {
+ "version": "10.62.0",
+ "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.62.0.tgz",
+ "integrity": "sha512-PChimVpY0wzs3H/hJqyl87/ITTHwIZWTSY68QoENZyLnp7DvLcFiZYub/gFws1pzDPhtIQXVLU72fbmUjT5PSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/browser": "10.62.0",
+ "@sentry/core": "10.62.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.14.0 || 17.x || 18.x || 19.x"
+ }
+ },
+ "node_modules/@sentry/replay": {
+ "version": "10.62.0",
+ "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-10.62.0.tgz",
+ "integrity": "sha512-rWp4hBhZOmdQhisxcKzAwTGiRk/LvWnNaElWe7nbRhjsM/usp2095yfjq4iJ47v9MtO7xxY6eUz++fLBycqXKg==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/browser-utils": "10.62.0",
+ "@sentry/core": "10.62.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/replay-canvas": {
+ "version": "10.62.0",
+ "resolved": "https://registry.npmjs.org/@sentry/replay-canvas/-/replay-canvas-10.62.0.tgz",
+ "integrity": "sha512-CzPAxmpe5US/ABGA1TzpjFKOFZN5uqlzrRh/uM9/daVuzLVKIAQ0XRNxo/PPEXvlDm/PoMdI5L0qIODuIKnyyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "10.62.0",
+ "@sentry/replay": "10.62.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
+ "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.29.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-rc.3",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.40",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz",
+ "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.4",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz",
+ "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.38",
+ "caniuse-lite": "^1.0.30001799",
+ "electron-to-chromium": "^1.5.376",
+ "node-releases": "^2.0.48",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001799",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
+ "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.381",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.381.tgz",
+ "integrity": "sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.15",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.50",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
+ "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
+ "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.61.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
+ "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.16",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
+ "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.7"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.18.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
+ "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.18.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz",
+ "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.18.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
+ "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.62.2",
+ "@rollup/rollup-android-arm64": "4.62.2",
+ "@rollup/rollup-darwin-arm64": "4.62.2",
+ "@rollup/rollup-darwin-x64": "4.62.2",
+ "@rollup/rollup-freebsd-arm64": "4.62.2",
+ "@rollup/rollup-freebsd-x64": "4.62.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.62.2",
+ "@rollup/rollup-linux-arm64-musl": "4.62.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.62.2",
+ "@rollup/rollup-linux-loong64-musl": "4.62.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.62.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.62.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-musl": "4.62.2",
+ "@rollup/rollup-openbsd-x64": "4.62.2",
+ "@rollup/rollup-openharmony-arm64": "4.62.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.62.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.62.2",
+ "@rollup/rollup-win32-x64-gnu": "4.62.2",
+ "@rollup/rollup-win32-x64-msvc": "4.62.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
+ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/sentry-react/package.json b/sentry-react/package.json
new file mode 100644
index 0000000..af30231
--- /dev/null
+++ b/sentry-react/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "uptrace-sentry-react-example",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview",
+ "test": "playwright test"
+ },
+ "dependencies": {
+ "@sentry/react": "^10.57.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.18.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.56.0",
+ "@types/react": "^19.2.0",
+ "@types/react-dom": "^19.2.0",
+ "@vitejs/plugin-react": "^5.0.0",
+ "typescript": "^5.7.0",
+ "vite": "^6.0.0"
+ }
+}
diff --git a/sentry-react/playwright.config.ts b/sentry-react/playwright.config.ts
new file mode 100644
index 0000000..0c900ca
--- /dev/null
+++ b/sentry-react/playwright.config.ts
@@ -0,0 +1,21 @@
+import { defineConfig } from '@playwright/test'
+
+// The specs read the Sentry envelopes the app sends, intercepted in the browser.
+export default defineConfig({
+ testDir: './tests',
+ reporter: 'list',
+ use: { baseURL: 'http://localhost:5173' },
+ webServer: {
+ // The dev server also serves the /api/* endpoints the HTTP panel calls.
+ command: 'npm run dev -- --port 5173 --strictPort',
+ port: 5173,
+ reuseExistingServer: !process.env.CI,
+ // A dummy DSN, not a credential: nothing listens at that host. But it must be
+ // set — with no DSN the SDK is disabled and mints no spans, trace ids or
+ // envelopes at all, which is what these specs read.
+ env: {
+ VITE_SENTRY_DSN: 'http://testtoken@127.0.0.1:14318/1',
+ VITE_UPTRACE_URL: 'http://127.0.0.1:14318',
+ },
+ },
+})
diff --git a/sentry-react/src/App.css b/sentry-react/src/App.css
new file mode 100644
index 0000000..a11af85
--- /dev/null
+++ b/sentry-react/src/App.css
@@ -0,0 +1,447 @@
+/* App.css — warm, single-column layout in the todo-app style. The design tokens
+ (paper / ink / accent / ...) live in index.css; this file only composes them. */
+
+.shell {
+ max-width: 58rem;
+ margin: 0 auto;
+ padding: clamp(1.5rem, 5vh, 3rem) 1.25rem 6rem;
+ font-size: 0.95rem;
+ line-height: 1.55;
+}
+
+/* Two columns: the nav sidebar and the main content column. */
+.layout {
+ display: flex;
+ gap: 2rem;
+ align-items: flex-start;
+}
+
+.main {
+ flex: 1;
+ min-width: 0;
+}
+
+/* Nav ------------------------------------------------------------------- */
+
+.nav {
+ flex: none;
+ width: 12rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ position: sticky;
+ top: 1.5rem;
+}
+
+.nav__link {
+ display: flex;
+ flex-direction: column;
+ gap: 0.05rem;
+ padding: 0.35rem 0.6rem;
+ border-radius: 7px;
+ color: var(--ink);
+ text-decoration: none;
+ transition: background 0.15s var(--ease);
+}
+
+.nav__link:hover {
+ background: var(--raised);
+}
+
+.nav__link.active {
+ background: var(--accent-ghost);
+}
+
+.nav__name {
+ font-size: 0.9rem;
+ font-weight: 560;
+}
+
+.nav__link.active .nav__name {
+ color: var(--accent-strong);
+}
+
+.nav__path {
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 0.76rem;
+ color: var(--muted);
+}
+
+/* Status bar: trace badge + delivery ------------------------------------ */
+
+.statusbar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.75rem 1.5rem;
+ margin-top: 0;
+ padding: 0.7rem 0.9rem;
+ background: var(--raised);
+ border: 1px solid var(--line);
+ border-radius: var(--radius);
+ font-size: 0.85rem;
+}
+
+/* Masthead + routed content --------------------------------------------- */
+
+.content {
+ margin-top: 1.25rem;
+}
+
+.tagline {
+ margin: 0.3rem 0 0;
+ color: var(--muted);
+ font-size: 0.95rem;
+}
+
+/* Panels: flat sections divided by hairlines, not repeated cards --------- */
+
+.panels {
+ display: flex;
+ flex-direction: column;
+ margin-top: 0.5rem;
+}
+
+.panel {
+ padding: 1.5rem 0;
+ border-top: 1px solid var(--line);
+}
+
+.panel:first-child {
+ border-top: none;
+ padding-top: 0.5rem;
+}
+
+.panel h2 {
+ margin: 0 0 0.15rem;
+ font-size: 0.78rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.07em;
+ color: var(--muted);
+}
+
+.panel p {
+ margin: 0 0 0.85rem;
+ color: var(--faint);
+ font-size: 0.85rem;
+}
+
+.hint {
+ margin: 0.85rem 0 0;
+ font-size: 0.8rem;
+ color: var(--faint);
+}
+
+.hint code {
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+}
+
+.panel__actions {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.panel__actions input {
+ flex: 1;
+ min-width: 11rem;
+ padding: 0.55rem 0.75rem;
+ font: inherit;
+ font-size: 0.9rem;
+ color: var(--ink);
+ background: var(--raised);
+ border: 1px solid var(--line-strong);
+ border-radius: var(--radius);
+ transition: border-color 0.15s var(--ease), box-shadow 0.15s var(--ease);
+}
+
+.panel__actions input::placeholder {
+ color: var(--faint);
+}
+
+.panel__actions input:focus {
+ outline: none;
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-ghost);
+}
+
+/* Todo list ------------------------------------------------------------- */
+
+.todos {
+ list-style: none;
+ margin: 0.85rem 0 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ /* Grow with the viewport but stop before HTTP/Errors would slide under the fixed
+ last-signal bar, then scroll inside the list. The offset is everything else in
+ the column (trace badge, Todos header/input, HTTP, Errors, inspector). */
+ max-height: max(6rem, calc(100dvh - 38rem));
+ overflow-y: auto;
+ scrollbar-gutter: stable;
+}
+
+.todo {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.5rem 0;
+ border-top: 1px solid var(--line);
+ animation: enter 0.2s var(--ease);
+}
+
+.todo:first-child {
+ border-top: none;
+}
+
+.todo__text {
+ flex: 1;
+ min-width: 0;
+ font-size: 0.9rem;
+}
+
+.todo__link {
+ flex: none;
+ display: inline-flex;
+ align-items: center;
+ color: var(--accent);
+ text-decoration: none;
+ font-size: 1.35rem;
+ line-height: 1;
+ padding: 0.3rem 0.5rem;
+ border-radius: 7px;
+ transition: background 0.15s var(--ease);
+}
+
+.todo__link:hover {
+ background: var(--accent-ghost);
+}
+
+@keyframes enter {
+ from {
+ opacity: 0;
+ transform: translateY(-4px);
+ }
+}
+
+/* Buttons --------------------------------------------------------------- */
+
+.btn {
+ padding: 0.55rem 0.95rem;
+ font: inherit;
+ font-size: 0.88rem;
+ font-weight: 560;
+ color: var(--ink);
+ background: var(--raised);
+ border: 1px solid var(--line-strong);
+ border-radius: var(--radius);
+ cursor: pointer;
+ transition: color 0.15s var(--ease), background 0.15s var(--ease),
+ border-color 0.15s var(--ease), transform 0.08s var(--ease);
+}
+
+.btn:hover:not(:disabled) {
+ border-color: var(--ink);
+}
+
+.btn:active:not(:disabled) {
+ transform: translateY(1px);
+}
+
+.btn:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+}
+
+.btn-primary {
+ color: var(--on-accent);
+ background: var(--accent);
+ border-color: transparent;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: var(--accent-strong);
+ border-color: transparent;
+}
+
+.btn-danger {
+ color: var(--danger);
+ background: transparent;
+ border-color: var(--danger);
+}
+
+.btn-danger:hover:not(:disabled) {
+ background: var(--danger-tint);
+ border-color: var(--danger);
+}
+
+/* Trace badge ----------------------------------------------------------- */
+
+.trace-badge {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.trace-badge__label {
+ color: var(--muted);
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.trace-badge__id {
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 0.78rem;
+ color: var(--ink);
+}
+
+.trace-badge__cta {
+ color: var(--accent);
+ text-decoration: none;
+}
+
+.trace-badge__cta:hover {
+ text-decoration: underline;
+}
+
+/* Delivery status ------------------------------------------------------- */
+
+.delivery {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: var(--muted);
+ font-size: 0.82rem;
+}
+
+.delivery__dot {
+ width: 0.5rem;
+ height: 0.5rem;
+ border-radius: 999px;
+ background: var(--faint);
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--faint) 16%, transparent);
+}
+
+.delivery[data-state='ok'] .delivery__dot {
+ background: var(--ok);
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--ok) 18%, transparent);
+}
+
+.delivery[data-state='failed'] .delivery__dot {
+ background: var(--danger);
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--danger) 18%, transparent);
+}
+
+.delivery[data-state='sending'] .delivery__dot {
+ background: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-ghost);
+}
+
+/* Inspector: fixed "last signal" bar with a kind-colored dot ------------- */
+
+.inspector {
+ position: fixed;
+ left: 50%;
+ bottom: 1rem;
+ transform: translateX(-50%);
+ width: min(40rem, calc(100% - 2rem));
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem 0.75rem;
+ padding: 0.6rem 0.9rem;
+ background: var(--raised);
+ border: 1px solid var(--line-strong);
+ border-radius: 12px;
+ box-shadow: 0 8px 28px oklch(0.27 0.018 60 / 0.12);
+ --signal: var(--accent);
+}
+
+/* A todo and an HTTP request are both spans in Uptrace, so they share one color. */
+.inspector[data-kind='span'],
+.inspector[data-kind='http'] {
+ --signal: var(--ok);
+}
+
+.inspector[data-kind='error'] {
+ --signal: var(--danger);
+}
+
+.inspector[data-kind='none'] {
+ --signal: var(--faint);
+}
+
+.inspector__label {
+ flex: none;
+ display: inline-flex;
+ align-items: center;
+ color: var(--muted);
+ font-size: 0.68rem;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+
+.inspector__label::before {
+ content: '';
+ display: inline-block;
+ width: 0.5rem;
+ height: 0.5rem;
+ margin-right: 0.4rem;
+ border-radius: 999px;
+ background: var(--signal);
+ box-shadow: 0 0 0 3px color-mix(in oklch, var(--signal) 18%, transparent);
+}
+
+.inspector__signal {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ flex-wrap: wrap;
+}
+
+.inspector .delivery {
+ flex: none;
+ font-size: 0.76rem;
+}
+
+.inspector__kind {
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 0.7rem;
+ padding: 0.1rem 0.45rem;
+ border-radius: 999px;
+ color: var(--signal);
+ background: color-mix(in oklch, var(--signal) 12%, transparent);
+}
+
+.inspector__link {
+ color: var(--accent);
+ text-decoration: none;
+ font-size: 0.85rem;
+}
+
+.inspector__link:hover {
+ text-decoration: underline;
+}
+
+.inspector__text {
+ font-size: 0.85rem;
+ color: var(--ink);
+}
+
+.inspector__empty {
+ color: var(--faint);
+ font-size: 0.85rem;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ * {
+ transition: none !important;
+ animation: none !important;
+ }
+}
diff --git a/sentry-react/src/delivery.ts b/sentry-react/src/delivery.ts
new file mode 100644
index 0000000..256c0d6
--- /dev/null
+++ b/sentry-react/src/delivery.ts
@@ -0,0 +1,106 @@
+// delivery.ts — observes whether telemetry envelopes actually reach the ingest
+// server and publishes the latest outcome to a tiny store the UI subscribes to.
+// It wraps the standard fetch transport; it only watches send results, it never
+// changes what or how Sentry sends. This is how the app can show "delivered" vs
+// "couldn't reach Uptrace" instead of leaving that only in the Network tab.
+import { useSyncExternalStore } from 'react'
+import * as Sentry from '@sentry/react'
+
+// DeliveryState is the lifecycle of the most recent envelope send.
+export type DeliveryState = 'idle' | 'sending' | 'ok' | 'failed'
+
+// DeliveryStatus is the current snapshot shown in the UI. `host` is the ingest
+// host from the DSN, used to name the target in the failure message.
+export interface DeliveryStatus {
+ state: DeliveryState
+ statusCode?: number
+ host: string | null
+}
+
+// The ingest host, so a failure message can name what it could not reach.
+const host = hostFromDsn(import.meta.env.VITE_SENTRY_DSN)
+
+// The current snapshot, replaced (never mutated) by emit so useSyncExternalStore
+// sees a stable reference between sends.
+let snapshot: DeliveryStatus = { state: 'idle', host }
+
+// The last snapshot that settled (ok/failed), ignoring the transient idle/sending.
+// Every send emits 'sending' first, so UI driven by the live state would flicker on
+// each action: a failure warning would blink out, a link would flash and vanish.
+let settled: DeliveryStatus | null = null
+
+const listeners = new Set<() => void>()
+
+// makeReportingTransport wraps the standard fetch transport and reports each send's
+// outcome. A rejected send (server down / CORS) is a failure; a resolved send with an
+// HTTP status >= 400 (rejected token/project) is a failure; anything else is a
+// success. The inner result/error is passed through untouched.
+export function makeReportingTransport(
+ options: Parameters[0],
+): ReturnType {
+ const inner = Sentry.makeFetchTransport(options)
+ return {
+ send: async (request) => {
+ emit({ state: 'sending' })
+ try {
+ const result = await inner.send(request)
+ const code = result?.statusCode
+ // A resolve with no statusCode (or < 400) just means the SDK accepted the
+ // send — e.g. it may have dropped/rate-limited items client-side and
+ // resolved with `{}` — not a hard guarantee the envelope was delivered.
+ emit(
+ code && code >= 400
+ ? { state: 'failed', statusCode: code }
+ : { state: 'ok', statusCode: code },
+ )
+ return result
+ } catch (err) {
+ emit({ state: 'failed' })
+ throw err
+ }
+ },
+ flush: (timeout) => inner.flush(timeout),
+ }
+}
+
+// emit replaces the snapshot, remembers it if it settled, and notifies subscribers.
+function emit(next: { state: DeliveryState; statusCode?: number }): void {
+ snapshot = { state: next.state, statusCode: next.statusCode, host }
+ if (snapshot.state === 'ok' || snapshot.state === 'failed') {
+ settled = snapshot
+ }
+ for (const listener of listeners) {
+ listener()
+ }
+}
+
+// useSettledDelivery subscribes the UI to the last settled outcome. Read it instead
+// of the live state whenever the UI reacts to delivery.
+export function useSettledDelivery(): DeliveryStatus | null {
+ return useSyncExternalStore(subscribeDelivery, getSettledSnapshot)
+}
+
+// subscribeDelivery registers a listener and returns an unsubscribe function.
+function subscribeDelivery(listener: () => void): () => void {
+ listeners.add(listener)
+ return () => listeners.delete(listener)
+}
+
+// getSettledSnapshot returns the last settled outcome, or null before the first send
+// settles (stable reference between emits).
+function getSettledSnapshot(): DeliveryStatus | null {
+ return settled
+}
+
+// hostFromDsn returns the ingest host (e.g. "localhost:14318") from the DSN, or null
+// if the DSN is absent or malformed.
+function hostFromDsn(dsn: string | undefined): string | null {
+ if (!dsn) {
+ return null
+ }
+ try {
+ return new URL(dsn).host
+ } catch {
+ return null
+ }
+}
diff --git a/sentry-react/src/index.css b/sentry-react/src/index.css
new file mode 100644
index 0000000..73b696f
--- /dev/null
+++ b/sentry-react/src/index.css
@@ -0,0 +1,46 @@
+:root {
+ /* Warm-tinted neutrals (no pure black/white) plus one cool violet accent.
+ OKLCH keeps the tints consistent in lightness. */
+ --paper: oklch(0.985 0.006 85);
+ --raised: oklch(0.998 0.004 85);
+ --ink: oklch(0.27 0.018 60);
+ --muted: oklch(0.55 0.016 70);
+ --faint: oklch(0.7 0.014 75);
+ --line: oklch(0.92 0.008 80);
+ --line-strong: oklch(0.84 0.01 80);
+
+ --accent: oklch(0.55 0.19 300);
+ --accent-strong: oklch(0.48 0.2 300);
+ --on-accent: oklch(0.99 0.01 300);
+ --accent-ghost: oklch(0.55 0.19 300 / 0.12);
+
+ --danger: oklch(0.55 0.18 25);
+ --danger-tint: oklch(0.96 0.03 25);
+ --info: oklch(0.6 0.12 230);
+ --ok: oklch(0.62 0.13 155);
+
+ --radius: 9px;
+ --ease: cubic-bezier(0.22, 1, 0.36, 1); /* ease-out-quint */
+
+ font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+
+ color: var(--ink);
+ background-color: var(--paper);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+}
+
+:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ border-radius: 4px;
+}
diff --git a/sentry-react/src/inspector.tsx b/sentry-react/src/inspector.tsx
new file mode 100644
index 0000000..b092b74
--- /dev/null
+++ b/sentry-react/src/inspector.tsx
@@ -0,0 +1,92 @@
+// The fixed bottom bar: what the last control sent, and whether it reached Uptrace.
+import { useSyncExternalStore } from 'react'
+import { subscribeSignals, getSignalSnapshot } from './signals'
+import type { SignalRecord } from './signals'
+import { uptraceUrl } from './telemetry'
+import { useSettledDelivery } from './delivery'
+import type { DeliveryStatus as Status } from './delivery'
+
+// Inspector shows what the last control sent on the left and the delivery status on
+// the right, so "what was sent" and "did it arrive" sit together. It links the signal
+// only once some envelope has reached Uptrace: a check that the connection works, not
+// that this signal's own envelope landed.
+export function Inspector() {
+ const record = useSyncExternalStore(subscribeSignals, getSignalSnapshot)
+ const delivered = useSettledDelivery()?.state === 'ok'
+ const url = record && delivered ? uptraceUrl(record.traceId, record.spanId) : null
+ return (
+
+ )
+}
+
+// uptraceKind maps the app's control type to the signal type Uptrace actually stores,
+// so the pill matches what you find there: an HTTP request is just a span.
+const uptraceKind: Record = {
+ span: 'span',
+ http: 'span',
+ error: 'error',
+}
+
+// describe turns a record into the one-line summary the Inspector shows.
+function describe(r: SignalRecord): string {
+ switch (r.kind) {
+ case 'span':
+ return `span "${r.label}", ${r.durationMs}ms`
+ case 'http':
+ return `${r.label}, ${r.detail} in ${r.durationMs}ms`
+ case 'error':
+ return `${r.errorType}: "${r.label}"`
+ }
+}
+
+// DeliveryStatus makes a broken DSN/host visible: it stays quiet until an envelope
+// fails to reach Uptrace (delivery.ts observes every send).
+function DeliveryStatus() {
+ const settled = useSettledDelivery()
+ if (settled?.state !== 'failed') {
+ return null
+ }
+ return (
+
+
+ {deliveryMessage(settled)}
+
+ )
+}
+
+// deliveryMessage is the line shown when a send fails.
+function deliveryMessage(status: Status): string {
+ switch (status.state) {
+ case 'idle':
+ return 'Waiting for the first event…'
+ case 'sending':
+ return 'Connecting to Uptrace…'
+ case 'ok':
+ return 'Connected to Uptrace ✓'
+ case 'failed':
+ // A statusCode means the host answered but rejected the data; none means we
+ // never reached it, so name the host — a wrong DSN host is the usual cause.
+ return status.statusCode
+ ? `Uptrace rejected the data (HTTP ${status.statusCode})`
+ : `Can't reach Uptrace at ${status.host ?? 'the ingest server'}`
+ }
+}
diff --git a/sentry-react/src/instrument.ts b/sentry-react/src/instrument.ts
new file mode 100644
index 0000000..c83bfed
--- /dev/null
+++ b/sentry-react/src/instrument.ts
@@ -0,0 +1,52 @@
+// Sentry initialization — the only Uptrace-specific code in the app. Imported FIRST
+// in main.tsx so instrumentation is in place before React renders. Point the DSN at
+// your Uptrace project: copy .env.example to .env (see README.md).
+import * as Sentry from '@sentry/react'
+import { useEffect } from 'react'
+import { createRoutesFromChildren, matchRoutes, useLocation, useNavigationType } from 'react-router-dom'
+import { makeReportingTransport } from './delivery'
+import { installPageRootTracking } from './telemetry'
+
+const dsn = import.meta.env.VITE_SENTRY_DSN
+
+if (!dsn) {
+ // Without a DSN the SDK is inert, so say so instead of dropping every event.
+ console.warn(
+ 'VITE_SENTRY_DSN is not set. Copy .env.example to .env and paste your ' +
+ 'Uptrace Sentry DSN, then restart `npm run dev`.',
+ )
+}
+
+Sentry.init({
+ dsn,
+
+ // Observes each send so the UI can show whether envelopes arrive; it wraps the
+ // standard fetch transport and does not change delivery (src/delivery.ts).
+ transport: makeReportingTransport,
+
+ // Opens a pageload trace on load and a navigation trace per route change, named by
+ // route pattern (/products/:id). The app never starts a trace itself.
+ integrations: [
+ Sentry.reactRouterBrowserTracingIntegration({
+ useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ ],
+
+ // Sample everything: this is a demo. Lower it in production.
+ tracesSampleRate: 1.0,
+
+ // Attaches user/IP to events. Turn off to avoid PII.
+ sendDefaultPii: true,
+
+ // An attribute on every event, so you can filter this example's data.
+ environment: 'development',
+})
+
+// Track each page's root span so signals nest under it (Uptrace keeps one root per
+// trace). Must run right after init to catch the pageload span Sentry.init just
+// started — see installPageRootTracking.
+installPageRootTracking()
diff --git a/sentry-react/src/main.tsx b/sentry-react/src/main.tsx
new file mode 100644
index 0000000..20e7894
--- /dev/null
+++ b/sentry-react/src/main.tsx
@@ -0,0 +1,55 @@
+// Import Sentry instrumentation BEFORE anything else so it is initialized
+// before React renders.
+import './instrument.ts'
+
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import * as Sentry from '@sentry/react'
+import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
+
+import { Layout } from './shell'
+import { CategoriesPage, Console, NotFound, ProductsPage, RoutePage } from './pages'
+import './index.css'
+import './App.css'
+
+// Wrapping Routes lets the router-tracing integration name navigation traces by
+// their parameterized path (/products/:id, /categories/*) rather than the URL.
+const SentryRoutes = Sentry.wrapReactRouterRouting(Routes)
+
+createRoot(document.getElementById('root')!).render(
+
+ {/* Sentry.ErrorBoundary reports any uncaught render error to Uptrace and
+ shows a fallback instead of a blank screen. */}
+ Something went wrong, check Uptrace.
}>
+
+
+ }>
+ } />
+ } />
+ } />
+
+ }
+ />
+
+ }
+ />
+ } />
+ } />
+
+
+
+
+ ,
+)
diff --git a/sentry-react/src/pages.tsx b/sentry-react/src/pages.tsx
new file mode 100644
index 0000000..0ce743f
--- /dev/null
+++ b/sentry-react/src/pages.tsx
@@ -0,0 +1,134 @@
+// The routed pages. Each navigation mints a new trace named by its route pattern.
+import { useEffect, useRef } from 'react'
+import type { ReactNode } from 'react'
+import { Link, useLocation, useParams } from 'react-router-dom'
+import { reportNamedError } from './telemetry'
+import { ErrorPanel, HttpPanel, TodoPanel } from './panels'
+
+// Console is the home route: the three signal panels.
+export function Console() {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+const PRODUCT_ERRORS = [
+ { name: 'ProductNotFound', message: 'Product not found' },
+ { name: 'ProductCreateFailed', message: 'Product creation failed' },
+]
+
+// ProductsPage is the dynamic route (/products/:id). "Next product" navigates to
+// another id, so you can watch every concrete id group into one route in Uptrace.
+export function ProductsPage() {
+ const { id } = useParams()
+ const next = (Number(id) || 0) + 1
+ return (
+
+
+ Next product (#{next}) →
+
+
+ }
+ />
+ )
+}
+
+const CATEGORY_ERRORS = [
+ { name: 'CategoryNotFound', message: 'Category not found' },
+ { name: 'CategoryCreateFailed', message: 'Category creation failed' },
+]
+
+// CategoriesPage is a catch-all route (/categories/*): the whole tail is one splat
+// param, and Uptrace names the trace /categories/*.
+export function CategoriesPage() {
+ const tail = useParams()['*'] ?? ''
+ return (
+
+ )
+}
+
+// NotFound renders for any unmatched path and reports it as a distinct PageNotFound
+// issue, so unknown routes are trackable rather than a silent navigation.
+export function NotFound() {
+ const { pathname } = useLocation()
+ const reported = useRef(null)
+
+ // Report once per path, not once per mount: every unmatched path renders this same
+ // catch-all route, so navigating between two of them keeps the component mounted.
+ // Remembering the path still absorbs StrictMode's dev double-invoke, which re-runs
+ // the effect with the pathname unchanged.
+ useEffect(() => {
+ if (reported.current === pathname) return
+ reported.current = pathname
+ reportNamedError('PageNotFound', `Page not found: ${pathname}`)
+ }, [pathname])
+
+ return (
+
+
+ No route matches {pathname}. It is reported to Uptrace as a PageNotFound error
+ on this route's trace.
+
+
+
+ Back home
+
+
+
+ )
+}
+
+// RouteError is one domain error a route offers as a button.
+export interface RouteError {
+ name: string
+ message: string
+}
+
+// RoutePage is the shared body for the sub-routes: a subtitle plus one button per
+// domain error, each attaching to this route's trace. extra renders below it.
+export function RoutePage({
+ subtitle,
+ errors,
+ extra,
+}: {
+ subtitle: string
+ errors: RouteError[]
+ extra?: ReactNode
+}) {
+ return (
+
+
{subtitle}
+ {extra}
+
+
+
Errors
+
Each error attaches to this route's trace.
+
+ {errors.map((e) => (
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/sentry-react/src/panels.tsx b/sentry-react/src/panels.tsx
new file mode 100644
index 0000000..31465e8
--- /dev/null
+++ b/sentry-react/src/panels.tsx
@@ -0,0 +1,131 @@
+// The three demo controls on the home route. Each fires one explicit Sentry signal.
+import { useState } from 'react'
+import { completeTodo, createTodo, reportError, sendRequest, uptraceUrl } from './telemetry'
+import type { ErrorType, RequestKind, Todo } from './telemetry'
+import { useSettledDelivery } from './delivery'
+
+// TodoPanel: add a todo (sends a "created" span) and mark it Done (sends a "completed"
+// span whose duration is how long the todo was open).
+export function TodoPanel() {
+ const [text, setText] = useState('')
+ const [todos, setTodos] = useState([])
+ // Only link a span once some envelope has reached Uptrace: a check that the
+ // connection works, not that this span's own envelope landed.
+ const delivered = useSettledDelivery()?.state === 'ok'
+
+ function add() {
+ const trimmed = text.trim()
+ if (!trimmed) return
+ // createTodo sends the span, so it stays out of the updater: StrictMode
+ // double-invokes updaters in dev, which would send it twice.
+ const todo = createTodo(trimmed)
+ setTodos((prev) => [...prev, todo])
+ setText('')
+ }
+
+ function done(todo: Todo) {
+ completeTodo(todo)
+ setTodos((prev) => prev.filter((t) => t.id !== todo.id))
+ }
+
+ return (
+
Fetch a dev endpoint — each produces a request span on the current trace.
+
+ {REQUESTS.map(({ kind, label }) => (
+
+ ))}
+
+
+ )
+}
+
+const TYPES: ErrorType[] = ['Error', 'TypeError', 'RangeError', 'SyncError']
+
+// ErrorPanel captures one exception per button — each a distinct type, so each is a
+// distinct issue in Uptrace.
+export function ErrorPanel() {
+ return (
+
+
Errors
+
Each button captures one exception on the current trace.
+
+ {TYPES.map((type) => (
+
+ ))}
+
+
+ )
+}
diff --git a/sentry-react/src/shell.tsx b/sentry-react/src/shell.tsx
new file mode 100644
index 0000000..5dae8ff
--- /dev/null
+++ b/sentry-react/src/shell.tsx
@@ -0,0 +1,87 @@
+// The persistent app frame: route nav on the left, current-trace badge above the
+// routed content, inspector pinned to the bottom.
+import { useSyncExternalStore } from 'react'
+import type { ReactNode } from 'react'
+import { NavLink, Outlet } from 'react-router-dom'
+import { getPageTraceId, subscribePageTrace, uptraceUrl } from './telemetry'
+import { useSettledDelivery } from './delivery'
+import { Inspector } from './inspector'
+
+// Layout is the persistent frame: the nav rail, the current-trace badge above the
+// routed content, and the inspector pinned to the bottom.
+export function Layout() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+// The demo routes. /products/42 uses an arbitrary sample id: Uptrace names its trace
+// by the pattern (/products/:id), not the id. The last is an unmatched path.
+const ROUTES = [
+ { to: '/', label: 'Home' },
+ { to: '/products/42', label: 'Products' },
+ { to: '/categories', label: 'Categories' },
+ { to: '/news', label: 'News' },
+ { to: '/settings', label: 'Settings' },
+ { to: '/redirect', label: 'Redirect' },
+ { to: '/404', label: 'Not found' },
+]
+
+// NavBar is the route list; navigating mints a new trace and the badge updates.
+function NavBar() {
+ return (
+
+ )
+}
+
+// TraceBadge names the current page's trace, and links to it only once some envelope
+// has reached Uptrace: a check that the connection works, not that this trace landed.
+function TraceBadge() {
+ const traceId = useSyncExternalStore(subscribePageTrace, getPageTraceId)
+ const settled = useSettledDelivery()
+ const url = uptraceUrl(traceId)
+
+ let linkNode: ReactNode = null
+ if (traceId && !url) {
+ linkNode = set VITE_UPTRACE_URL for a link
+ } else if (url && settled?.state === 'ok') {
+ linkNode = (
+
+ View in Uptrace ↗
+
+ )
+ }
+
+ return (
+
+ current trace
+ {traceId ?? '—'}
+ {linkNode}
+
+ )
+}
diff --git a/sentry-react/src/signals.ts b/sentry-react/src/signals.ts
new file mode 100644
index 0000000..f277130
--- /dev/null
+++ b/sentry-react/src/signals.ts
@@ -0,0 +1,44 @@
+// signals.ts — a framework-free store holding the last signal the app sent, so the
+// Inspector can show it without the devtools Network tab. telemetry.ts pushes here.
+
+// SignalKind is which signal type a record describes.
+export type SignalKind = 'span' | 'http' | 'error'
+
+// SignalRecord is one produced signal, shaped for display.
+export interface SignalRecord {
+ kind: SignalKind
+ // label is the human summary: the span/error name or the request line.
+ label: string
+ // traceId is the trace the signal attached to when it was produced.
+ traceId: string | null
+ // durationMs is set for spans and http requests (their measured time).
+ durationMs?: number
+ // spanId is set for custom spans, so the Inspector can deep-link to that span.
+ spanId?: string
+ // errorType is the error kind for errors (e.g. 'RangeError').
+ errorType?: string
+ // detail is optional extra context (e.g. an HTTP status).
+ detail?: string
+}
+
+let snapshot: SignalRecord | null = null
+const listeners = new Set<() => void>()
+
+// push records the newest signal and notifies subscribers.
+export function push(record: SignalRecord): void {
+ snapshot = record
+ for (const listener of listeners) {
+ listener()
+ }
+}
+
+// subscribeSignals registers a listener and returns an unsubscribe function.
+export function subscribeSignals(listener: () => void): () => void {
+ listeners.add(listener)
+ return () => listeners.delete(listener)
+}
+
+// getSignalSnapshot returns the latest record (stable reference between pushes).
+export function getSignalSnapshot(): SignalRecord | null {
+ return snapshot
+}
diff --git a/sentry-react/src/telemetry.ts b/sentry-react/src/telemetry.ts
new file mode 100644
index 0000000..7ca7da3
--- /dev/null
+++ b/sentry-react/src/telemetry.ts
@@ -0,0 +1,250 @@
+// telemetry.ts — every Sentry SDK call the app makes. Nothing here starts a trace:
+// each signal attaches to the current pageload/navigation trace, nested under that
+// page's root span, so Uptrace (which keeps one root per trace) draws the page as a
+// single tree. Each call also leaves a breadcrumb and pushes to the signals store.
+import * as Sentry from '@sentry/react'
+import { push } from './signals'
+
+// Base URL of the Uptrace UI. Optional: without it the badge still shows the id.
+const UPTRACE_URL = import.meta.env.VITE_UPTRACE_URL
+
+// Project id, taken from the last path segment of the DSN, needed for trace links.
+const PROJECT_ID = projectIdFromDsn(import.meta.env.VITE_SENTRY_DSN)
+
+// pageRoot is the current page's root span (the pageload/navigation transaction).
+// Interactions nest under it; it is refreshed on every navigation.
+let pageRoot: Sentry.Span | undefined
+
+// Listeners notified whenever pageRoot changes, i.e. once per pageload/navigation.
+const pageTraceListeners = new Set<() => void>()
+
+// Guards against a second spanStart listener (duplicate imports / HMR).
+let pageRootTrackingInstalled = false
+
+// installPageRootTracking tracks each page's root span. Call it once, right after
+// Sentry.init. The spanStart hook catches every later navigation span; the initial
+// pageload span needs the separate seed below.
+export function installPageRootTracking(): void {
+ if (pageRootTrackingInstalled) return
+ pageRootTrackingInstalled = true
+
+ Sentry.getClient()?.on('spanStart', (span) => {
+ const { parent_span_id: parentSpanId, op } = Sentry.spanToJSON(span)
+ if (!parentSpanId && (op === 'pageload' || op === 'navigation')) {
+ setPageRoot(span)
+ }
+ })
+
+ // The pageload span started synchronously inside Sentry.init — before the hook
+ // above existed, so it never saw it. Seed from the still-active span.
+ const active = Sentry.getActiveSpan()
+ if (active) {
+ const root = Sentry.getRootSpan(active)
+ const op = Sentry.spanToJSON(root).op
+ if (op === 'pageload' || op === 'navigation') {
+ setPageRoot(root)
+ }
+ }
+}
+
+// setPageRoot records the new page root and notifies the UI subscribed to it.
+function setPageRoot(span: Sentry.Span): void {
+ pageRoot = span
+ for (const listener of pageTraceListeners) {
+ listener()
+ }
+}
+
+// getPageTraceId returns the trace every signal on this page attaches to. Null only
+// before the first pageload span exists.
+export function getPageTraceId(): string | null {
+ return pageRoot?.spanContext().traceId ?? null
+}
+
+// subscribePageTrace registers a listener for page-root changes (one per navigation).
+export function subscribePageTrace(listener: () => void): () => void {
+ pageTraceListeners.add(listener)
+ return () => pageTraceListeners.delete(listener)
+}
+
+// uptraceUrl links into the Uptrace UI: a trace, or a span within it when spanId is
+// given (/traces// — the explore UI ignores ?span_id=). Null when
+// the UI URL, project id or trace id is missing.
+export function uptraceUrl(traceId: string | null, spanId?: string): string | null {
+ if (!UPTRACE_URL || !PROJECT_ID || !traceId) {
+ return null
+ }
+ const base = UPTRACE_URL.replace(/\/+$/, '')
+ const url = `${base}/explore/${PROJECT_ID}/traces/${traceId}`
+ return spanId ? `${url}/${spanId}` : url
+}
+
+// projectIdFromDsn returns the project id, the last path segment of the DSN
+// (e.g. "2" in http://token@host/2), or null if the DSN is absent or malformed.
+function projectIdFromDsn(dsn: string | undefined): string | null {
+ if (!dsn) {
+ return null
+ }
+ try {
+ const segments = new URL(dsn).pathname.split('/').filter(Boolean)
+ return segments.at(-1) ?? null
+ } catch {
+ return null
+ }
+}
+
+// ErrorType is the demo error kinds — varied so Uptrace groups them as distinct
+// issues instead of one repeated error.
+export type ErrorType = 'Error' | 'TypeError' | 'RangeError' | 'SyncError'
+
+// reportError captures one exception of a fixed demo kind (the home Errors panel).
+export function reportError(type: ErrorType): void {
+ breadcrumb(`Reporting a ${type}`)
+ captureAppError(type, buildError(type))
+}
+
+// reportNamedError reports an error with a custom exception name and message (the
+// per-route error buttons). Distinct names become distinct issues in Uptrace.
+export function reportNamedError(name: string, message: string): void {
+ breadcrumb(`Reporting ${name}: ${message}`)
+ captureAppError(name, Object.assign(new Error(message), { name }))
+}
+
+// captureAppError captures err inside its own span, a child of the page root. No op
+// is set, so Uptrace names the span "error: " instead of prefixing it.
+function captureAppError(type: string, err: Error): void {
+ const { traceId, spanId } = nested({ name: `error: ${type}` }, (span) => {
+ Sentry.captureException(err)
+ return span.spanContext()
+ })
+ push({ kind: 'error', label: err.message, errorType: type, spanId, traceId })
+}
+
+// buildError constructs a fresh error of the requested kind so its stack points
+// at the app.
+function buildError(type: ErrorType): Error {
+ switch (type) {
+ case 'TypeError':
+ return new TypeError("Cannot read properties of undefined (reading 'value')")
+ case 'RangeError':
+ return new RangeError('Value out of range')
+ case 'SyncError':
+ return Object.assign(new Error('Failed to sync: network request timed out'), {
+ name: 'SyncError',
+ })
+ case 'Error':
+ default:
+ return new Error('Example error')
+ }
+}
+
+// Todo is one item in the Todos panel. createdAt (ms epoch) records when it was
+// added, so completeTodo can back-date the completed span and measure the time the
+// todo was open.
+export interface Todo {
+ id: number
+ text: string
+ createdAt: number
+ // traceId/spanId of the "created todo" span, so each row can deep-link to it.
+ traceId: string
+ spanId: string
+}
+
+// nextTodoId hands out a stable id per todo for the list's React keys.
+let nextTodoId = 1
+
+// createTodo sends an instant "created todo: " span, nested under the page root.
+export function createTodo(text: string): Todo {
+ const createdAt = Date.now()
+ breadcrumb(`Created todo "${text}"`)
+ const span = Sentry.startInactiveSpan({
+ name: `created todo: ${text}`,
+ parentSpan: pageRoot,
+ forceTransaction: true,
+ })
+ const { spanId, traceId } = span.spanContext()
+ span.end()
+ push({ kind: 'span', label: `created todo: ${text}`, durationMs: 0, spanId, traceId })
+ return { id: nextTodoId++, text, createdAt, traceId, spanId }
+}
+
+// completeTodo sends a "completed todo: " span back-dated to the todo's creation,
+// so its duration is how long the todo was open.
+export function completeTodo(todo: Todo): void {
+ breadcrumb(`Completed todo "${todo.text}"`)
+ const span = Sentry.startInactiveSpan({
+ name: `completed todo: ${todo.text}`,
+ startTime: new Date(todo.createdAt),
+ parentSpan: pageRoot,
+ forceTransaction: true,
+ })
+ // Both ids come from the span we just sent: the deep link pairs them, so a
+ // scope-derived trace id could name a trace this span does not live in.
+ const { spanId, traceId } = span.spanContext()
+ span.end()
+ const durationMs = Date.now() - todo.createdAt
+ push({ kind: 'span', label: `completed todo: ${todo.text}`, durationMs, spanId, traceId })
+}
+
+// RequestKind is the three demo endpoints the HTTP panel can call.
+export type RequestKind = 'ok' | 'slow' | 'fail'
+
+// sendRequest fetches a dev endpoint inside a request span of its own, which the SDK's
+// auto-instrumented http.client span then nests under: alone, that span would be a
+// sibling root once the pageload span has ended. A non-OK response is captured as an
+// error too.
+export async function sendRequest(kind: RequestKind): Promise {
+ breadcrumb(`Sending ${kind} request`)
+ const startedAt = performance.now()
+ await nested({ name: `GET /api/${kind}`, op: 'http' }, async (span) => {
+ const { spanId, traceId } = span.spanContext()
+ try {
+ const res = await fetch(`/api/${kind}`)
+ // Semconv key, so the status is queryable in Uptrace.
+ span.setAttribute('http.response.status_code', res.status)
+ const durationMs = Math.round(performance.now() - startedAt)
+ push({
+ kind: 'http',
+ label: `GET /api/${kind}`,
+ durationMs,
+ detail: `HTTP ${res.status}`,
+ spanId,
+ traceId,
+ })
+ if (!res.ok) {
+ const err = new Error(`Request to /api/${kind} failed: HTTP ${res.status}`)
+ Sentry.captureException(err)
+ push({ kind: 'error', label: err.message, errorType: 'Error', spanId, traceId })
+ }
+ } catch (e) {
+ const durationMs = Math.round(performance.now() - startedAt)
+ Sentry.captureException(e)
+ push({
+ kind: 'http',
+ label: `GET /api/${kind}`,
+ durationMs,
+ detail: 'network error',
+ spanId,
+ traceId,
+ })
+ // The exception is on its way to Uptrace, so record it as an error signal too
+ // — as the non-OK path above does — or the Inspector would only show the span.
+ const err = e instanceof Error ? e : new Error(String(e))
+ push({ kind: 'error', label: err.message, errorType: err.name, spanId, traceId })
+ }
+ })
+}
+
+// nested runs a span as a child transaction of the page root, so the interaction
+// joins the page's tree instead of becoming a sibling root (which Uptrace drops).
+// parentSpan works even after the pageload transaction ended: only its spanContext
+// is read. Falls back to the active span/scope before the first pageload span.
+function nested(options: Parameters[0], cb: (span: Sentry.Span) => T): T {
+ return Sentry.startSpan({ ...options, parentSpan: pageRoot, forceTransaction: true }, cb)
+}
+
+// breadcrumb records an action so it appears in the breadcrumb trail of any
+// event later sent on this trace.
+export function breadcrumb(message: string): void {
+ Sentry.addBreadcrumb({ category: 'signal', message, level: 'info' })
+}
diff --git a/sentry-react/tests/helpers/envelopes.ts b/sentry-react/tests/helpers/envelopes.ts
new file mode 100644
index 0000000..b5c80c5
--- /dev/null
+++ b/sentry-react/tests/helpers/envelopes.ts
@@ -0,0 +1,54 @@
+import type { Page } from '@playwright/test'
+
+// CapturedTransaction is the trace-context of one transaction item pulled from a
+// Sentry envelope, enough to check trace/parent linkage.
+export interface CapturedTransaction {
+ traceId: string
+ spanId: string
+ parentSpanId?: string
+ op?: string
+ name?: string
+}
+
+// acceptEnvelopes answers every envelope POST with 200, standing in for a reachable
+// Uptrace: delivery settles on ok, which is what the UI gates its trace/span links on.
+export async function acceptEnvelopes(page: Page): Promise {
+ await page.route('**/envelope/**', (route) =>
+ route.fulfill({ status: 200, contentType: 'application/json', body: '{}' }),
+ )
+}
+
+// captureTransactions intercepts Sentry envelopes POSTed to the ingest endpoint,
+// fulfilling them locally (so no real Uptrace is needed) and collecting every
+// transaction item's trace context into the returned array. A Sentry envelope is
+// a header line followed by (item-header, item-payload) line pairs; transaction
+// payloads are single-line JSON.
+export async function captureTransactions(page: Page): Promise {
+ const out: CapturedTransaction[] = []
+ await page.route('**/envelope/**', async (route) => {
+ const body = route.request().postData() ?? ''
+ const [, ...items] = body.split('\n').filter(Boolean)
+ // Assumes every item is exactly two lines (item header + single-line JSON
+ // payload), which holds for this app's JSON transaction items.
+ for (let i = 0; i + 1 < items.length; i += 2) {
+ let header: { type?: string }
+ try {
+ header = JSON.parse(items[i])
+ } catch {
+ continue
+ }
+ if (header.type !== 'transaction') continue
+ const payload = JSON.parse(items[i + 1])
+ const trace = payload.contexts?.trace ?? {}
+ out.push({
+ traceId: trace.trace_id,
+ spanId: trace.span_id,
+ parentSpanId: trace.parent_span_id,
+ op: trace.op,
+ name: payload.transaction,
+ })
+ }
+ await route.fulfill({ status: 200, contentType: 'application/json', body: '{}' })
+ })
+ return out
+}
diff --git a/sentry-react/tests/navigation.spec.ts b/sentry-react/tests/navigation.spec.ts
new file mode 100644
index 0000000..7510e93
--- /dev/null
+++ b/sentry-react/tests/navigation.spec.ts
@@ -0,0 +1,22 @@
+import { test, expect } from '@playwright/test'
+import { captureTransactions } from './helpers/envelopes'
+
+// Each navigation must mint a new trace named by its route pattern, not the concrete
+// URL: that is the low-cardinality grouping Uptrace shows navigations under.
+test('navigating mints a new trace named by its route pattern', async ({ page }) => {
+ const txns = await captureTransactions(page)
+ await page.goto('/')
+ await expect.poll(() => txns.some((t) => t.op === 'pageload'), { timeout: 15_000 }).toBe(true)
+
+ await page.getByRole('link', { name: 'Products' }).click()
+ await expect(page).toHaveURL(/\/products\/42$/)
+
+ await expect
+ .poll(() => txns.find((t) => t.op === 'navigation')?.name, { timeout: 15_000 })
+ .toBe('/products/:id')
+
+ // A new trace, not a continuation of the pageload one.
+ const pageload = txns.find((t) => t.op === 'pageload')!
+ const navigation = txns.find((t) => t.op === 'navigation')!
+ expect(navigation.traceId).not.toBe(pageload.traceId)
+})
diff --git a/sentry-react/tests/nesting.spec.ts b/sentry-react/tests/nesting.spec.ts
new file mode 100644
index 0000000..f3ea456
--- /dev/null
+++ b/sentry-react/tests/nesting.spec.ts
@@ -0,0 +1,69 @@
+import { test, expect } from '@playwright/test'
+import { captureTransactions } from './helpers/envelopes'
+
+test('a completed todo span is a child of the pageload root', async ({ page }) => {
+ const txns = await captureTransactions(page)
+ await page.goto('/')
+
+ // Wait for the pageload transaction (its idle span has ended) BEFORE interacting,
+ // so the span can't piggyback on a still-active pageload span; it must rely on the
+ // tracked page root.
+ await expect.poll(() => txns.some((t) => t.op === 'pageload'), { timeout: 15_000 }).toBe(true)
+
+ await page.getByLabel('Todo text').fill('nested-todo')
+ await page.getByRole('button', { name: 'Add' }).click()
+ await page.waitForTimeout(200)
+ await page.getByRole('button', { name: 'Done' }).click()
+
+ await expect
+ .poll(() => txns.some((t) => t.name === 'completed todo: nested-todo'), { timeout: 15_000 })
+ .toBe(true)
+
+ const pageload = txns.find((t) => t.op === 'pageload')!
+ const span = txns.find((t) => t.name === 'completed todo: nested-todo')!
+ expect(span.traceId).toBe(pageload.traceId)
+ expect(span.parentSpanId).toBe(pageload.spanId)
+})
+
+test('an HTTP request span is a child of the pageload root', async ({ page }) => {
+ const txns = await captureTransactions(page)
+ await page.goto('/')
+
+ // Wait for the pageload transaction to be sent (the idle span has ended)
+ // BEFORE interacting, so the request span can no longer piggyback on the
+ // still-active pageload span via the active-span fallback — it must rely
+ // on the tracked page root instead.
+ await expect.poll(() => txns.some((t) => t.op === 'pageload'), { timeout: 15_000 }).toBe(true)
+
+ await page.getByRole('button', { name: 'OK (200)', exact: true }).click()
+
+ await expect.poll(() => txns.some((t) => t.name === 'GET /api/ok'), { timeout: 15_000 }).toBe(true)
+
+ const pageload = txns.find((t) => t.op === 'pageload')!
+ const http = txns.find((t) => t.name === 'GET /api/ok')!
+ expect(http.traceId).toBe(pageload.traceId)
+ expect(http.parentSpanId).toBe(pageload.spanId)
+})
+
+test('a captured error is a child of the pageload root', async ({ page }) => {
+ const txns = await captureTransactions(page)
+ await page.goto('/')
+
+ // Wait for the pageload transaction (its idle span has ended) BEFORE the error,
+ // so the error span can't piggyback on a still-active pageload span; it must rely
+ // on the tracked page root.
+ await expect.poll(() => txns.some((t) => t.op === 'pageload'), { timeout: 15_000 }).toBe(true)
+
+ await page.getByRole('button', { name: 'RangeError', exact: true }).click()
+
+ // The error's span carries no op, so Uptrace names it "error: RangeError"; find it
+ // by name and assert it nests under the pageload root.
+ await expect
+ .poll(() => txns.some((t) => t.name === 'error: RangeError'), { timeout: 15_000 })
+ .toBe(true)
+
+ const pageload = txns.find((t) => t.op === 'pageload')!
+ const error = txns.find((t) => t.name === 'error: RangeError')!
+ expect(error.traceId).toBe(pageload.traceId)
+ expect(error.parentSpanId).toBe(pageload.spanId)
+})
diff --git a/sentry-react/tsconfig.json b/sentry-react/tsconfig.json
new file mode 100644
index 0000000..ab81502
--- /dev/null
+++ b/sentry-react/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+
+ /* Vite injects this type for `import.meta.env` */
+ "types": ["vite/client"]
+ },
+ "include": ["src"]
+}
diff --git a/sentry-react/vite.config.ts b/sentry-react/vite.config.ts
new file mode 100644
index 0000000..8ca5c97
--- /dev/null
+++ b/sentry-react/vite.config.ts
@@ -0,0 +1,44 @@
+import { defineConfig } from 'vite'
+import type { Plugin } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// The slow endpoint's delay, long enough to show as a visibly slow span.
+const SLOW_MS = 5000
+
+// devApi serves /api/ok|slow|fail from the Vite dev server so the app can make
+// real requests (producing http.client spans) with no separate backend. These
+// exist only under `npm run dev`, not in a static `vite preview` build.
+function devApi(): Plugin {
+ return {
+ name: 'dev-api',
+ configureServer(server) {
+ server.middlewares.use((req, res, next) => {
+ const url = req.url ?? ''
+ if (url === '/api/ok') {
+ res.setHeader('content-type', 'application/json')
+ res.end(JSON.stringify({ ok: true }))
+ return
+ }
+ if (url === '/api/fail') {
+ res.statusCode = 500
+ res.setHeader('content-type', 'application/json')
+ res.end(JSON.stringify({ error: 'intentional failure' }))
+ return
+ }
+ if (url === '/api/slow') {
+ setTimeout(() => {
+ res.setHeader('content-type', 'application/json')
+ res.end(JSON.stringify({ ok: true, slow: true }))
+ }, SLOW_MS)
+ return
+ }
+ next()
+ })
+ },
+ }
+}
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react(), devApi()],
+})