@aidenappleby/monitor-js 1.0.0 → 1.2.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/README.md ADDED
@@ -0,0 +1,102 @@
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"]`. The Node handlers report and return;
56
+ they do not alter process-crash behavior.
57
+
58
+ ### Correlation ids
59
+
60
+ `request_id`, `trace_id` and `job_id` must be a UUID or 8–64 hex characters — anything
61
+ else is cleared before sending and kept in `data.invalid_<field>`. Each `Monitor` mints a
62
+ session `job_id`; mint the others with the helpers:
63
+
64
+ ```ts
65
+ import { newRequestId, newTraceId, isValidCorrelationId } from "@aidenappleby/monitor-js";
66
+
67
+ monitor.info("upload.start.success", { requestId: newRequestId(), traceId: newTraceId() });
68
+ ```
69
+
70
+ ### Axios integration
71
+
72
+ ```ts
73
+ import axios from "axios";
74
+ import { attachAxiosMonitor } from "@aidenappleby/monitor-js";
75
+
76
+ const api = axios.create({ baseURL: "/api", validateStatus: () => true });
77
+ attachAxiosMonitor(api, monitor, { ignorePaths: ["/health"] });
78
+ ```
79
+
80
+ Reported URLs have their query string removed.
81
+
82
+ ## Role in the Monitor ecosystem
83
+
84
+ - **`monitor-core`** — ingestion target (`POST /v1/events`, `X-Api-Key`).
85
+ - **`go-monitor`** — the Go SDK; shares the wire format and delivery rules.
86
+ - **`monitor-web`** — displays the events (it does not use this SDK).
87
+
88
+ ## Development
89
+
90
+ | Command | What it does |
91
+ |---|---|
92
+ | `npm run check` | `tsc --noEmit` |
93
+ | `npm test` | vitest |
94
+ | `npm run build` | tsup → `dist/` (CJS + ESM + d.ts) |
95
+
96
+ Publishing to npm runs the build via `prepublishOnly` and requires 2FA.
97
+
98
+ ## Contributing & further reading
99
+
100
+ Read **[AGENTS.md](./AGENTS.md)** before working here — it documents the event pipeline,
101
+ the delivery rules, the exact wire contract (kept in lockstep with `go-monitor` /
102
+ `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;
@@ -18,6 +22,20 @@ interface MonitorConfig {
18
22
  captureUnhandledRejections?: boolean;
19
23
  /** Enable debug logging to console (default: false) */
20
24
  debug?: boolean;
25
+ /**
26
+ * Patterns (strings or RegExp) matched against captured error messages and
27
+ * stack traces. Matching errors are silently dropped before reaching the
28
+ * ingest queue. Use to filter browser-extension noise, third-party script
29
+ * errors, or other non-actionable events. Applies to both `client.error.uncaught`
30
+ * and `client.error.unhandled_rejection`. Default: [] (no filtering).
31
+ */
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;
21
39
  }
22
40
  interface MonitorEvent {
23
41
  timestamp: string;
@@ -41,20 +59,43 @@ interface EmitOptions {
41
59
  /** Arbitrary data payload */
42
60
  data?: Record<string, unknown>;
43
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
+ }
44
75
 
45
76
  declare class Monitor {
46
77
  private config;
78
+ private ignoreErrors;
79
+ private onDrop?;
47
80
  private queue;
48
81
  private timer;
49
82
  private userId;
50
83
  private jobId;
51
84
  private active;
85
+ private backoffUntil;
86
+ private failures;
87
+ private warnedMisconfigured;
88
+ private counters;
52
89
  constructor(config: MonitorConfig);
53
90
  /** Set a persistent user ID for all subsequent events */
54
91
  setUser(userId: string): void;
55
92
  /** Clear the user ID */
56
93
  clearUser(): void;
57
- /** 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
+ */
58
99
  setJobId(jobId: string): void;
59
100
  /** Emit an event at a specific level */
60
101
  emit(name: string, level: LogLevel, opts?: EmitOptions): void;
@@ -68,15 +109,51 @@ declare class Monitor {
68
109
  error(name: string, opts?: EmitOptions): void;
69
110
  /** Emit a fatal event */
70
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;
71
118
  /** Flush all queued events to the ingest endpoint */
72
119
  flush(): void;
73
120
  /** Stop the monitor and flush remaining events */
74
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;
75
138
  private start;
76
139
  private handleVisibilityChange;
77
140
  private handlePageHide;
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;
78
153
  private errorHandler;
79
154
  private rejectionHandler;
155
+ private nodeExceptionHandler;
156
+ private nodeRejectionHandler;
80
157
  private installErrorHandler;
81
158
  private installRejectionHandler;
82
159
  private removeListeners;
@@ -109,4 +186,16 @@ interface AxiosMonitorOptions {
109
186
  */
110
187
  declare function attachAxiosMonitor(axiosInstance: AxiosInstance, monitor: Monitor, opts?: AxiosMonitorOptions): void;
111
188
 
112
- export { type AxiosMonitorOptions, type EmitOptions, type LogLevel, Monitor, type MonitorConfig, type MonitorEvent, attachAxiosMonitor };
189
+ /**
190
+ * Whether monitor-core would accept `id` as a job_id, request_id or trace_id.
191
+ * The empty string is valid: the server skips empty ids.
192
+ */
193
+ declare function isValidCorrelationId(id: string): boolean;
194
+ /** A request_id monitor-core accepts: 16 hex characters. */
195
+ declare function newRequestId(): string;
196
+ /** A job_id monitor-core accepts: 16 hex characters. */
197
+ declare function newJobId(): string;
198
+ /** A trace_id monitor-core accepts: a hyphenated UUID v4. */
199
+ declare function newTraceId(): string;
200
+
201
+ 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;
@@ -18,6 +22,20 @@ interface MonitorConfig {
18
22
  captureUnhandledRejections?: boolean;
19
23
  /** Enable debug logging to console (default: false) */
20
24
  debug?: boolean;
25
+ /**
26
+ * Patterns (strings or RegExp) matched against captured error messages and
27
+ * stack traces. Matching errors are silently dropped before reaching the
28
+ * ingest queue. Use to filter browser-extension noise, third-party script
29
+ * errors, or other non-actionable events. Applies to both `client.error.uncaught`
30
+ * and `client.error.unhandled_rejection`. Default: [] (no filtering).
31
+ */
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;
21
39
  }
22
40
  interface MonitorEvent {
23
41
  timestamp: string;
@@ -41,20 +59,43 @@ interface EmitOptions {
41
59
  /** Arbitrary data payload */
42
60
  data?: Record<string, unknown>;
43
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
+ }
44
75
 
45
76
  declare class Monitor {
46
77
  private config;
78
+ private ignoreErrors;
79
+ private onDrop?;
47
80
  private queue;
48
81
  private timer;
49
82
  private userId;
50
83
  private jobId;
51
84
  private active;
85
+ private backoffUntil;
86
+ private failures;
87
+ private warnedMisconfigured;
88
+ private counters;
52
89
  constructor(config: MonitorConfig);
53
90
  /** Set a persistent user ID for all subsequent events */
54
91
  setUser(userId: string): void;
55
92
  /** Clear the user ID */
56
93
  clearUser(): void;
57
- /** 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
+ */
58
99
  setJobId(jobId: string): void;
59
100
  /** Emit an event at a specific level */
60
101
  emit(name: string, level: LogLevel, opts?: EmitOptions): void;
@@ -68,15 +109,51 @@ declare class Monitor {
68
109
  error(name: string, opts?: EmitOptions): void;
69
110
  /** Emit a fatal event */
70
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;
71
118
  /** Flush all queued events to the ingest endpoint */
72
119
  flush(): void;
73
120
  /** Stop the monitor and flush remaining events */
74
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;
75
138
  private start;
76
139
  private handleVisibilityChange;
77
140
  private handlePageHide;
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;
78
153
  private errorHandler;
79
154
  private rejectionHandler;
155
+ private nodeExceptionHandler;
156
+ private nodeRejectionHandler;
80
157
  private installErrorHandler;
81
158
  private installRejectionHandler;
82
159
  private removeListeners;
@@ -109,4 +186,16 @@ interface AxiosMonitorOptions {
109
186
  */
110
187
  declare function attachAxiosMonitor(axiosInstance: AxiosInstance, monitor: Monitor, opts?: AxiosMonitorOptions): void;
111
188
 
112
- export { type AxiosMonitorOptions, type EmitOptions, type LogLevel, Monitor, type MonitorConfig, type MonitorEvent, attachAxiosMonitor };
189
+ /**
190
+ * Whether monitor-core would accept `id` as a job_id, request_id or trace_id.
191
+ * The empty string is valid: the server skips empty ids.
192
+ */
193
+ declare function isValidCorrelationId(id: string): boolean;
194
+ /** A request_id monitor-core accepts: 16 hex characters. */
195
+ declare function newRequestId(): string;
196
+ /** A job_id monitor-core accepts: 16 hex characters. */
197
+ declare function newJobId(): string;
198
+ /** A trace_id monitor-core accepts: a hyphenated UUID v4. */
199
+ declare function newTraceId(): string;
200
+
201
+ export { type AxiosMonitorOptions, type EmitOptions, type LogLevel, Monitor, type MonitorConfig, type MonitorEvent, type MonitorStats, attachAxiosMonitor, isValidCorrelationId, newJobId, newRequestId, newTraceId };