@aidenappleby/monitor-js 1.1.0 → 1.2.1

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 ADDED
@@ -0,0 +1,109 @@
1
+ # monitor-js
2
+
3
+ Lightweight JavaScript/TypeScript client for the Monitor platform — ships structured
4
+ events (and auto-captured browser errors) as NDJSON to `monitor-core`.
5
+
6
+ > **Monitor platform** · TypeScript SDK · `@aidenappleby/monitor-js` (npm)
7
+
8
+ ---
9
+
10
+ ## Overview
11
+
12
+ `monitor-js` is the browser/Node SDK for Monitor, the counterpart to `go-monitor`. It
13
+ queues and batches events, ships them to a zone's ingest endpoint over `fetch`, and can
14
+ auto-capture uncaught errors and unhandled promise rejections (browser and Node). It keeps
15
+ the same wire format as the Go SDK — including its rules for surviving ingest's
16
+ all-or-nothing validation: invalid ids are cleared before sending, a rejected batch is
17
+ split until only the malformed event is dropped, and transient failures are retried with
18
+ backoff instead of discarded.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install @aidenappleby/monitor-js
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```ts
29
+ import { Monitor } from "@aidenappleby/monitor-js";
30
+
31
+ const monitor = new Monitor({
32
+ service: "my-web-app",
33
+ ingestUrl: "https://appleby-monitor-api.appleby.cloud/v1/events", // one zone's ingest endpoint
34
+ apiKey: process.env.MONITOR_API_KEY!, // ingest-scoped key minted on that zone
35
+ env: "production",
36
+ onDrop: (total) => droppedEvents.set(total), // optional: be told about loss
37
+ });
38
+
39
+ monitor.setUser("user_123");
40
+ monitor.info("checkout.start.success", { data: { cartTotal: 4200 } });
41
+ monitor.error("checkout.payment.failed", { data: { reason: "card_declined" } });
42
+
43
+ monitor.stats(); // { enqueued, flushed, dropped, quarantined, queued }
44
+
45
+ // on teardown
46
+ monitor.shutdown();
47
+ ```
48
+
49
+ > **In a browser, `apiKey` is public** — anyone who loads the page can read it. Prefer
50
+ > pointing `ingestUrl` at a route on your own origin that forwards to Monitor server-side.
51
+
52
+ Uncaught errors and unhandled rejections are captured automatically in **both** the
53
+ browser (`window`) and Node (`process.on`) — disable with `captureErrors: false` /
54
+ `captureUnhandledRejections: false`. Filter noise with
55
+ `ignoreErrors: [/extension/i, "ResizeObserver"]`.
56
+
57
+ In Node, installing a process listener would normally stop an uncaught exception from
58
+ crashing the process. The SDK keeps Node's behavior: when it is the **only**
59
+ `uncaughtException` listener, it reports, prints the error, waits up to 1.5s for the batch
60
+ to leave, and exits 1; an unhandled rejection with no other listener is re-raised as an
61
+ uncaught exception, exactly as Node does by default. If your app has its own listener, the
62
+ SDK only reports. Server code that manages its own crash handling (e.g. a Next.js
63
+ `instrumentation.ts`) should pass `captureErrors: false, captureUnhandledRejections: false`.
64
+
65
+ ### Correlation ids
66
+
67
+ `request_id`, `trace_id` and `job_id` must be a UUID or 8–64 hex characters — anything
68
+ else is cleared before sending and kept in `data.invalid_<field>`. Each `Monitor` mints a
69
+ session `job_id`; mint the others with the helpers:
70
+
71
+ ```ts
72
+ import { newRequestId, newTraceId, isValidCorrelationId } from "@aidenappleby/monitor-js";
73
+
74
+ monitor.info("upload.start.success", { requestId: newRequestId(), traceId: newTraceId() });
75
+ ```
76
+
77
+ ### Axios integration
78
+
79
+ ```ts
80
+ import axios from "axios";
81
+ import { attachAxiosMonitor } from "@aidenappleby/monitor-js";
82
+
83
+ const api = axios.create({ baseURL: "/api", validateStatus: () => true });
84
+ attachAxiosMonitor(api, monitor, { ignorePaths: ["/health"] });
85
+ ```
86
+
87
+ Reported URLs have their query string removed.
88
+
89
+ ## Role in the Monitor ecosystem
90
+
91
+ - **`monitor-core`** — ingestion target (`POST /v1/events`, `X-Api-Key`).
92
+ - **`go-monitor`** — the Go SDK; shares the wire format and delivery rules.
93
+ - **`monitor-web`** — displays the events (it does not use this SDK).
94
+
95
+ ## Development
96
+
97
+ | Command | What it does |
98
+ |---|---|
99
+ | `npm run check` | `tsc --noEmit` |
100
+ | `npm test` | vitest |
101
+ | `npm run build` | tsup → `dist/` (CJS + ESM + d.ts) |
102
+
103
+ Publishing to npm runs the build via `prepublishOnly` and requires 2FA.
104
+
105
+ ## Contributing & further reading
106
+
107
+ Read **[AGENTS.md](./AGENTS.md)** before working here — it documents the event pipeline,
108
+ the delivery rules, the exact wire contract (kept in lockstep with `go-monitor` /
109
+ `monitor-core`), the config surface, and current known issues.
package/dist/index.d.mts CHANGED
@@ -2,9 +2,13 @@ type LogLevel = "debug" | "info" | "warn" | "error" | "fatal";
2
2
  interface MonitorConfig {
3
3
  /** Service name reported with every event */
4
4
  service: string;
5
- /** Monitor ingest URL (e.g. https://monitor-ingest.appleby.cloud/v1/events) */
5
+ /** Full ingest endpoint of one zone (e.g. https://appleby-monitor-api.appleby.cloud/v1/events) */
6
6
  ingestUrl: string;
7
- /** API key for authentication */
7
+ /**
8
+ * Ingest-scoped API key, minted on the zone ingestUrl points at. In a
9
+ * browser bundle this value is public — anyone who loads the page can read
10
+ * it — so prefer posting to a same-origin route that forwards server-side.
11
+ */
8
12
  apiKey: string;
9
13
  /** Environment name (default: "production") */
10
14
  env?: string;
@@ -26,6 +30,12 @@ interface MonitorConfig {
26
30
  * and `client.error.unhandled_rejection`. Default: [] (no filtering).
27
31
  */
28
32
  ignoreErrors?: (string | RegExp)[];
33
+ /**
34
+ * Called with the running total whenever events are lost: queue overflow,
35
+ * data that cannot be serialized, events ingest rejected as malformed, or a
36
+ * refused API key. Keep it cheap — bump a counter. A throw is swallowed.
37
+ */
38
+ onDrop?: (total: number) => void;
29
39
  }
30
40
  interface MonitorEvent {
31
41
  timestamp: string;
@@ -49,21 +59,43 @@ interface EmitOptions {
49
59
  /** Arbitrary data payload */
50
60
  data?: Record<string, unknown>;
51
61
  }
62
+ /** Lifetime counters for one Monitor instance. */
63
+ interface MonitorStats {
64
+ /** Events accepted into the queue. */
65
+ enqueued: number;
66
+ /** Events the ingest endpoint accepted. */
67
+ flushed: number;
68
+ /** Events lost for good: overflow, unserializable, malformed, or refused credentials. */
69
+ dropped: number;
70
+ /** The part of `dropped` ingest refused as malformed even when sent alone. */
71
+ quarantined: number;
72
+ /** Events currently waiting in the queue. */
73
+ queued: number;
74
+ }
52
75
 
53
76
  declare class Monitor {
54
77
  private config;
55
78
  private ignoreErrors;
79
+ private onDrop?;
56
80
  private queue;
57
81
  private timer;
58
82
  private userId;
59
83
  private jobId;
60
84
  private active;
85
+ private backoffUntil;
86
+ private failures;
87
+ private warnedMisconfigured;
88
+ private counters;
61
89
  constructor(config: MonitorConfig);
62
90
  /** Set a persistent user ID for all subsequent events */
63
91
  setUser(userId: string): void;
64
92
  /** Clear the user ID */
65
93
  clearUser(): void;
66
- /** Set a persistent job ID (session-level identifier) */
94
+ /**
95
+ * Set a persistent job ID (session-level identifier). It must be a UUID or
96
+ * 8-64 hex characters — see `isValidCorrelationId`; anything else is
97
+ * cleared from each event and kept in data.invalid_job_id.
98
+ */
67
99
  setJobId(jobId: string): void;
68
100
  /** Emit an event at a specific level */
69
101
  emit(name: string, level: LogLevel, opts?: EmitOptions): void;
@@ -77,16 +109,61 @@ declare class Monitor {
77
109
  error(name: string, opts?: EmitOptions): void;
78
110
  /** Emit a fatal event */
79
111
  fatal(name: string, opts?: EmitOptions): void;
112
+ /**
113
+ * Lifetime counters. Surface them wherever loss would otherwise go
114
+ * unnoticed: the system that would report dropped telemetry is the one
115
+ * dropping it.
116
+ */
117
+ stats(): MonitorStats;
80
118
  /** Flush all queued events to the ingest endpoint */
81
119
  flush(): void;
82
120
  /** Stop the monitor and flush remaining events */
83
121
  shutdown(): void;
122
+ /**
123
+ * @param unloading the page (or process) is going away: ignore the backoff,
124
+ * since this is the last chance these events get.
125
+ */
126
+ private flushQueue;
127
+ private send;
128
+ private handleResponse;
129
+ /** Put events back at the front of the queue and back off before retrying. */
130
+ private retryLater;
131
+ /**
132
+ * One NDJSON line for e, or null if it cannot be serialized. Never throws:
133
+ * flush runs inside emit's auto-flush, and emit must never throw into the
134
+ * caller.
135
+ */
136
+ private serialize;
137
+ private recordDrop;
84
138
  private start;
85
139
  private handleVisibilityChange;
86
140
  private handlePageHide;
87
141
  private shouldIgnoreError;
142
+ /**
143
+ * The route a browser error happened on.
144
+ *
145
+ * Deliberately `pathname` only — never the search string or hash. Query
146
+ * parameters routinely carry tokens, emails and other personal data, and this
147
+ * value is both stored on the event and folded into the server-side issue
148
+ * fingerprint, so anything included here is retained and grouped on.
149
+ *
150
+ * Returns undefined outside a browser so the Node handlers stay unaffected.
151
+ */
152
+ private currentPath;
88
153
  private errorHandler;
89
154
  private rejectionHandler;
155
+ /** Grace for the final batch to leave before a Node-style crash exits. */
156
+ private static readonly NODE_CRASH_GRACE_MS;
157
+ /** Rejections already reported, so the re-raise below is not reported twice. */
158
+ private reportedRejections;
159
+ private nodeExceptionHandler;
160
+ private nodeRejectionHandler;
161
+ /** Hand an unhandled rejection back to Node as an uncaught exception. */
162
+ private reraise;
163
+ /** True when this instance's own handler is the only listener for the event. */
164
+ private isSoleListener;
165
+ /** Print the error as Node would, give the batch a moment to leave, exit 1. */
166
+ private crashLikeNode;
90
167
  private installErrorHandler;
91
168
  private installRejectionHandler;
92
169
  private removeListeners;
@@ -119,4 +196,16 @@ interface AxiosMonitorOptions {
119
196
  */
120
197
  declare function attachAxiosMonitor(axiosInstance: AxiosInstance, monitor: Monitor, opts?: AxiosMonitorOptions): void;
121
198
 
122
- export { type AxiosMonitorOptions, type EmitOptions, type LogLevel, Monitor, type MonitorConfig, type MonitorEvent, attachAxiosMonitor };
199
+ /**
200
+ * Whether monitor-core would accept `id` as a job_id, request_id or trace_id.
201
+ * The empty string is valid: the server skips empty ids.
202
+ */
203
+ declare function isValidCorrelationId(id: string): boolean;
204
+ /** A request_id monitor-core accepts: 16 hex characters. */
205
+ declare function newRequestId(): string;
206
+ /** A job_id monitor-core accepts: 16 hex characters. */
207
+ declare function newJobId(): string;
208
+ /** A trace_id monitor-core accepts: a hyphenated UUID v4. */
209
+ declare function newTraceId(): string;
210
+
211
+ export { type AxiosMonitorOptions, type EmitOptions, type LogLevel, Monitor, type MonitorConfig, type MonitorEvent, type MonitorStats, attachAxiosMonitor, isValidCorrelationId, newJobId, newRequestId, newTraceId };
package/dist/index.d.ts CHANGED
@@ -2,9 +2,13 @@ type LogLevel = "debug" | "info" | "warn" | "error" | "fatal";
2
2
  interface MonitorConfig {
3
3
  /** Service name reported with every event */
4
4
  service: string;
5
- /** Monitor ingest URL (e.g. https://monitor-ingest.appleby.cloud/v1/events) */
5
+ /** Full ingest endpoint of one zone (e.g. https://appleby-monitor-api.appleby.cloud/v1/events) */
6
6
  ingestUrl: string;
7
- /** API key for authentication */
7
+ /**
8
+ * Ingest-scoped API key, minted on the zone ingestUrl points at. In a
9
+ * browser bundle this value is public — anyone who loads the page can read
10
+ * it — so prefer posting to a same-origin route that forwards server-side.
11
+ */
8
12
  apiKey: string;
9
13
  /** Environment name (default: "production") */
10
14
  env?: string;
@@ -26,6 +30,12 @@ interface MonitorConfig {
26
30
  * and `client.error.unhandled_rejection`. Default: [] (no filtering).
27
31
  */
28
32
  ignoreErrors?: (string | RegExp)[];
33
+ /**
34
+ * Called with the running total whenever events are lost: queue overflow,
35
+ * data that cannot be serialized, events ingest rejected as malformed, or a
36
+ * refused API key. Keep it cheap — bump a counter. A throw is swallowed.
37
+ */
38
+ onDrop?: (total: number) => void;
29
39
  }
30
40
  interface MonitorEvent {
31
41
  timestamp: string;
@@ -49,21 +59,43 @@ interface EmitOptions {
49
59
  /** Arbitrary data payload */
50
60
  data?: Record<string, unknown>;
51
61
  }
62
+ /** Lifetime counters for one Monitor instance. */
63
+ interface MonitorStats {
64
+ /** Events accepted into the queue. */
65
+ enqueued: number;
66
+ /** Events the ingest endpoint accepted. */
67
+ flushed: number;
68
+ /** Events lost for good: overflow, unserializable, malformed, or refused credentials. */
69
+ dropped: number;
70
+ /** The part of `dropped` ingest refused as malformed even when sent alone. */
71
+ quarantined: number;
72
+ /** Events currently waiting in the queue. */
73
+ queued: number;
74
+ }
52
75
 
53
76
  declare class Monitor {
54
77
  private config;
55
78
  private ignoreErrors;
79
+ private onDrop?;
56
80
  private queue;
57
81
  private timer;
58
82
  private userId;
59
83
  private jobId;
60
84
  private active;
85
+ private backoffUntil;
86
+ private failures;
87
+ private warnedMisconfigured;
88
+ private counters;
61
89
  constructor(config: MonitorConfig);
62
90
  /** Set a persistent user ID for all subsequent events */
63
91
  setUser(userId: string): void;
64
92
  /** Clear the user ID */
65
93
  clearUser(): void;
66
- /** Set a persistent job ID (session-level identifier) */
94
+ /**
95
+ * Set a persistent job ID (session-level identifier). It must be a UUID or
96
+ * 8-64 hex characters — see `isValidCorrelationId`; anything else is
97
+ * cleared from each event and kept in data.invalid_job_id.
98
+ */
67
99
  setJobId(jobId: string): void;
68
100
  /** Emit an event at a specific level */
69
101
  emit(name: string, level: LogLevel, opts?: EmitOptions): void;
@@ -77,16 +109,61 @@ declare class Monitor {
77
109
  error(name: string, opts?: EmitOptions): void;
78
110
  /** Emit a fatal event */
79
111
  fatal(name: string, opts?: EmitOptions): void;
112
+ /**
113
+ * Lifetime counters. Surface them wherever loss would otherwise go
114
+ * unnoticed: the system that would report dropped telemetry is the one
115
+ * dropping it.
116
+ */
117
+ stats(): MonitorStats;
80
118
  /** Flush all queued events to the ingest endpoint */
81
119
  flush(): void;
82
120
  /** Stop the monitor and flush remaining events */
83
121
  shutdown(): void;
122
+ /**
123
+ * @param unloading the page (or process) is going away: ignore the backoff,
124
+ * since this is the last chance these events get.
125
+ */
126
+ private flushQueue;
127
+ private send;
128
+ private handleResponse;
129
+ /** Put events back at the front of the queue and back off before retrying. */
130
+ private retryLater;
131
+ /**
132
+ * One NDJSON line for e, or null if it cannot be serialized. Never throws:
133
+ * flush runs inside emit's auto-flush, and emit must never throw into the
134
+ * caller.
135
+ */
136
+ private serialize;
137
+ private recordDrop;
84
138
  private start;
85
139
  private handleVisibilityChange;
86
140
  private handlePageHide;
87
141
  private shouldIgnoreError;
142
+ /**
143
+ * The route a browser error happened on.
144
+ *
145
+ * Deliberately `pathname` only — never the search string or hash. Query
146
+ * parameters routinely carry tokens, emails and other personal data, and this
147
+ * value is both stored on the event and folded into the server-side issue
148
+ * fingerprint, so anything included here is retained and grouped on.
149
+ *
150
+ * Returns undefined outside a browser so the Node handlers stay unaffected.
151
+ */
152
+ private currentPath;
88
153
  private errorHandler;
89
154
  private rejectionHandler;
155
+ /** Grace for the final batch to leave before a Node-style crash exits. */
156
+ private static readonly NODE_CRASH_GRACE_MS;
157
+ /** Rejections already reported, so the re-raise below is not reported twice. */
158
+ private reportedRejections;
159
+ private nodeExceptionHandler;
160
+ private nodeRejectionHandler;
161
+ /** Hand an unhandled rejection back to Node as an uncaught exception. */
162
+ private reraise;
163
+ /** True when this instance's own handler is the only listener for the event. */
164
+ private isSoleListener;
165
+ /** Print the error as Node would, give the batch a moment to leave, exit 1. */
166
+ private crashLikeNode;
90
167
  private installErrorHandler;
91
168
  private installRejectionHandler;
92
169
  private removeListeners;
@@ -119,4 +196,16 @@ interface AxiosMonitorOptions {
119
196
  */
120
197
  declare function attachAxiosMonitor(axiosInstance: AxiosInstance, monitor: Monitor, opts?: AxiosMonitorOptions): void;
121
198
 
122
- export { type AxiosMonitorOptions, type EmitOptions, type LogLevel, Monitor, type MonitorConfig, type MonitorEvent, attachAxiosMonitor };
199
+ /**
200
+ * Whether monitor-core would accept `id` as a job_id, request_id or trace_id.
201
+ * The empty string is valid: the server skips empty ids.
202
+ */
203
+ declare function isValidCorrelationId(id: string): boolean;
204
+ /** A request_id monitor-core accepts: 16 hex characters. */
205
+ declare function newRequestId(): string;
206
+ /** A job_id monitor-core accepts: 16 hex characters. */
207
+ declare function newJobId(): string;
208
+ /** A trace_id monitor-core accepts: a hyphenated UUID v4. */
209
+ declare function newTraceId(): string;
210
+
211
+ export { type AxiosMonitorOptions, type EmitOptions, type LogLevel, Monitor, type MonitorConfig, type MonitorEvent, type MonitorStats, attachAxiosMonitor, isValidCorrelationId, newJobId, newRequestId, newTraceId };