@volter/twin-sentry 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +142 -0
- package/client/sentry-mirror.css +99 -0
- package/client/sentry-mirror.tsx +352 -0
- package/dist/client/sentry-mirror.bundle.js +321 -0
- package/dist/client/sentry-mirror.css +99 -0
- package/dist/client/sentry-mirror.d.ts +17 -0
- package/dist/client/sentry-mirror.js +156 -0
- package/dist/client/sentry-mirror.tsx +352 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +36 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.js +75 -0
- package/dist/src/sentry-budget.d.ts +50 -0
- package/dist/src/sentry-budget.js +145 -0
- package/dist/src/sentry-capabilities.d.ts +3 -0
- package/dist/src/sentry-capabilities.js +1180 -0
- package/dist/src/sentry-conformance.d.ts +22 -0
- package/dist/src/sentry-conformance.js +97 -0
- package/dist/src/sentry-connector.d.ts +77 -0
- package/dist/src/sentry-connector.js +226 -0
- package/dist/src/sentry-events.d.ts +54 -0
- package/dist/src/sentry-events.js +131 -0
- package/dist/src/sentry-ingest.d.ts +137 -0
- package/dist/src/sentry-ingest.js +387 -0
- package/dist/src/sentry-mirror-ui.d.ts +38 -0
- package/dist/src/sentry-mirror-ui.js +162 -0
- package/dist/src/sentry-perform-harness.d.ts +9 -0
- package/dist/src/sentry-perform-harness.js +20 -0
- package/dist/src/sentry-server.d.ts +14 -0
- package/dist/src/sentry-server.js +27 -0
- package/dist/src/sentry-twin.d.ts +17 -0
- package/dist/src/sentry-twin.js +1739 -0
- package/dist/test-fixtures/sentry-openapi-operations.SOURCE.md +24 -0
- package/dist/test-fixtures/sentry-openapi-operations.json +1837 -0
- package/package.json +75 -0
- package/src/cli.ts +34 -0
- package/src/index.ts +145 -0
- package/src/sentry-budget.ts +171 -0
- package/src/sentry-capabilities.ts +1222 -0
- package/src/sentry-conformance.ts +110 -0
- package/src/sentry-connector.ts +260 -0
- package/src/sentry-events.ts +171 -0
- package/src/sentry-ingest.ts +471 -0
- package/src/sentry-mirror-ui.ts +162 -0
- package/src/sentry-perform-harness.ts +19 -0
- package/src/sentry-server.ts +35 -0
- package/src/sentry-twin.ts +1615 -0
- package/test-fixtures/sentry-openapi-operations.SOURCE.md +24 -0
- package/test-fixtures/sentry-openapi-operations.json +1837 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Sentry MIRROR UI — a Sentry-dashboard-like view served as a React/TSX app (Bun-bundled,
|
|
2
|
+
// the repo convention). It renders by consuming the twin's OWN Web API on the same origin
|
|
3
|
+
// (/api/0/...) — the same endpoints a real client uses. API↔UI parity: every screen is
|
|
4
|
+
// backed by real twin state, never a hand-made fixture.
|
|
5
|
+
//
|
|
6
|
+
// PURE FRONTEND (R3): the mirror imports no handler and no twin internals — it MOUNTS the
|
|
7
|
+
// pack's OWN fetch adapter (createSentryTwinFetch) as its API backend and reads every byte
|
|
8
|
+
// of state back over the wire, exactly as a browser does.
|
|
9
|
+
//
|
|
10
|
+
// The pure format/level helpers live here so they can be unit-tested AND shared by the
|
|
11
|
+
// React client (Bun tree-shakes the server-only exports out of the browser bundle).
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { bundleClient, fileResponse } from '@volter/world-core';
|
|
14
|
+
import { serveHttp } from '@volter/world-core';
|
|
15
|
+
import { createSentryTwinFetch } from "./sentry-server.js";
|
|
16
|
+
import { DEFAULT_ORG, DEFAULT_PROJECT } from "./sentry-ingest.js";
|
|
17
|
+
const CLIENT_ENTRY = () => new URL('../client/sentry-mirror.tsx', import.meta.url).pathname; // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
|
|
18
|
+
const CLIENT_CSS = () => new URL('../client/sentry-mirror.css', import.meta.url).pathname; // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
|
|
19
|
+
/** Map a Sentry level to a UI tone class. */
|
|
20
|
+
export function levelTone(level) {
|
|
21
|
+
const l = String(level ?? '').toLowerCase();
|
|
22
|
+
if (l === 'fatal' || l === 'error' || l === 'warning' || l === 'info' || l === 'debug')
|
|
23
|
+
return l;
|
|
24
|
+
return 'muted';
|
|
25
|
+
}
|
|
26
|
+
/** Map an issue status to a UI tone class. */
|
|
27
|
+
export function statusTone(status) {
|
|
28
|
+
const s = String(status ?? '').toLowerCase();
|
|
29
|
+
if (s === 'resolved')
|
|
30
|
+
return 'resolved';
|
|
31
|
+
if (s === 'ignored' || s === 'muted')
|
|
32
|
+
return 'ignored';
|
|
33
|
+
if (s === 'unresolved')
|
|
34
|
+
return 'unresolved';
|
|
35
|
+
return 'muted';
|
|
36
|
+
}
|
|
37
|
+
/** "12.3k", "1.2M", "999" — Sentry's compact event-count formatting. */
|
|
38
|
+
export function formatCount(value) {
|
|
39
|
+
const n = Number(value);
|
|
40
|
+
if (!Number.isFinite(n))
|
|
41
|
+
return '0';
|
|
42
|
+
if (n >= 1_000_000)
|
|
43
|
+
return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`;
|
|
44
|
+
if (n >= 1_000)
|
|
45
|
+
return `${(n / 1_000).toFixed(1).replace(/\.0$/, '')}k`;
|
|
46
|
+
return String(Math.trunc(n));
|
|
47
|
+
}
|
|
48
|
+
/** Relative "time ago" for a lastSeen/firstSeen ISO string (deterministic given `now`). */
|
|
49
|
+
export function timeAgo(iso, now = Date.now()) {
|
|
50
|
+
if (typeof iso !== 'string' || !iso)
|
|
51
|
+
return '—';
|
|
52
|
+
const t = Date.parse(iso);
|
|
53
|
+
if (Number.isNaN(t))
|
|
54
|
+
return '—';
|
|
55
|
+
const s = Math.max(0, Math.floor((now - t) / 1000));
|
|
56
|
+
if (s < 60)
|
|
57
|
+
return `${s}s ago`;
|
|
58
|
+
if (s < 3600)
|
|
59
|
+
return `${Math.floor(s / 60)}m ago`;
|
|
60
|
+
if (s < 86400)
|
|
61
|
+
return `${Math.floor(s / 3600)}h ago`;
|
|
62
|
+
return `${Math.floor(s / 86400)}d ago`;
|
|
63
|
+
}
|
|
64
|
+
export function latestStacktrace(event) {
|
|
65
|
+
if (!event || typeof event !== 'object')
|
|
66
|
+
return [];
|
|
67
|
+
const entries = event.entries;
|
|
68
|
+
if (!Array.isArray(entries))
|
|
69
|
+
return [];
|
|
70
|
+
const ex = entries.find((e) => e && typeof e === 'object' && e.type === 'exception');
|
|
71
|
+
const values = ex && ex.data?.values;
|
|
72
|
+
if (!Array.isArray(values))
|
|
73
|
+
return [];
|
|
74
|
+
const top = values[values.length - 1];
|
|
75
|
+
const frames = top?.stacktrace?.frames;
|
|
76
|
+
return Array.isArray(frames) ? frames : [];
|
|
77
|
+
}
|
|
78
|
+
/** The event's tags as a flat [key,value] list for rendering. */
|
|
79
|
+
export function eventTags(event) {
|
|
80
|
+
if (!event || typeof event !== 'object')
|
|
81
|
+
return [];
|
|
82
|
+
const tags = event.tags;
|
|
83
|
+
if (!Array.isArray(tags))
|
|
84
|
+
return [];
|
|
85
|
+
return tags
|
|
86
|
+
.filter((t) => t && typeof t === 'object')
|
|
87
|
+
.map((t) => ({ key: String(t.key), value: String(t.value) }));
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Server-only: bundle the React client + serve it alongside the twin Web API.
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
let clientBundle = null;
|
|
93
|
+
export function buildSentryMirrorClient() {
|
|
94
|
+
if (!clientBundle) {
|
|
95
|
+
clientBundle = bundleClient(CLIENT_ENTRY())
|
|
96
|
+
.catch((error) => { clientBundle = null; throw error; });
|
|
97
|
+
}
|
|
98
|
+
return clientBundle;
|
|
99
|
+
}
|
|
100
|
+
const APP_SHELL = `<!doctype html>
|
|
101
|
+
<html lang="en">
|
|
102
|
+
<head>
|
|
103
|
+
<meta charset="utf-8" />
|
|
104
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
105
|
+
<base href="/"><title>Sentry (twin)</title>
|
|
106
|
+
<link rel="stylesheet" href="assets/styles.css" />
|
|
107
|
+
</head>
|
|
108
|
+
<body>
|
|
109
|
+
<div id="root"></div>
|
|
110
|
+
<script type="module" src="assets/app.js"></script>
|
|
111
|
+
</body>
|
|
112
|
+
</html>`;
|
|
113
|
+
/** Serve the Sentry dashboard mirror UI (React app) + its backing Web API. */
|
|
114
|
+
export async function createSentryMirrorServer(options = {}) {
|
|
115
|
+
const twin = createSentryTwinFetch(options);
|
|
116
|
+
const server = await serveHttp({
|
|
117
|
+
// LOOPBACK-SPECIFIC bind (2026-08-20, the roving ui-verify flake): with the default
|
|
118
|
+
// wildcard hostname, `port: 0` can be handed a port some long-running app already LISTENS
|
|
119
|
+
// on at 127.0.0.1 (SO_REUSEADDR allows the overlapping non-identical bind), and the more
|
|
120
|
+
// specific loopback listener then shadows this server for every 127.0.0.1 fetch — the
|
|
121
|
+
// verify talks to a STRANGER (captured: a desktop app's asset server answering 404s on the
|
|
122
|
+
// mirror's port). Binding 127.0.0.1 makes the kernel allocate a port that is actually free
|
|
123
|
+
// on loopback, so the verify's fetches deterministically reach THIS server.
|
|
124
|
+
hostname: '127.0.0.1',
|
|
125
|
+
port: options.port ?? 0,
|
|
126
|
+
idleTimeout: 60,
|
|
127
|
+
async fetch(request) {
|
|
128
|
+
const url = new URL(request.url);
|
|
129
|
+
if (request.method === 'GET' && url.pathname === '/assets/app.js') {
|
|
130
|
+
try {
|
|
131
|
+
return new Response(await buildSentryMirrorClient(), { headers: { 'content-type': 'text/javascript; charset=utf-8' } });
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
return new Response(String(error), { status: 500 });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (request.method === 'GET' && url.pathname === '/assets/styles.css') {
|
|
138
|
+
return fileResponse(CLIENT_CSS(), { headers: { 'content-type': 'text/css; charset=utf-8' } });
|
|
139
|
+
}
|
|
140
|
+
if (request.method === 'GET' && (url.pathname === '/' || url.pathname === '')) {
|
|
141
|
+
return new Response(APP_SHELL, { headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
142
|
+
}
|
|
143
|
+
// everything else → the twin's OWN FETCH ADAPTER (composition, R2): the Web API the React
|
|
144
|
+
// client calls (/api/0/...), the SDK's ingest path (/api/:projectId/envelope/) and the
|
|
145
|
+
// uniform doors (GET /twin) all come from the SAME closure `createSentryTwinServer` serves,
|
|
146
|
+
// so the mirror port cannot drift from the API port — one serving code path, one truth.
|
|
147
|
+
return twin(request);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
151
|
+
}
|
|
152
|
+
/** The app-shell HTML (pure, for tests). */
|
|
153
|
+
export function sentryMirrorHtml() {
|
|
154
|
+
return APP_SHELL;
|
|
155
|
+
}
|
|
156
|
+
/** The mirror's stylesheet, for a host serving the shell's `assets/styles.css` itself (the hosted mirror mount). */
|
|
157
|
+
export function sentryMirrorStyles() {
|
|
158
|
+
return readFile(CLIENT_CSS(), 'utf8');
|
|
159
|
+
}
|
|
160
|
+
/** The default org/project the mirror lists issues for (matches the seeded defaults). */
|
|
161
|
+
export const MIRROR_DEFAULT_ORG = DEFAULT_ORG;
|
|
162
|
+
export const MIRROR_DEFAULT_PROJECT = DEFAULT_PROJECT;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type SentryExecute } from './sentry-connector.js';
|
|
2
|
+
export declare function performPending(execute: SentryExecute, opts: {
|
|
3
|
+
root?: string;
|
|
4
|
+
occurredAt: string;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
pushed: number;
|
|
7
|
+
confirmed: string[];
|
|
8
|
+
externalIds: Record<string, string>;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// A MINIATURE OF THE HEAD, for this pack's own claims and suites (protocol 2). Not on the serve path.
|
|
2
|
+
import { confirmAction, deployableEntries, worldNow } from '@volter/world-core';
|
|
3
|
+
import { pushSentryAction } from "./sentry-connector.js";
|
|
4
|
+
export async function performPending(execute, opts) {
|
|
5
|
+
const confirmed = [];
|
|
6
|
+
const externalIds = {};
|
|
7
|
+
for (const entry of deployableEntries('sentry', opts.root)) {
|
|
8
|
+
if (entry.operation !== 'issue.update')
|
|
9
|
+
continue; // only triage crosses
|
|
10
|
+
const { externalId } = await pushSentryAction(execute, { operation: entry.operation, subject: entry.subject, fields: entry.fields ?? {} });
|
|
11
|
+
confirmAction({
|
|
12
|
+
service: 'sentry', actionId: entry.id, subject: entry.subject, fields: entry.fields ?? {},
|
|
13
|
+
occurredAt: opts.occurredAt ?? worldNow(), vendorSubjectId: externalId, receipt: { status: 'deployed' },
|
|
14
|
+
...(opts.root !== undefined ? { root: opts.root } : {}),
|
|
15
|
+
});
|
|
16
|
+
confirmed.push(entry.id);
|
|
17
|
+
externalIds[entry.id] = externalId;
|
|
18
|
+
}
|
|
19
|
+
return { pushed: confirmed.length, confirmed, externalIds };
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Options every Sentry-twin HTTP surface needs, independent of who owns the socket. */
|
|
2
|
+
export interface SentryTwinFetchOptions {
|
|
3
|
+
root?: string;
|
|
4
|
+
readOnly?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function createSentryTwinFetch(options?: SentryTwinFetchOptions): (request: Request) => Promise<Response>;
|
|
7
|
+
export declare function createSentryTwinServer(options?: {
|
|
8
|
+
root?: string;
|
|
9
|
+
port?: number;
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
port: number;
|
|
13
|
+
stop: () => void;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Sentry twin HTTP server — serve the full Sentry twin handler over HTTP so the real
|
|
2
|
+
// `@sentry/node` SDK (pointed at a twin DSN whose host is this server) POSTs captured
|
|
3
|
+
// events to /api/:projectId/envelope/ and they become grouped issues, AND the Web API
|
|
4
|
+
// (/api/0/...) answers reads/writes. Writable by default; pass readOnly to reject writes.
|
|
5
|
+
// No real Sentry is ever contacted (D4) — fully local + offline.
|
|
6
|
+
//
|
|
7
|
+
// FETCH-FIRST (runtime contract R12b): the serve path is the plain fetch below, built from the
|
|
8
|
+
// kernel's ONE adaptation (`createTwinFetchFromHandler`); this file contributes only VALUES. The
|
|
9
|
+
// vendor's 204 deletes return `{ status: 204, body: null }`, which the adapter's null-body rule
|
|
10
|
+
// serves as a genuinely empty response. The server is one line of Bun.serve around that closure.
|
|
11
|
+
import { serveHttp } from '@volter/world-core';
|
|
12
|
+
import { handleSentryTwinRequest } from "./sentry-twin.js";
|
|
13
|
+
import { createTwinFetchFromHandler, statefulTwinManifest } from '@volter/world-core';
|
|
14
|
+
export function createSentryTwinFetch(options = {}) {
|
|
15
|
+
return createTwinFetchFromHandler(handleSentryTwinRequest, {
|
|
16
|
+
...options,
|
|
17
|
+
manifest: statefulTwinManifest({ vendor: 'sentry', twinOf: 'the Sentry error-monitoring API', stores: 'ingested error/transaction envelopes, readable back per project' }),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export async function createSentryTwinServer(options = {}) {
|
|
21
|
+
const server = await serveHttp({
|
|
22
|
+
port: options.port ?? 0,
|
|
23
|
+
idleTimeout: 60,
|
|
24
|
+
fetch: createSentryTwinFetch(options),
|
|
25
|
+
});
|
|
26
|
+
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type SentryWebhookDelivery } from './sentry-events.js';
|
|
2
|
+
export type SentryRequest = {
|
|
3
|
+
method: string;
|
|
4
|
+
path: string;
|
|
5
|
+
body?: string;
|
|
6
|
+
headers?: Record<string, string | undefined>;
|
|
7
|
+
occurredAt?: string;
|
|
8
|
+
root?: string;
|
|
9
|
+
readOnly?: boolean;
|
|
10
|
+
/** Injected webhook deliverer (fake in tests / HTTP live). Offline by default (no-op). */
|
|
11
|
+
deliver?: SentryWebhookDelivery;
|
|
12
|
+
};
|
|
13
|
+
export type SentryResponse = {
|
|
14
|
+
status: number;
|
|
15
|
+
body: unknown;
|
|
16
|
+
};
|
|
17
|
+
export declare function handleSentryTwinRequest(req: SentryRequest): Promise<SentryResponse>;
|