@mizchi/utels
v0.1.0
Published
Sentry alternative for browser and Node.js error tracking with source maps, issue grouping, alerts, and AI-ready debugging context.
Maintainers
Readme
UTELS
ブラウザ / Node.js 向けの軽量エラートラッキング基盤。
v0 では Sentry 代替の error tracking を主導線にし、OpenTelemetry の命名と一部 semantic conventions は補助的に取り入れる。ブラウザ側 SDK は独自の最小実装にする。
Product Positioning
UTELS は browser / Node.js error tracking を主導線にした、self-host 可能な Sentry 代替の最小基盤。raw stack を送って server-side stack parsing と source map 復元を行い、同じ原因の例外を issue として集計する。UX 指標、breadcrumb、AI 向け context bundle、Playwright investigation は error 調査を補助する機能として扱う。BI / product analytics は主導線に混ぜず、必要な契約だけを src/bi/ に隔離する。
v0 の方針
- モダンブラウザ前提
- ブラウザ側は最小スクリプト
- Node.js は bearer token 付きの trusted ingest として扱う
- 送信は
navigator.sendBeacon()優先 - サーバー側は Cloudflare Workers
- UI 計測は
data-utelsによる opt-in - エラーは raw stack を送り、server-side parser / symbolication で復元する
- Web Vitals は一通り送信
- source map 復元はサーバー側
- センシティブデータは収集しない
- sampling rate を設定可能にする
主導線
ブラウザ:
import { createBrowserErrorTracker } from "@mizchi/utels/browser";
const tracker = createBrowserErrorTracker({
endpoint: "https://utels.dev/__utels?v=1",
projectId: "proj-example",
release: "2026.04.11",
buildId: "build-123",
sample: {
session: 1,
error: 1,
vital: 1,
ui: 0.1,
feature: 0.01,
},
});
tracker.captureException(new Error("manual error"));Node.js:
import { createNodeErrorTracker } from "@mizchi/utels/node";
const tracker = createNodeErrorTracker({
endpoint: "https://utels.dev/__utels?v=1",
projectId: "proj-example",
release: "2026.04.11",
buildId: "build-123",
token: process.env.UTELS_INGEST_TOKEN,
sample: {
session: 1,
error: 1,
vital: 0,
ui: 0,
feature: 0,
},
});
tracker.captureException(new Error("manual error"));src/client は低レベルの browser telemetry client として残し、利用者向けの入口は package subpath の @mizchi/utels/browser と @mizchi/utels/node に寄せる。root の @mizchi/utels は両方を re-export する。BI / product analytics は主導線から外し、追加する場合は src/bi/ 配下に隔離する。
BI layer の v1 境界と session / identity / funnel / retention の判断は src/bi/README.md と BI_LAYER_CONTRACT_V1 に固定する。
v0 の主要イベント
exceptionbrowser.web_vitalui.component.mountui.component.exposeui.component.clickui.component.unmountui.screen.viewfeature.usehttp.clientapp.jank
error.enrich は過去 client-side parser との互換イベントとして Worker 側に残すが、v0 の主経路では送らない。
exception には直近 breadcrumb を添付できる。client は ui.screen.view、feature.use、ui.component.click、失敗した http.client から breadcrumb を自動生成し、console は v0 では自動収集しない。
詳細は docs/telemetry-spec.md を参照。 今後の製品方針は docs/product-direction.md を参照。 Sentry 相当の機能整理は docs/sentry-parity.md を参照。 server-side noise filter の既定ルールは docs/noise-filter.md を参照。
Cloudflare 側の resource 構成は docs/cloudflare-infra.md を参照。 Dashboard / issue context API の JSON Schema は docs/api-schema.json を参照。 OTel export 用の metric mapping は docs/otel-metric-mapping.md を参照。 Grafana / Prometheus 向けの sample dashboard は docs/grafana-utels-dashboard.json を参照。
正式リリース前の security review、token rotation、retention、production smoke、rollback は docs/release-gates.md を gate として扱う。
GitHub Actions の例は docs/github-actions.md を参照。
workers.dev への初期リリース手順は docs/release-workers-dev.md を参照。
今後の実装タスクは TODO.md を参照。
初期状態では UTELS_BOOTSTRAP_TOKEN で route 全体を閉じられる。
自分自身への dogfood は Node tracker から synthetic error を送る。
pnpm dogfood:self -- \
--endpoint https://utels.dev \
--token "$UTELS_INGEST_TOKEN" \
--release "$(node -p 'require("./package.json").version')" \
--build-id "$(git rev-parse --short HEAD)"OTLP metrics export は D1 aggregate から batch job として実行する。
pnpm export:otlp -- \
--project-id utels-self-host \
--endpoint https://otel.example.com/v1/metrics \
--database ANALYTICS_DBOpenObserve へ試す場合は preset で endpoint と header を組み立てられる。
pnpm export:otlp -- \
--project-id utels-self-host \
--preset openobserve \
--openobserve-url https://api.openobserve.ai \
--openobserve-org org_name \
--openobserve-stream utels_metrics \
--openobserve-username [email protected] \
--openobserve-password "$OPENOBSERVE_PASSWORD"Issue Debug Context API
AI triage や bug tracking automation から、対象 issue の調査材料が揃っているかを問い合わせられる。
GET /api/projects/:publicHash/issues/:issueId/debug-context?v=1レスポンスは utels.issue-debug-context.v1 contract の JSON で、readiness.ok / score / missing / warnings / recommendedActions を返す。元データは issue detail snapshot なので、sourcemap 復元、release/runtime、browser/OS/node version、route/screen、occurrence timeline の不足を同じ API で確認できる。
AI に調査材料一式を渡す場合は context bundle API を使う。
GET /api/projects/:publicHash/issues/:issueId/context?v=1レスポンスは utels.issue-context.v1 contract の JSON で、issue detail、debug context、occurrence timeline、breakdowns、environment、route/screen、source pointer、source snippet、representative raw samples、release artifact metadata / content hash をまとめて返す。source snippet は sourcemap の sourcesContent から添付し、取得できない場合は limitations に明示する。
AI/agent に Playwright 調査を任せる場合は investigation job API を使う。
GET /api/projects/:publicHash/issues/:issueId/investigation?v=1レスポンスは utels.issue-investigation.v1 contract の JSON で、context/debug-context API、候補 URL、Chromium / Firefox / WebKit の実行対象、収集すべき evidence を返す。ローカルまたは CI の runner から実行する場合は:
pnpm investigate:issue -- \
--endpoint http://127.0.0.1:8787 \
--public-hash uph_xxx \
--issue-id iss_xxx調査結果を Worker 側に保存する場合は --upload と bootstrap token を付ける。
pnpm investigate:issue -- \
--endpoint http://127.0.0.1:8787 \
--public-hash uph_xxx \
--issue-id iss_xxx \
--upload \
--bootstrap-token "$UTELS_BOOTSTRAP_TOKEN"保存済み report は issue 単位で取得できる。
GET /api/projects/:publicHash/issues/:issueId/investigation-reports?v=1
GET /api/projects/:publicHash/issues/:issueId/investigation-reports?v=1&include=body
x-utels-bootstrap-token: <UTELS_BOOTSTRAP_TOKEN>component expose / click の trend は component / screen / event で絞り込める。
GET /api/projects/:publicHash/component-trends?v=1&event=ui.component.click&componentId=2550221387&screenId=3474889267&limit=24session / pageview 単位の直近統計は raw archive から組み立てる。高 cardinality な永続 BI 集計ではなく、debug 用の短期 API として扱う。
GET /api/projects/:publicHash/pageviews?v=1&sessionId=s1&pageViewId=p1&limit=20&archiveLimit=20sampling config は project ごとに取得でき、更新は bootstrap token が必要。
GET /api/projects/:publicHash/sampling-config?v=1
PATCH /api/projects/:publicHash/sampling-config?v=1
x-utels-bootstrap-token: <UTELS_BOOTSTRAP_TOKEN>issue 一覧は status / text / release / runtime / browser などで絞り込める。query は q の alias としても受け付ける。sort=count|last_seen|first_seen、order=desc|asc、limit、offset で stream 表示を制御する。未定義の query parameter は typo 防止のため 400 にする。
GET /api/projects/:publicHash/issues?v=1&status=open&query=TypeError&release=2026.05.07&sort=count&order=descdashboard API は snapshot.contract と x-utels-contract header で schema を versioning する。project dashboard は utels.project-dashboard.v1、UX dashboard は utels.project-ux-dashboard.v1、component trend は utels.component-trend.v1、pageview stats は utels.pageview-stats.v1、sampling config は utels.sampling-config.v1、issue detail は utels.issue-detail.v1、issue search は utels.issue-search.v1。
issue search は status=all を指定すると resolved / ignored も含めて検索する。
issue の lifecycle は bootstrap token 付き API で更新できる。
PATCH /api/projects/:publicHash/issues/:issueId/status?v=1
x-utels-bootstrap-token: <UTELS_BOOTSTRAP_TOKEN>
{
"v": 1,
"status": "resolved"
}status は open / resolved / ignored。
resolved issue に同じ canonical fingerprint の error が再発した場合は open に戻る。ignored は自動 reopen しない。
誤って分かれた issue は bootstrap token 付き API で merge / split できる。
POST /api/projects/:publicHash/issues/:targetIssueId/merge?v=1
POST /api/projects/:publicHash/issues/:targetIssueId/split?v=1
x-utels-bootstrap-token: <UTELS_BOOTSTRAP_TOKEN>
{
"v": 1,
"sourceIssueIds": ["iss_source"]
}merge は source issue の occurrence / breakdown を target issue に加算し、split 用 snapshot を issue_group_merge に保存する。split は snapshot の分だけ target から減算して source issue を復元する。
canonical fingerprint の override rule は project 単位に保存できる。
POST /api/projects/:publicHash/fingerprint-rules?v=1
GET /api/projects/:publicHash/fingerprint-rules?v=1
x-utels-bootstrap-token: <UTELS_BOOTSTRAP_TOKEN>
{
"v": 1,
"name": "Checkout hook grouping",
"priority": 10,
"match": {
"field": "top_source",
"contains": "useCheckout.ts"
},
"fingerprint": "override:checkout-hook"
}field は canonical_fingerprint / raw_fingerprint / error_type / error_message / top_source / top_function。最初に match した enabled rule が queue consumer と raw backfill の issue grouping に使われる。既存 aggregate に反映するには raw backfill を実行する。
Alert Rules API
alert rule は project 単位に保存できる。new_issue / regression / spike は queue consumer が generic webhook へ utels.alert.v1 payload を送る。
POST /api/projects/:publicHash/alert-rules?v=1
x-utels-bootstrap-token: <UTELS_BOOTSTRAP_TOKEN>
{
"v": 1,
"name": "New issue webhook",
"trigger": "new_issue",
"action": {
"type": "webhook",
"target": "https://hooks.example.com/utels"
}
}GET /api/projects/:publicHash/alert-rules?v=1
x-utels-bootstrap-token: <UTELS_BOOTSTRAP_TOKEN>action.type は webhook / slack。webhook は utels.alert.v1 をそのまま POST し、slack は Slack incoming webhook 向けの text / blocks payload に変換して送る。
action.target は http / https のみ受け付ける。SSRF 防止のため credentials 付き URL、localhost、private / link-local / reserved IP、metadata host は作成時と送信直前の両方で拒否する。
trigger は new_issue / regression / spike / rate_limited。spike は threshold.count と threshold.windowSec を使い、issue の window 内 occurrence が threshold 以上になったときに送る。regression は resolved issue が再発して open に戻るときに送る。rate_limited は ingest が 429 になったときに送る。
dashboard API は releaseRegressions に release 間の増加も返す。error_issue_breakdown_hourly の release dimension から issue ごとに最新 release と直前 release を比較し、最新 release の occurrence が増えた open issue を deltaCount 順に表示する。これは resolved issue の再発 alert とは別に、release 後に悪化した issue を一覧化するための検出である。
初期プリセットは以下を推奨する。
new_issue: 新しい原因の error を即時通知regression: resolved issue の再発を即時通知spike:threshold.count = 10,threshold.windowSec = 3600rate_limited: ingest の 429 を通知
Slack へ送る場合は上の rule の action.type を slack、action.target を Slack incoming webhook URL にする。
webhook payload:
{
"v": 1,
"contract": "utels.alert.v1",
"trigger": "new_issue",
"rule": {
"ruleId": "arule_xxx",
"name": "New issue webhook"
},
"project": {
"projectId": "proj-example"
},
"issue": {
"issueId": "iss_xxx",
"canonicalFingerprint": "TypeError|src/app.ts:10:render",
"errorType": "TypeError",
"errorMessage": "boom"
},
"spike": {
"count": 12,
"bucketStart": 1778144400,
"windowStart": 1778144400,
"threshold": {
"count": 10,
"windowSec": 3600
}
},
"regression": {
"previousStatus": "resolved",
"previousRelease": "2026.05.06",
"currentRelease": "2026.05.07"
},
"occurredAt": 1778147803853
}spike は spike alert のときだけ、regression は regression alert のときだけ入る。
delivery は alert_delivery に記録し、rule_id + dedupe_key で同じ issue への重複送信を避ける。失敗した delivery は bootstrap token 付き API で再送できる。
delivery history:
GET /api/projects/:publicHash/alert-deliveries?v=1&status=failed&include=payload
x-utels-bootstrap-token: <UTELS_BOOTSTRAP_TOKEN>status は all / pending / delivered / failed。include=payload を付けたときだけ保存済み utels.alert.v1 payload を返す。
POST /__utels/alerts/retry?v=1
x-utels-bootstrap-token: <UTELS_BOOTSTRAP_TOKEN>
{
"v": 1,
"projectId": "proj-example",
"limit": 20
}Ingest Guardrails
Worker 側で既知ノイズを queue 前に落とす。対象は ResizeObserver loop limit exceeded、extension stack、stack 無し Script error. など。
INGEST_RATE_LIMIT_FREE_PER_MINUTE / INGEST_RATE_LIMIT_PAID_PER_MINUTE を設定すると、project の plan_tier ごとに 1 分 window の rate limit を分けられる。判定は project 全体と project + origin/token 単位の両方で行う。旧 INGEST_RATE_LIMIT_PER_MINUTE は tier 別 limit が未設定のときの fallback。超過時は 429 と Retry-After を返す。
ingest_metric_hourly には accepted / invalid_payload / rate_limited / noise_dropped を outcome dimension として集計する。queue consumer 側は queue_outcome dimension に invalid_message / raw_archived / raw_archive_failed / aggregate_persisted / aggregate_failed / dead_letter_archived / dead_letter_archive_failed を記録する。DLQ 到達後は再集計せず、R2 の dead/project=<projectId>/...*.jsonl.br に退避する。
Retention / Privacy
retention policy は docs/retention-privacy.md と src/shared/privacy.ts の utels.retention-privacy-policy.v1 contract で固定する。
- raw
.jsonl.brarchive: 30 days - D1 error aggregate / ingest metric: 400 days
- alert delivery / investigation report: 90 days
- release manifest / sourcemap artifact: manual release GC
PII は SDK と Worker の両方で scrub する。対象は exception.message / exception.stacktrace / rawFingerprint で、secret key-value、Authorization header、JWT、URL credentials、email address を queue / raw archive 前に除去する。full DOM replay、input value、cookie、storage、request body は収集しない。
Raw Backfill
sourcemap を後から upload した場合は、R2 の raw .jsonl.br archive から project の error aggregate を再構築できる。
POST /__utels/backfill/raw?v=1
x-utels-bootstrap-token: <UTELS_BOOTSTRAP_TOKEN>
{
"v": 1,
"projectId": "proj-example",
"resetProjectErrors": true
}実行時は raw/project=<projectId>/ 配下の .jsonl.br を列挙し、現在の sourcemap / release manifest で error issue、occurrence、breakdown だけを作り直す。Web Vitals / UI / feature aggregate は二重加算を避けるため replay 対象にしない。確認だけなら "dryRun": true を使う。
Release Artifact Upload
release manifest と sourcemap は CLI で UTELS_ARTIFACTS 用 R2 bucket に upload する。
Project の作成
第三者向けに使わせる前に、project を登録する。
pnpm create:project -- \
--display-name "Example App" \
--plan free既存 project の一覧:
pnpm list:projectsDomain の検証タグを発行する
GA と同じように、最初は verification 用の埋め込みタグでサイト管理者であることを確認する。
pnpm create:domain -- \
--project-id proj-example \
--origin https://example.com \
--endpoint https://utels.dev返り値には verification token と、サイトへ貼る <script> tag が含まれる。
一覧確認:
pnpm list:domains -- \
--project-id proj-exampleverified_at が入った origin だけが browser ingest の許可対象になる。
Upload Token の発行
第三者向け CI upload の前に、運用者が project ごとの upload token を発行する。
pnpm create:upload-token -- \
--project-id proj-example \
--label "GitHub Actions"このコマンドは artifact_upload_token に hash だけを書き込み、平文 token は標準出力に 1 回だけ出す。CI ではその値を UTELS_UPLOAD_TOKEN として secret に保存する。
発行済み token の一覧:
pnpm list:upload-tokens -- \
--project-id proj-exampletoken の失効:
pnpm revoke:upload-token -- \
--token-id utok_xxx \
--project-id proj-exampleIngest Token の発行
Node.js など trusted runtime から Authorization: Bearer で送る場合は、upload token ではなく ingest token を使う。
pnpm create:ingest-token -- \
--project-id proj-example \
--label "Node production"このコマンドは ingest_token に hash だけを書き込み、平文 token は標準出力に 1 回だけ出す。Node.js SDK ではその値を token に渡す。
pnpm list:ingest-tokens -- \
--project-id proj-example
pnpm revoke:ingest-token -- \
--token-id itok_xxx \
--project-id proj-examplerelease 時に sourcemap がまだ無い場合は、先に manifest だけ upload できる。
pnpm upload:release -- \
--dir dist \
--project-id proj-example \
--release 2026.04.11 \
--build-id build-123 \
--asset-base-url https://cdn.example.com/assets/ \
--manifest-only \
--manifest-out dist/utels-manifest.jsonsourcemap を後から追加するときは、保存しておいた manifest を参照して sourcemap だけ upload する。
pnpm upload:release -- \
--dir dist \
--project-id proj-example \
--release 2026.04.11 \
--build-id build-123 \
--manifest dist/utels-manifest.json \
--sourcemaps-onlyVite build 時に upload する
Vite project では plugin で build 後に manifest と sourcemap を upload できる。
// vite.config.ts
import { defineConfig } from "vite";
import { utelsVitePlugin } from "@mizchi/utels/vite";
export default defineConfig({
build: {
sourcemap: true,
},
plugins: [
utelsVitePlugin({
projectId: process.env.UTELS_PROJECT_ID,
release: process.env.UTELS_RELEASE,
buildId: process.env.UTELS_BUILD_ID,
assetBaseUrl: process.env.UTELS_ASSET_BASE_URL,
endpoint: process.env.UTELS_ENDPOINT,
token: process.env.UTELS_UPLOAD_TOKEN,
}),
],
});manifest の各 asset には contentHash: "sha256:<hex>" が入り、upload された sourcemap と build artifact を照合できる。
第三者向けの CI では、wrangler の代わりに bearer token 付き HTTP upload を使える。
pnpm upload:release -- \
--dir dist \
--project-id proj-example \
--release 2026.04.11 \
--build-id build-123 \
--asset-base-url https://cdn.example.com/assets/ \
--endpoint https://utels.dev \
--token "$UTELS_UPLOAD_TOKEN"UTELS_PROJECT_ID UTELS_ENDPOINT UTELS_UPLOAD_TOKEN UTELS_RELEASE UTELS_BUILD_ID UTELS_ASSET_BASE_URL を環境変数で渡せば、CI では pnpm upload:release -- --dir dist だけでも動く。utel / UTEL_* 互換 alias は提供しない。
artifact は project 単位の key に保存され、公開 read endpoint は持たない。source map は worker 内の symbolication からだけ参照される。
browser ingest は Origin / Referer を verified_domain と照合して許可する。public browser path は CORS preflight を避けるため、text/plain の simple request 前提で送る。Node.js ingest は Authorization: Bearer <token> を使う。
注意:
- 後付けした sourcemap は以後の symbolication に使われる
- すでに集計済みの過去 raw event を再解決する backfill job はまだ未実装
- upload token は project ごとに分ける
- upload token の平文は再表示できない前提で扱う
- revoke は
revoked_atを立てるだけで物理削除しない
dry run:
pnpm upload:release -- \
--dir dist \
--project-id proj-example \
--release 2026.04.11 \
--build-id build-123 \
--asset-base-url https://cdn.example.com/assets/ \
--dry-runhello サンプルの aggregate を確認するときは:
pnpm inspect:helloブラウザの dashboard は:
https://utels.dev/projects/<publicHash>/dashboard
https://utels.dev/projects/<publicHash>/uxpublicHash は pnpm create:project の返り値、または pnpm list:projects の出力に含まれる。dashboard は最新 1 件ではなく、component / feature / web vital / error の集計値を表示する。web vital は histogram 由来の p75 / p90 / p99 upper bound を表示する。error は open issue 数と occurrence count を分け、issue list は total_count の多い順に表示する。
UX dashboard は route / screen ごとに views、UI event、HTTP failure、jank blocking をまとめ、Web Vitals は route ごとの percentile として表示する。
異なる browser で同じ原因の error が出た場合は、sourcemap 解決後の exception.type + original source + line + function を canonical fingerprint にして同じ issue に寄せる。sourcemap が無い場合は raw stack 由来の fallback fingerprint を使う。
同じ集計値は JSON API でも取れる。
https://utels.dev/api/projects/<publicHash>/dashboard?v=1
https://utels.dev/api/projects/<publicHash>/ux?v=1
https://utels.dev/api/projects/<publicHash>/component-trends?v=1&event=all
https://utels.dev/api/projects/<publicHash>/pageviews?v=1&archiveLimit=20
https://utels.dev/api/projects/<publicHash>/sampling-config?v=1