@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.
Files changed (50) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +142 -0
  3. package/client/sentry-mirror.css +99 -0
  4. package/client/sentry-mirror.tsx +352 -0
  5. package/dist/client/sentry-mirror.bundle.js +321 -0
  6. package/dist/client/sentry-mirror.css +99 -0
  7. package/dist/client/sentry-mirror.d.ts +17 -0
  8. package/dist/client/sentry-mirror.js +156 -0
  9. package/dist/client/sentry-mirror.tsx +352 -0
  10. package/dist/src/cli.d.ts +2 -0
  11. package/dist/src/cli.js +36 -0
  12. package/dist/src/index.d.ts +15 -0
  13. package/dist/src/index.js +75 -0
  14. package/dist/src/sentry-budget.d.ts +50 -0
  15. package/dist/src/sentry-budget.js +145 -0
  16. package/dist/src/sentry-capabilities.d.ts +3 -0
  17. package/dist/src/sentry-capabilities.js +1180 -0
  18. package/dist/src/sentry-conformance.d.ts +22 -0
  19. package/dist/src/sentry-conformance.js +97 -0
  20. package/dist/src/sentry-connector.d.ts +77 -0
  21. package/dist/src/sentry-connector.js +226 -0
  22. package/dist/src/sentry-events.d.ts +54 -0
  23. package/dist/src/sentry-events.js +131 -0
  24. package/dist/src/sentry-ingest.d.ts +137 -0
  25. package/dist/src/sentry-ingest.js +387 -0
  26. package/dist/src/sentry-mirror-ui.d.ts +38 -0
  27. package/dist/src/sentry-mirror-ui.js +162 -0
  28. package/dist/src/sentry-perform-harness.d.ts +9 -0
  29. package/dist/src/sentry-perform-harness.js +20 -0
  30. package/dist/src/sentry-server.d.ts +14 -0
  31. package/dist/src/sentry-server.js +27 -0
  32. package/dist/src/sentry-twin.d.ts +17 -0
  33. package/dist/src/sentry-twin.js +1739 -0
  34. package/dist/test-fixtures/sentry-openapi-operations.SOURCE.md +24 -0
  35. package/dist/test-fixtures/sentry-openapi-operations.json +1837 -0
  36. package/package.json +75 -0
  37. package/src/cli.ts +34 -0
  38. package/src/index.ts +145 -0
  39. package/src/sentry-budget.ts +171 -0
  40. package/src/sentry-capabilities.ts +1222 -0
  41. package/src/sentry-conformance.ts +110 -0
  42. package/src/sentry-connector.ts +260 -0
  43. package/src/sentry-events.ts +171 -0
  44. package/src/sentry-ingest.ts +471 -0
  45. package/src/sentry-mirror-ui.ts +162 -0
  46. package/src/sentry-perform-harness.ts +19 -0
  47. package/src/sentry-server.ts +35 -0
  48. package/src/sentry-twin.ts +1615 -0
  49. package/test-fixtures/sentry-openapi-operations.SOURCE.md +24 -0
  50. package/test-fixtures/sentry-openapi-operations.json +1837 -0
@@ -0,0 +1,352 @@
1
+ // Sentry UI mirror — React/TSX client (Bun-bundled). A Sentry-dashboard-style view:
2
+ // top bar + left nav + an Issues stream (grouped errors with count / last-seen / level /
3
+ // status), an Issue detail (latest event stacktrace + tags), and Releases — all backed by
4
+ // REAL twin state via the twin's own Web API (/api/0/...). API↔UI parity.
5
+ //
6
+ // Pure format/level helpers live in ../src/sentry-mirror-ui.ts (Bun tree-shakes the
7
+ // server-only exports out of this browser bundle).
8
+ import React, { useEffect, useMemo, useState } from 'react';
9
+ import { createRoot } from 'react-dom/client';
10
+ import {
11
+ levelTone, statusTone, formatCount, timeAgo, latestStacktrace, eventTags,
12
+ MIRROR_DEFAULT_ORG, MIRROR_DEFAULT_PROJECT,
13
+ type IssueRow, type StackFrame,
14
+ } from '../src/sentry-mirror-ui.ts';
15
+
16
+ // WHERE THIS MIRROR LIVES. Served at a vendor root the base is '' (fetches are root-relative, as
17
+ // before); served under a path prefix with a <base> tag every read and write resolves inside that
18
+ // prefix instead of escaping it.
19
+ const WIRE_BASE = typeof document === 'undefined' || document.querySelector('base[href]') === null ? '' : new URL('.', document.baseURI).pathname.replace(/\/$/, '');
20
+
21
+ type Nav = 'issues' | 'dashboards' | 'discover' | 'releases' | 'projects' | 'alerts' | 'settings';
22
+
23
+ const NAV: Array<{ key: Nav; label: string }> = [
24
+ { key: 'issues', label: 'Issues' },
25
+ { key: 'dashboards', label: 'Dashboards' },
26
+ { key: 'discover', label: 'Discover' },
27
+ { key: 'releases', label: 'Releases' },
28
+ { key: 'projects', label: 'Projects' },
29
+ { key: 'alerts', label: 'Alerts' },
30
+ { key: 'settings', label: 'Settings' },
31
+ ];
32
+
33
+ async function api(path: string): Promise<any> {
34
+ const res = await fetch(`${WIRE_BASE}/api/0${path}`);
35
+ try { return await res.json(); } catch { return null; }
36
+ }
37
+
38
+ function LevelPill({ level }: { level: any }) {
39
+ return <span className={`pill level-${levelTone(level)}`}>{String(level ?? '').toUpperCase() || '—'}</span>;
40
+ }
41
+ function StatusPill({ status }: { status: any }) {
42
+ return <span className={`pill status-${statusTone(status)}`}>{String(status ?? '').replace(/_/g, ' ') || '—'}</span>;
43
+ }
44
+
45
+ function IssueRowView({ issue, selected, onSelect }: { issue: IssueRow; selected: boolean; onSelect: () => void }) {
46
+ return (
47
+ <button className={`issue-row ${selected ? 'selected' : ''}`} onClick={onSelect}>
48
+ <div className="issue-main">
49
+ <div className="issue-title">{issue.title}</div>
50
+ <div className="issue-culprit mono">{issue.culprit || issue.shortId}</div>
51
+ </div>
52
+ <div className="issue-meta">
53
+ <LevelPill level={issue.level} />
54
+ <StatusPill status={issue.status} />
55
+ <span className="issue-count" title="event count">{formatCount(issue.count)} events</span>
56
+ <span className="issue-lastseen">{timeAgo(issue.lastSeen)}</span>
57
+ </div>
58
+ </button>
59
+ );
60
+ }
61
+
62
+ function Stacktrace({ frames }: { frames: StackFrame[] }) {
63
+ if (frames.length === 0) return <div className="empty">No stacktrace</div>;
64
+ return (
65
+ <div className="stacktrace">
66
+ {frames.map((f, i) => (
67
+ <div className="frame mono" key={i}>
68
+ <span className="frame-fn">{f.function ?? '?'}</span>
69
+ <span className="frame-file">{f.filename ?? f.module ?? ''}{f.lineno ? `:${f.lineno}` : ''}</span>
70
+ </div>
71
+ ))}
72
+ </div>
73
+ );
74
+ }
75
+
76
+ function IssueDetail({ issue }: { issue: IssueRow }) {
77
+ const [event, setEvent] = useState<any>(null);
78
+ useEffect(() => {
79
+ let live = true;
80
+ api(`/issues/${issue.id}/events/latest/`).then((e) => { if (live) setEvent(e); });
81
+ return () => { live = false; };
82
+ }, [issue.id]);
83
+ const frames = useMemo(() => latestStacktrace(event), [event]);
84
+ const tags = useMemo(() => eventTags(event), [event]);
85
+ return (
86
+ <div className="issue-detail">
87
+ <h2 className="detail-title">{issue.title}</h2>
88
+ <div className="detail-sub mono">{issue.culprit}</div>
89
+ <div className="detail-stats">
90
+ <div><span className="stat-label">Events</span><span className="stat-value">{formatCount(issue.count)}</span></div>
91
+ <div><span className="stat-label">First seen</span><span className="stat-value">{timeAgo(issue.firstSeen)}</span></div>
92
+ <div><span className="stat-label">Last seen</span><span className="stat-value">{timeAgo(issue.lastSeen)}</span></div>
93
+ <div><span className="stat-label">Status</span><StatusPill status={issue.status} /></div>
94
+ </div>
95
+ <h3>Latest Event Stacktrace</h3>
96
+ <Stacktrace frames={frames} />
97
+ <h3>Tags</h3>
98
+ {tags.length === 0 ? <div className="empty">No tags</div> : (
99
+ <div className="tags">{tags.map((t) => <span className="tag mono" key={t.key}>{t.key}: {t.value}</span>)}</div>
100
+ )}
101
+ </div>
102
+ );
103
+ }
104
+
105
+ function IssuesScreen() {
106
+ const [issues, setIssues] = useState<IssueRow[]>([]);
107
+ const [selected, setSelected] = useState<string | null>(null);
108
+ const [query, setQuery] = useState('is:unresolved');
109
+ useEffect(() => {
110
+ api(`/projects/${MIRROR_DEFAULT_ORG}/${MIRROR_DEFAULT_PROJECT}/issues/?query=${encodeURIComponent(query)}`)
111
+ .then((rows) => setIssues(Array.isArray(rows) ? rows : []));
112
+ }, [query]);
113
+ const current = issues.find((i) => i.id === selected) ?? issues[0];
114
+ return (
115
+ <div className="split">
116
+ <div className="list">
117
+ <div className="search-bar">
118
+ <input className="search-input" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="is:unresolved level:error ..." />
119
+ </div>
120
+ {issues.length === 0 ? <div className="empty">No issues</div> :
121
+ issues.map((i) => <IssueRowView key={i.id} issue={i} selected={current?.id === i.id} onSelect={() => setSelected(i.id)} />)}
122
+ </div>
123
+ <div className="detail">{current ? <IssueDetail issue={current} /> : <div className="empty">Select an issue</div>}</div>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ // Presentational (data-in, no fetch): the Releases screen rendered over a given list of
129
+ // releases. Exported so verify() can render it offline over seeded twin state with
130
+ // react-dom/server and assert one `release-row` per seeded release carrying its version.
131
+ export function ReleasesView({ releases }: { releases: any[] }) {
132
+ return (
133
+ <div className="list full">
134
+ <h2 className="screen-title">Releases</h2>
135
+ {releases.length === 0 ? <div className="empty">No releases</div> :
136
+ releases.map((r) => (
137
+ <div className="release-row" key={r.id}>
138
+ <span className="release-version mono">{r.version}</span>
139
+ <span className="release-commits">{r.commitCount ?? 0} commits</span>
140
+ <span className="release-date">{timeAgo(r.dateCreated)}</span>
141
+ </div>
142
+ ))}
143
+ </div>
144
+ );
145
+ }
146
+
147
+ function ReleasesScreen() {
148
+ const [releases, setReleases] = useState<any[]>([]);
149
+ useEffect(() => { api(`/organizations/${MIRROR_DEFAULT_ORG}/releases/`).then((r) => setReleases(Array.isArray(r) ? r : [])); }, []);
150
+ return <ReleasesView releases={releases} />;
151
+ }
152
+
153
+ // Presentational (data-in, no fetch): the Projects screen rendered over a given list of
154
+ // projects. Exported so verify() can render it offline over seeded twin state with
155
+ // react-dom/server and assert one `project-row` per seeded project carrying its slug.
156
+ export function ProjectsView({ projects }: { projects: any[] }) {
157
+ return (
158
+ <div className="list full">
159
+ <h2 className="screen-title">Projects</h2>
160
+ {projects.length === 0 ? <div className="empty">No projects</div> :
161
+ projects.map((p) => (
162
+ <div className="project-row" key={p.id}>
163
+ <span className="project-name">{p.name}</span>
164
+ <span className="project-slug mono">{p.slug}</span>
165
+ <span className="project-platform">{p.platform ?? '—'}</span>
166
+ </div>
167
+ ))}
168
+ </div>
169
+ );
170
+ }
171
+
172
+ function ProjectsScreen() {
173
+ const [projects, setProjects] = useState<any[]>([]);
174
+ useEffect(() => { api(`/projects/`).then((p) => setProjects(Array.isArray(p) ? p : [])); }, []);
175
+ return <ProjectsView projects={projects} />;
176
+ }
177
+
178
+ function AlertsScreen() {
179
+ const [rules, setRules] = useState<any[]>([]);
180
+ useEffect(() => { api(`/projects/${MIRROR_DEFAULT_ORG}/${MIRROR_DEFAULT_PROJECT}/rules/`).then((r) => setRules(Array.isArray(r) ? r : [])); }, []);
181
+ return (
182
+ <div className="list full alerts-screen">
183
+ <h2 className="screen-title">Alerts</h2>
184
+ <div className="alerts-sub">Issue alert rules for {MIRROR_DEFAULT_PROJECT}</div>
185
+ {rules.length === 0 ? <div className="empty">No alert rules</div> :
186
+ rules.map((r) => (
187
+ <div className="alert-rule-row" key={r.id}>
188
+ <span className="alert-rule-name">{r.name}</span>
189
+ <span className="alert-rule-triggers">
190
+ {(Array.isArray(r.triggers) ? r.triggers : []).map((t: string) => <span className="alert-trigger pill" key={t}>{t}</span>)}
191
+ </span>
192
+ <span className="alert-rule-endpoints mono">{(Array.isArray(r.endpoints) ? r.endpoints : []).length} endpoint(s)</span>
193
+ </div>
194
+ ))}
195
+ </div>
196
+ );
197
+ }
198
+
199
+ function SettingsScreen() {
200
+ const [project, setProject] = useState<any>(null);
201
+ const [keys, setKeys] = useState<any[]>([]);
202
+ const [envs, setEnvs] = useState<any[]>([]);
203
+ useEffect(() => {
204
+ api(`/projects/${MIRROR_DEFAULT_ORG}/${MIRROR_DEFAULT_PROJECT}/`).then(setProject);
205
+ api(`/projects/${MIRROR_DEFAULT_ORG}/${MIRROR_DEFAULT_PROJECT}/keys/`).then((k) => setKeys(Array.isArray(k) ? k : []));
206
+ api(`/projects/${MIRROR_DEFAULT_ORG}/${MIRROR_DEFAULT_PROJECT}/environments/`).then((e) => setEnvs(Array.isArray(e) ? e : []));
207
+ }, []);
208
+ return (
209
+ <div className="list full settings-screen">
210
+ <h2 className="screen-title">Settings</h2>
211
+ <section className="settings-section">
212
+ <h3>General</h3>
213
+ <div className="setting-field"><span className="setting-label">Name</span><span className="setting-value">{project?.name ?? '—'}</span></div>
214
+ <div className="setting-field"><span className="setting-label">Slug</span><span className="setting-value mono">{project?.slug ?? '—'}</span></div>
215
+ <div className="setting-field"><span className="setting-label">Platform</span><span className="setting-value">{project?.platform ?? '—'}</span></div>
216
+ </section>
217
+ <section className="settings-section">
218
+ <h3>Client Keys (DSN)</h3>
219
+ {keys.length === 0 ? <div className="empty">No keys</div> :
220
+ keys.map((k) => (
221
+ <div className="dsn-row" key={k.id}>
222
+ <span className="dsn-name">{k.name}</span>
223
+ <span className="dsn-public mono">{k.dsn?.public}</span>
224
+ <span className={`pill ${k.isActive === false ? 'status-ignored' : 'status-resolved'}`}>{k.isActive === false ? 'revoked' : 'active'}</span>
225
+ </div>
226
+ ))}
227
+ </section>
228
+ <section className="settings-section">
229
+ <h3>Environments</h3>
230
+ {envs.length === 0 ? <div className="empty">No environments</div> :
231
+ <div className="env-list">{envs.map((e) => <span className="env-item pill" key={e.id}>{e.name}</span>)}</div>}
232
+ </section>
233
+ </div>
234
+ );
235
+ }
236
+
237
+ // Presentational (data-in, no fetch): the Dashboards screen rendered over a given list of
238
+ // dashboards. Exported so verify() can render it offline over seeded twin state with
239
+ // react-dom/server and assert one `dashboard-row` per seeded dashboard (+ its widgets).
240
+ export function DashboardsView({ dashboards }: { dashboards: any[] }) {
241
+ return (
242
+ <div className="list full dashboards-screen">
243
+ <h2 className="screen-title">Dashboards</h2>
244
+ <div className="dashboards-sub">Custom dashboards & widgets for {MIRROR_DEFAULT_ORG}</div>
245
+ {dashboards.length === 0 ? <div className="empty">No dashboards</div> :
246
+ dashboards.map((d) => (
247
+ <div className="dashboard-row" key={d.id}>
248
+ <span className="dashboard-title">{d.title}</span>
249
+ <div className="dashboard-widgets">
250
+ {(Array.isArray(d.widgets) ? d.widgets : []).map((w: any, i: number) => (
251
+ <div className="widget-card" key={i}>
252
+ <span className="widget-title">{w.title}</span>
253
+ <span className="widget-type pill">{w.displayType}</span>
254
+ </div>
255
+ ))}
256
+ {(!Array.isArray(d.widgets) || d.widgets.length === 0) && <span className="empty">No widgets</span>}
257
+ </div>
258
+ </div>
259
+ ))}
260
+ </div>
261
+ );
262
+ }
263
+
264
+ function DashboardsScreen() {
265
+ const [dashboards, setDashboards] = useState<any[]>([]);
266
+ useEffect(() => { api(`/organizations/${MIRROR_DEFAULT_ORG}/dashboards/`).then((d) => setDashboards(Array.isArray(d) ? d : [])); }, []);
267
+ return <DashboardsView dashboards={dashboards} />;
268
+ }
269
+
270
+ // Presentational (data-in, no fetch): the Discover screen rendered over a given query-builder
271
+ // state + list of saved queries. Exported so verify() can render it offline over seeded twin
272
+ // state and assert one `saved-query-row` per seeded saved query. The builder is purely local
273
+ // editor state in the live screen (column/query inputs); we accept it as props so the render is
274
+ // deterministic and self-contained.
275
+ export function DiscoverView({ saved, fields = 'title, count(), last_seen()', query = 'event.type:error', onFields, onQuery }: { saved: any[]; fields?: string; query?: string; onFields?: (v: string) => void; onQuery?: (v: string) => void }) {
276
+ const columns = fields.split(',').map((c) => c.trim()).filter(Boolean);
277
+ return (
278
+ <div className="list full discover-screen">
279
+ <h2 className="screen-title">Discover</h2>
280
+ <div className="discover-builder">
281
+ <div className="discover-field">
282
+ <label className="discover-label">Columns</label>
283
+ <input className="discover-columns" value={fields} onChange={onFields ? (e) => onFields(e.target.value) : undefined} placeholder="title, count(), last_seen()" readOnly={!onFields} />
284
+ </div>
285
+ <div className="discover-field">
286
+ <label className="discover-label">Query</label>
287
+ <input className="discover-query" value={query} onChange={onQuery ? (e) => onQuery(e.target.value) : undefined} placeholder="event.type:error" readOnly={!onQuery} />
288
+ </div>
289
+ <div className="discover-columns-preview">
290
+ {columns.map((c) => <span className="discover-column-chip mono" key={c}>{c}</span>)}
291
+ </div>
292
+ </div>
293
+ <div className="discover-results-note empty">
294
+ Live query execution requires the data-plane query engine (not built yet) — the query builder
295
+ config is faithful and saved queries persist.
296
+ </div>
297
+ <h3>Saved Queries</h3>
298
+ {saved.length === 0 ? <div className="empty">No saved queries</div> :
299
+ saved.map((q) => (
300
+ <div className="saved-query-row" key={q.id}>
301
+ <span className="saved-query-name">{q.name}</span>
302
+ <span className="saved-query-fields mono">{(Array.isArray(q.fields) ? q.fields : []).join(', ')}</span>
303
+ </div>
304
+ ))}
305
+ </div>
306
+ );
307
+ }
308
+
309
+ function DiscoverScreen() {
310
+ const [saved, setSaved] = useState<any[]>([]);
311
+ const [fields, setFields] = useState('title, count(), last_seen()');
312
+ const [query, setQuery] = useState('event.type:error');
313
+ useEffect(() => { api(`/organizations/${MIRROR_DEFAULT_ORG}/discover/saved/`).then((q) => setSaved(Array.isArray(q) ? q : [])); }, []);
314
+ return <DiscoverView saved={saved} fields={fields} query={query} onFields={setFields} onQuery={setQuery} />;
315
+ }
316
+
317
+ function App() {
318
+ const [nav, setNav] = useState<Nav>('issues');
319
+ return (
320
+ <div className="app">
321
+ <header className="topbar">
322
+ <span className="brand">Sentry <span className="badge">twin</span></span>
323
+ <span className="org mono">{MIRROR_DEFAULT_ORG}</span>
324
+ </header>
325
+ <div className="body">
326
+ <nav className="sidenav">
327
+ {NAV.map((n) => (
328
+ <button key={n.key} className={`nav-item ${nav === n.key ? 'active' : ''}`} onClick={() => setNav(n.key)}>{n.label}</button>
329
+ ))}
330
+ </nav>
331
+ <main className="content">
332
+ {nav === 'issues' && <IssuesScreen />}
333
+ {nav === 'dashboards' && <DashboardsScreen />}
334
+ {nav === 'discover' && <DiscoverScreen />}
335
+ {nav === 'releases' && <ReleasesScreen />}
336
+ {nav === 'projects' && <ProjectsScreen />}
337
+ {nav === 'alerts' && <AlertsScreen />}
338
+ {nav === 'settings' && <SettingsScreen />}
339
+ </main>
340
+ </div>
341
+ </div>
342
+ );
343
+ }
344
+
345
+ // Browser-only mount. Guarded so the presentational components above (DashboardsView,
346
+ // DiscoverView, …) can be imported and renderToStaticMarkup'd in a DOM-less test/SSR
347
+ // environment without this top-level `document` access throwing. The browser bundle still
348
+ // mounts exactly as before.
349
+ if (typeof document !== 'undefined') {
350
+ const rootEl = document.getElementById('root');
351
+ if (rootEl) createRoot(rootEl).render(<App />);
352
+ }