@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 +102 -0
- package/dist/index.d.mts +93 -4
- package/dist/index.d.ts +93 -4
- package/dist/index.js +329 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +324 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
/**
|
|
5
|
+
/** Full ingest endpoint of one zone (e.g. https://appleby-monitor-api.appleby.cloud/v1/events) */
|
|
6
6
|
ingestUrl: string;
|
|
7
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
5
|
+
/** Full ingest endpoint of one zone (e.g. https://appleby-monitor-api.appleby.cloud/v1/events) */
|
|
6
6
|
ingestUrl: string;
|
|
7
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 };
|