dashboard-blipburst 0.2.0 → 0.2.4

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/README.md CHANGED
@@ -26,6 +26,23 @@ Override the preferred port with `--port` / `-p`, or the `BLIPBURST_DASHBOARD_PO
26
26
  blipburst-dashboard --port 5000
27
27
  ```
28
28
 
29
+ ### Hosted environments
30
+
31
+ Locally, the SDK-side adapter finds the dashboard automatically via the `.blipburst/port` file written to the current directory — that only works because both processes share a filesystem. In a hosted setup (the dashboard deployed somewhere, the BlipBurst-instrumented app deployed somewhere else), there's no shared file, so use env vars on both sides instead:
32
+
33
+ **On the dashboard's deployment**, set `PORT` (the convention Render/Railway/Fly/Heroku-style platforms already inject) or `BLIPBURST_DASHBOARD_PORT`. Either one makes the server bind exactly there and **fail loudly instead of silently drifting to another port** on conflict — unlike a bare local run, which auto-increments for dev convenience, a hosted deployment can only be reached on the port its platform is routing to, so drifting silently would just make it unreachable:
34
+
35
+ ```bash
36
+ # most hosting platforms set PORT for you automatically — nothing else needed
37
+ blipburst-dashboard
38
+ ```
39
+
40
+ **On the app reporting events**, set `BLIPBURST_DASHBOARD_URL` to the dashboard's public URL — every adapter function (`toDashboardTransport`, `toDashboardWebhookUrl`, `wrapForDashboard`) picks it up automatically, no code change needed:
41
+
42
+ ```bash
43
+ BLIPBURST_DASHBOARD_URL=https://blipburst-dashboard.internal.example.com node server.js
44
+ ```
45
+
29
46
  ## Wire it up to BlipBurst
30
47
 
31
48
  No changes to `blipburst` itself are needed — everything below plugs into config options `blipburst` already supports.
@@ -78,6 +95,10 @@ await sim.makeRequest(); // now tracked whether or not a fault fired
78
95
 
79
96
  This also unlocks exact MTTR (measured time from a failed call to the next success on that endpoint) instead of the log-only estimate (duration of consecutive fault bursts).
80
97
 
98
+ ### A quiet dashboard in production is expected, not broken
99
+
100
+ BlipBurst's `enabled: false` option (or `BLIPBURST_ENABLED=false`) is a global kill switch — when it's off, no faults ever fire, so nothing reaches any of the ingest endpoints above regardless of how the transport/webhook/wrapper is wired. If you point the dashboard at a production deployment that correctly disables chaos there, an empty live feed and heatmap is the dashboard working correctly, not a wiring bug. Point it at a dev/staging deployment (where chaos is actually enabled) to see live data.
101
+
81
102
  ## What the dashboard shows
82
103
 
83
104
  - **Live fault feed** — streamed over SSE as `fault.injected` / `request.failed` events arrive.
package/bin/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { startDashboardServer } from '../src/server.js';
3
- import { DEFAULT_PORT } from '../src/port.js';
3
+ import { DEFAULT_PORT, explicitPortFromEnv } from '../src/port.js';
4
4
 
5
5
  function parseArgs(argv) {
6
6
  const out = { port: undefined };
@@ -16,16 +16,25 @@ function parseArgs(argv) {
16
16
  }
17
17
 
18
18
  async function main() {
19
- const { port } = parseArgs(process.argv.slice(2));
20
- const preferredPort = Number.isFinite(port) ? port : DEFAULT_PORT;
19
+ const { port: cliPort } = parseArgs(process.argv.slice(2));
20
+ // --port wins, then BLIPBURST_DASHBOARD_PORT / the hosting-platform-standard
21
+ // PORT (Render/Railway/Fly/Heroku all inject this). Any of these being set
22
+ // means something upstream expects this exact port — a hosted deployment
23
+ // routes traffic there and nowhere else — so silently drifting to another
24
+ // port on conflict would make the deployment unreachable. Bare local runs
25
+ // with nothing set get the old convenience behavior: auto-increment.
26
+ const explicitPort = Number.isFinite(cliPort) ? cliPort : explicitPortFromEnv();
27
+ const preferredPort = explicitPort ?? DEFAULT_PORT;
28
+ const maxPortAttempts = explicitPort ? 0 : 20;
21
29
 
22
- const { port: resolvedPort, store } = await startDashboardServer({ port: preferredPort });
30
+ const { port: resolvedPort, store } = await startDashboardServer({ port: preferredPort, maxPortAttempts });
23
31
 
24
32
  console.log(`BlipBurst dashboard running at http://localhost:${resolvedPort}`);
25
33
  if (resolvedPort !== preferredPort) {
26
34
  console.log(`(port ${preferredPort} was in use — auto-selected ${resolvedPort})`);
27
35
  }
28
36
  console.log(`Port written to .blipburst/port — point BlipBurst's logger/webhook adapter at it automatically.`);
37
+ console.log(`Hosted setup: set BLIPBURST_DASHBOARD_URL on the reporting app's environment to point it at this dashboard's public URL instead of relying on .blipburst/port.`);
29
38
 
30
39
  const shutdown = () => {
31
40
  store.flushNow(); // persist the last few seconds of heatmap/MTTR/run aggregates before exiting
package/index.d.ts CHANGED
@@ -23,7 +23,11 @@ export interface BlipBurstLike {
23
23
  export interface DashboardAdapterOptions {
24
24
  /** Group events from this call under a specific run id instead of a freshly generated one. */
25
25
  runId?: string;
26
- /** Dashboard port override; otherwise read from `.blipburst/port` or defaults to 4477. */
26
+ /**
27
+ * Dashboard port override for the local `http://localhost:<port>` case.
28
+ * Ignored when BLIPBURST_DASHBOARD_URL is set — see resolveDashboardUrl.
29
+ * Otherwise resolved from BLIPBURST_DASHBOARD_PORT / PORT / `.blipburst/port` / 4477.
30
+ */
27
31
  port?: number;
28
32
  }
29
33
 
@@ -70,3 +74,20 @@ export function startDashboardServer(options?: StartDashboardServerOptions): Pro
70
74
 
71
75
  export const DEFAULT_PORT: number;
72
76
  export function resolvePort(explicitPort?: number): number;
77
+
78
+ /**
79
+ * Resolve the full base URL to reach the dashboard at. Set
80
+ * BLIPBURST_DASHBOARD_URL (e.g. `https://dashboard.internal.example.com`)
81
+ * on the reporting app's environment to point every adapter function at a
82
+ * dashboard running on a different host — no code change needed. Falls
83
+ * back to `http://localhost:<resolvePort()>` otherwise.
84
+ */
85
+ export function resolveDashboardUrl(explicitPort?: number): string;
86
+
87
+ /**
88
+ * A port from BLIPBURST_DASHBOARD_PORT or the hosting-platform-standard
89
+ * PORT (Render/Railway/Fly/Heroku), or `null` if neither is set. Used to
90
+ * decide whether binding should fail loudly on conflict instead of
91
+ * auto-incrementing — see `startDashboardServer`'s `maxPortAttempts`.
92
+ */
93
+ export function explicitPortFromEnv(): number | null;
package/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { toDashboardTransport, toDashboardWebhookUrl, wrapForDashboard, flushBuffer } from './src/adapter.js';
2
2
  export { startDashboardServer } from './src/server.js';
3
- export { DEFAULT_PORT, resolvePort } from './src/port.js';
3
+ export { DEFAULT_PORT, resolvePort, resolveDashboardUrl, explicitPortFromEnv } from './src/port.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dashboard-blipburst",
3
- "version": "0.2.0",
3
+ "version": "0.2.4",
4
4
  "description": "Local live dashboard for BlipBurst — fault feed, experiment run history, endpoint/tenant chaos heatmap, and MTTR per fault type. Install globally, point BlipBurst's logger/webhook at it.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/src/adapter.js CHANGED
@@ -1,6 +1,16 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { resolvePort, ensureBlipDir, BUFFER_FILE } from './port.js';
3
+ import { resolveDashboardUrl, ensureBlipDir, BUFFER_FILE } from './port.js';
4
+
5
+ // toDashboardTransport() and wrapForDashboard() are commonly used together
6
+ // in the same process (the README's "full request coverage" combo) — they
7
+ // need to land in the same run, so default to one shared per-process runId
8
+ // generated lazily on first use, rather than each function minting its own.
9
+ // An explicit `options.runId` still overrides this per call.
10
+ let _defaultRunId = null;
11
+ function getDefaultRunId() {
12
+ return (_defaultRunId ??= randomUUID());
13
+ }
4
14
 
5
15
  function bufferEnvelope(envelope) {
6
16
  ensureBlipDir();
@@ -11,8 +21,8 @@ function bufferEnvelope(envelope) {
11
21
  }
12
22
  }
13
23
 
14
- async function postJson(port, path, payload) {
15
- const res = await fetch(`http://localhost:${port}${path}`, {
24
+ async function postJson(baseUrl, path, payload) {
25
+ const res = await fetch(`${baseUrl}${path}`, {
16
26
  method: 'POST',
17
27
  headers: { 'Content-Type': 'application/json' },
18
28
  body: JSON.stringify(payload),
@@ -21,9 +31,9 @@ async function postJson(port, path, payload) {
21
31
  }
22
32
 
23
33
  /** Best-effort send: on any failure (dashboard not running, network error) buffer to disk instead of throwing. */
24
- async function sendOrBuffer(port, path, payload) {
34
+ async function sendOrBuffer(baseUrl, path, payload) {
25
35
  try {
26
- await postJson(port, path, payload);
36
+ await postJson(baseUrl, path, payload);
27
37
  } catch {
28
38
  bufferEnvelope({ path, payload });
29
39
  }
@@ -35,7 +45,7 @@ async function sendOrBuffer(port, path, payload) {
35
45
  * drained. Failures leave the buffer file untouched so nothing is lost.
36
46
  */
37
47
  export async function flushBuffer(port) {
38
- const resolvedPort = resolvePort(port);
48
+ const baseUrl = resolveDashboardUrl(port);
39
49
  if (!existsSync(BUFFER_FILE)) return { flushed: 0 };
40
50
 
41
51
  let lines;
@@ -56,7 +66,7 @@ export async function flushBuffer(port) {
56
66
  continue; // drop corrupt line
57
67
  }
58
68
  try {
59
- await postJson(resolvedPort, envelope.path, envelope.payload);
69
+ await postJson(baseUrl, envelope.path, envelope.payload);
60
70
  flushed++;
61
71
  } catch {
62
72
  remaining.push(line);
@@ -79,22 +89,27 @@ export async function flushBuffer(port) {
79
89
  * are flushed automatically the next time this transport is created (i.e.
80
90
  * the next process start) or on demand via `flushBuffer()`.
81
91
  *
92
+ * Locally this discovers the dashboard via `.blipburst/port` automatically.
93
+ * In a hosted setup where the app and the dashboard run on different hosts,
94
+ * set BLIPBURST_DASHBOARD_URL (e.g. `https://dashboard.internal.example.com`)
95
+ * on the app's environment instead — no code change needed.
96
+ *
82
97
  * Usage:
83
98
  * import { toDashboardTransport } from 'dashboard-blipburst';
84
99
  * const sim = new BlipBurst({ logger: { transport: toDashboardTransport() } });
85
100
  */
86
101
  export function toDashboardTransport(port, options = {}) {
87
- const resolvedPort = resolvePort(port);
88
- const runId = options.runId ?? randomUUID();
102
+ const baseUrl = resolveDashboardUrl(port);
103
+ const runId = options.runId ?? getDefaultRunId();
89
104
 
90
105
  // Try to drain anything buffered from a previous run without blocking
91
106
  // transport creation.
92
- flushBuffer(resolvedPort).catch(() => {});
107
+ flushBuffer(port).catch(() => {});
93
108
 
94
109
  return function dashboardTransport(entry) {
95
110
  // LogTransport is synchronous/fire-and-forget by contract — never await
96
111
  // or throw here, BlipBurst calls this inline on the request path.
97
- sendOrBuffer(resolvedPort, '/ingest/log', { runId, entry, source: 'logger' }).catch(() => {});
112
+ sendOrBuffer(baseUrl, '/ingest/log', { runId, entry, source: 'logger' }).catch(() => {});
98
113
  };
99
114
  }
100
115
 
@@ -102,14 +117,14 @@ export function toDashboardTransport(port, options = {}) {
102
117
  * Returns the URL to hand to BlipBurst's `webhook.url` option so its
103
118
  * built-in WebhookEmitter posts fault/circuit events straight at the
104
119
  * dashboard — no adapter function needed on that path since BlipBurst's
105
- * webhook is already URL-based.
120
+ * webhook is already URL-based. Respects BLIPBURST_DASHBOARD_URL the same
121
+ * way toDashboardTransport does.
106
122
  *
107
123
  * Usage:
108
124
  * const sim = new BlipBurst({ webhook: { url: toDashboardWebhookUrl() } });
109
125
  */
110
126
  export function toDashboardWebhookUrl(port) {
111
- const resolvedPort = resolvePort(port);
112
- return `http://localhost:${resolvedPort}/ingest/webhook`;
127
+ return `${resolveDashboardUrl(port)}/ingest/webhook`;
113
128
  }
114
129
 
115
130
  function faultKindDiff(before, after) {
@@ -135,8 +150,8 @@ function faultKindDiff(before, after) {
135
150
  * await sim.makeRequest();
136
151
  */
137
152
  export function wrapForDashboard(sim, options = {}) {
138
- const resolvedPort = resolvePort(options.port);
139
- const runId = options.runId ?? randomUUID();
153
+ const baseUrl = resolveDashboardUrl(options.port);
154
+ const runId = options.runId ?? getDefaultRunId();
140
155
  const original = sim.makeRequest.bind(sim);
141
156
 
142
157
  sim.makeRequest = async function wrappedMakeRequest(overrideUrl) {
@@ -156,7 +171,7 @@ export function wrapForDashboard(sim, options = {}) {
156
171
  } finally {
157
172
  const after = sim.getMetrics().faultStats;
158
173
  const faultKind = faultKindDiff(before, after);
159
- sendOrBuffer(resolvedPort, '/ingest/request', {
174
+ sendOrBuffer(baseUrl, '/ingest/request', {
160
175
  runId,
161
176
  url,
162
177
  faultKind,
package/src/port.js CHANGED
@@ -36,10 +36,35 @@ export function writePortFile(port) {
36
36
  }
37
37
  }
38
38
 
39
+ /**
40
+ * A port set via BLIPBURST_DASHBOARD_PORT or the hosting-platform-standard
41
+ * PORT (Render/Railway/Fly/Heroku all inject this) means the deployment
42
+ * expects the process to bind exactly there — unlike the local port-file
43
+ * convenience, silently drifting to a different port would make it
44
+ * unreachable, so callers use this to decide whether auto-increment is safe.
45
+ */
46
+ export function explicitPortFromEnv() {
47
+ const raw = process.env.BLIPBURST_DASHBOARD_PORT ?? process.env.PORT;
48
+ const parsed = raw ? parseInt(raw, 10) : NaN;
49
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
50
+ }
51
+
39
52
  /** Resolve the port the dashboard is (or should be) reachable on. */
40
53
  export function resolvePort(explicitPort) {
41
54
  if (explicitPort) return explicitPort;
42
- const fromEnv = process.env.BLIPBURST_DASHBOARD_PORT;
43
- if (fromEnv && Number.isFinite(parseInt(fromEnv, 10))) return parseInt(fromEnv, 10);
44
- return readPortFile() ?? DEFAULT_PORT;
55
+ return explicitPortFromEnv() ?? readPortFile() ?? DEFAULT_PORT;
56
+ }
57
+
58
+ /**
59
+ * Resolve the full base URL to reach the dashboard at. In a hosted setup
60
+ * where the app reporting events and the dashboard aren't on the same
61
+ * machine (so there's no shared `.blipburst/port` file to read), set
62
+ * BLIPBURST_DASHBOARD_URL (e.g. `https://dashboard.internal.example.com`)
63
+ * on the app's side and every adapter function picks it up automatically.
64
+ * Falls back to the local `http://localhost:<port>` behavior otherwise.
65
+ */
66
+ export function resolveDashboardUrl(explicitPort) {
67
+ const fromEnv = process.env.BLIPBURST_DASHBOARD_URL;
68
+ if (fromEnv) return fromEnv.replace(/\/+$/, '');
69
+ return `http://localhost:${resolvePort(explicitPort)}`;
45
70
  }
package/src/server.js CHANGED
@@ -145,6 +145,12 @@ function listenWithAutoIncrement(server, port, attemptsLeft) {
145
145
  server.once('error', (err) => {
146
146
  if (err.code === 'EADDRINUSE' && attemptsLeft > 0) {
147
147
  resolve(listenWithAutoIncrement(server, port + 1, attemptsLeft - 1));
148
+ } else if (err.code === 'EADDRINUSE') {
149
+ reject(new Error(
150
+ `Port ${port} is already in use and auto-increment is disabled (maxPortAttempts: 0) — ` +
151
+ `this happens when the port came from --port, BLIPBURST_DASHBOARD_PORT, or PORT, since a ` +
152
+ `hosting platform expects the process to bind exactly there. Free the port or change the env var.`
153
+ ));
148
154
  } else {
149
155
  reject(err);
150
156
  }