@mantiq/heartbeat 0.2.2 → 0.3.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/package.json
CHANGED
|
@@ -26,6 +26,7 @@ export interface HeartbeatConfig {
|
|
|
26
26
|
tracing: { enabled: boolean; propagate: boolean }
|
|
27
27
|
sampling: { rate: number; always_sample_errors: boolean }
|
|
28
28
|
dashboard: { path: string; middleware: string[]; enabled: boolean }
|
|
29
|
+
widget?: { enabled: boolean }
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export const DEFAULT_CONFIG: HeartbeatConfig = {
|
|
@@ -56,4 +57,5 @@ export const DEFAULT_CONFIG: HeartbeatConfig = {
|
|
|
56
57
|
tracing: { enabled: true, propagate: true },
|
|
57
58
|
sampling: { rate: 1.0, always_sample_errors: true },
|
|
58
59
|
dashboard: { path: '/_heartbeat', middleware: [], enabled: true },
|
|
60
|
+
widget: { enabled: true },
|
|
59
61
|
}
|
package/src/index.ts
CHANGED
|
@@ -45,6 +45,9 @@ export { LogWatcher } from './watchers/LogWatcher.ts'
|
|
|
45
45
|
export { ScheduleWatcher } from './watchers/ScheduleWatcher.ts'
|
|
46
46
|
export { MailWatcher } from './watchers/MailWatcher.ts'
|
|
47
47
|
|
|
48
|
+
// ── Widget ──────────────────────────────────────────────────────────────────
|
|
49
|
+
export { renderWidget } from './widget/DebugWidget.ts'
|
|
50
|
+
|
|
48
51
|
// ── Tracing ─────────────────────────────────────────────────────────────────
|
|
49
52
|
export { Tracer } from './tracing/Tracer.ts'
|
|
50
53
|
export { Span } from './tracing/Span.ts'
|
|
@@ -3,6 +3,7 @@ import type { Heartbeat } from '../Heartbeat.ts'
|
|
|
3
3
|
import type { Tracer } from '../tracing/Tracer.ts'
|
|
4
4
|
import type { RequestWatcher } from '../watchers/RequestWatcher.ts'
|
|
5
5
|
import type { MetricsCollector } from '../metrics/MetricsCollector.ts'
|
|
6
|
+
import { renderWidget } from '../widget/DebugWidget.ts'
|
|
6
7
|
|
|
7
8
|
/** Max response body size to capture (16 KB). */
|
|
8
9
|
const MAX_RESPONSE_BODY = 16_384
|
|
@@ -139,6 +140,16 @@ export class HeartbeatMiddleware implements Middleware {
|
|
|
139
140
|
this.heartbeat.flush()
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
// Inject debug widget into HTML responses when APP_DEBUG=true
|
|
144
|
+
if (process.env.APP_DEBUG === 'true' && response!) {
|
|
145
|
+
const ct = response!.headers.get('content-type') ?? ''
|
|
146
|
+
if (ct.includes('text/html') && response!.status < 400) {
|
|
147
|
+
const duration = performance.now() - startTime
|
|
148
|
+
const memUsage = process.memoryUsage().rss - startMemory
|
|
149
|
+
response = await this.injectWidget(response!, duration, memUsage)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
142
153
|
return response!
|
|
143
154
|
}
|
|
144
155
|
|
|
@@ -202,4 +213,30 @@ export class HeartbeatMiddleware implements Middleware {
|
|
|
202
213
|
return { body: null, size: null, rebuilt: null }
|
|
203
214
|
}
|
|
204
215
|
}
|
|
216
|
+
|
|
217
|
+
private async injectWidget(response: Response, duration: number, memory: number): Promise<Response> {
|
|
218
|
+
if (!this.heartbeat.config.widget?.enabled) return response
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const html = await response.text()
|
|
222
|
+
if (!html.includes('</body>')) return new Response(html, { status: response.status, statusText: response.statusText, headers: response.headers })
|
|
223
|
+
|
|
224
|
+
const widget = renderWidget({
|
|
225
|
+
duration,
|
|
226
|
+
memory: Math.abs(memory),
|
|
227
|
+
status: response.status,
|
|
228
|
+
queries: 0, // TODO: wire up query count from QueryWatcher
|
|
229
|
+
dashboardPath: this.heartbeat.config.dashboard.path,
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const injected = html.replace('</body>', widget + '\n</body>')
|
|
233
|
+
return new Response(injected, {
|
|
234
|
+
status: response.status,
|
|
235
|
+
statusText: response.statusText,
|
|
236
|
+
headers: response.headers,
|
|
237
|
+
})
|
|
238
|
+
} catch {
|
|
239
|
+
return response
|
|
240
|
+
}
|
|
241
|
+
}
|
|
205
242
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floating debug widget — injected into HTML responses when APP_DEBUG=true.
|
|
3
|
+
* Shows request duration, memory, status, query count, and links to Heartbeat.
|
|
4
|
+
*
|
|
5
|
+
* Renders as a small pill at bottom-right that expands on hover/click.
|
|
6
|
+
* Minimal footprint — all inline, no external deps.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function renderWidget(data: {
|
|
10
|
+
duration: number
|
|
11
|
+
memory: number
|
|
12
|
+
status: number
|
|
13
|
+
queries: number
|
|
14
|
+
dashboardPath: string
|
|
15
|
+
}): string {
|
|
16
|
+
const { duration, memory, status, queries, dashboardPath } = data
|
|
17
|
+
const memMB = (memory / 1024 / 1024).toFixed(1)
|
|
18
|
+
const durationMs = duration.toFixed(0)
|
|
19
|
+
const statusColor = status >= 500 ? '#f87171' : status >= 400 ? '#fbbf24' : '#34d399'
|
|
20
|
+
|
|
21
|
+
return `<!-- mantiq:heartbeat-widget -->
|
|
22
|
+
<div id="__mantiq_widget" style="position:fixed;bottom:16px;right:16px;z-index:99999;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;font-size:12px;pointer-events:auto;">
|
|
23
|
+
<div id="__mw_pill" style="display:flex;align-items:center;gap:8px;background:#0a0a0b;border:1px solid #27272a;border-radius:8px;padding:6px 12px;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,.4);transition:all .2s ease;color:#a1a1aa;" onclick="document.getElementById('__mw_panel').style.display=document.getElementById('__mw_panel').style.display==='none'?'block':'none'">
|
|
24
|
+
<span style="width:6px;height:6px;border-radius:50%;background:${statusColor};flex-shrink:0"></span>
|
|
25
|
+
<span style="color:#fafafa;font-weight:600">${durationMs}ms</span>
|
|
26
|
+
<span style="color:#52525b">·</span>
|
|
27
|
+
<span>${memMB}MB</span>
|
|
28
|
+
<span style="color:#52525b">·</span>
|
|
29
|
+
<span>${queries}q</span>
|
|
30
|
+
<span style="color:#34d399;font-size:10px;margin-left:2px">▲</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div id="__mw_panel" style="display:none;position:absolute;bottom:calc(100% + 8px);right:0;background:#0a0a0b;border:1px solid #27272a;border-radius:10px;padding:0;min-width:260px;box-shadow:0 8px 24px rgba(0,0,0,.5);overflow:hidden;">
|
|
33
|
+
<div style="padding:12px 14px;border-bottom:1px solid #1e1e1e;display:flex;align-items:center;justify-content:space-between;">
|
|
34
|
+
<span style="color:#34d399;font-weight:600;font-size:11px;letter-spacing:.03em">● HEARTBEAT</span>
|
|
35
|
+
<a href="${dashboardPath}" style="color:#52525b;text-decoration:none;font-size:11px;transition:color .15s" onmouseover="this.style.color='#34d399'" onmouseout="this.style.color='#52525b'">Dashboard →</a>
|
|
36
|
+
</div>
|
|
37
|
+
<div style="padding:10px 14px;display:grid;grid-template-columns:1fr 1fr;gap:8px;">
|
|
38
|
+
<div>
|
|
39
|
+
<div style="color:#52525b;font-size:10px;text-transform:uppercase;letter-spacing:.05em;margin-bottom:2px">Duration</div>
|
|
40
|
+
<div style="color:#fafafa;font-weight:600;font-size:14px">${durationMs}<span style="color:#52525b;font-size:11px;font-weight:400">ms</span></div>
|
|
41
|
+
</div>
|
|
42
|
+
<div>
|
|
43
|
+
<div style="color:#52525b;font-size:10px;text-transform:uppercase;letter-spacing:.05em;margin-bottom:2px">Memory</div>
|
|
44
|
+
<div style="color:#fafafa;font-weight:600;font-size:14px">${memMB}<span style="color:#52525b;font-size:11px;font-weight:400">MB</span></div>
|
|
45
|
+
</div>
|
|
46
|
+
<div>
|
|
47
|
+
<div style="color:#52525b;font-size:10px;text-transform:uppercase;letter-spacing:.05em;margin-bottom:2px">Status</div>
|
|
48
|
+
<div style="color:${statusColor};font-weight:600;font-size:14px">${status}</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div>
|
|
51
|
+
<div style="color:#52525b;font-size:10px;text-transform:uppercase;letter-spacing:.05em;margin-bottom:2px">Queries</div>
|
|
52
|
+
<div style="color:#fafafa;font-weight:600;font-size:14px">${queries}</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div style="padding:8px 14px;border-top:1px solid #1e1e1e;">
|
|
56
|
+
<a href="${dashboardPath}" style="display:block;text-align:center;color:#0a0a0b;background:#34d399;padding:6px;border-radius:6px;font-size:11px;font-weight:600;text-decoration:none;transition:background .15s" onmouseover="this.style.background='#10b981'" onmouseout="this.style.background='#34d399'">Open Heartbeat</a>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<script>document.addEventListener('keydown',function(e){if(e.key==='Escape')document.getElementById('__mw_panel').style.display='none'});</script>
|
|
61
|
+
<!-- /mantiq:heartbeat-widget -->`
|
|
62
|
+
}
|