@orbitai/orbitrace 1.1.10 → 1.1.12
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 +27 -1
- package/dist/index.js +143 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,10 +59,36 @@ try {
|
|
|
59
59
|
```javascript
|
|
60
60
|
await orbitrace.captureMessage("User registered", {
|
|
61
61
|
userId: "123",
|
|
62
|
-
email: "user@example.com",
|
|
63
62
|
});
|
|
64
63
|
```
|
|
65
64
|
|
|
65
|
+
### 4. Delivery Behavior
|
|
66
|
+
|
|
67
|
+
Orbitrace never throws and never blocks your application for long:
|
|
68
|
+
|
|
69
|
+
- Each request times out after 5 seconds. Failures are logged to the console and swallowed.
|
|
70
|
+
- Repeats of the same event (same message and stack) within 10 seconds are counted locally and sent as one request with an occurrence count, so an error raised in a loop costs at most two requests per 10 seconds.
|
|
71
|
+
- At most 10 requests are in flight at once; events beyond that are dropped.
|
|
72
|
+
- When the API answers `429`, events are dropped until its `Retry-After` period ends.
|
|
73
|
+
|
|
74
|
+
In short-lived processes (scripts, CI jobs, serverless functions), call `flush()` before exiting so locally counted repeats are sent:
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
await orbitrace.flush();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Data Collected
|
|
81
|
+
|
|
82
|
+
Each event stores:
|
|
83
|
+
|
|
84
|
+
- The error message (truncated to 200 characters) and stack trace, or the message text.
|
|
85
|
+
- The `metadata` object you pass, plus `env`, `version` and `service` from your configuration.
|
|
86
|
+
- The IP address and user agent of the server that sent the request, with an approximate location derived from that IP.
|
|
87
|
+
|
|
88
|
+
Before sending, metadata values whose key contains `password`, `passwd`, `secret`, `token`, `authorization`, `cookie`, `apikey`, `privatekey`, `credential` or `session` (ignoring case, `-` and `_`) are replaced with `[Filtered]`, at any depth. Error messages and stack traces are sent as they are.
|
|
89
|
+
|
|
90
|
+
Filtering is a safety net, not a guarantee. Do not put personal data (emails, names, addresses) or secrets in metadata or error messages; send stable identifiers such as a user id instead.
|
|
91
|
+
|
|
66
92
|
## Environment Variables
|
|
67
93
|
|
|
68
94
|
```env
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
|
+
// Reporting must never slow down or break the host application, so every failure is logged and swallowed.
|
|
2
|
+
const REQUEST_TIMEOUT_MS = 5000;
|
|
3
|
+
// Repeats of the same event inside this window are counted locally and sent once as a single request.
|
|
4
|
+
const DEDUPE_WINDOW_MS = 10000;
|
|
5
|
+
// Caps concurrent requests so a loop of distinct errors cannot exhaust the host's sockets or memory.
|
|
6
|
+
const MAX_IN_FLIGHT = 10;
|
|
7
|
+
const DEFAULT_RETRY_AFTER_SECONDS = 60;
|
|
8
|
+
// Metadata keys containing any of these (ignoring case, "-" and "_") have their values replaced before sending.
|
|
9
|
+
const SENSITIVE_KEY_PARTS = [
|
|
10
|
+
"password",
|
|
11
|
+
"passwd",
|
|
12
|
+
"secret",
|
|
13
|
+
"token",
|
|
14
|
+
"authorization",
|
|
15
|
+
"cookie",
|
|
16
|
+
"apikey",
|
|
17
|
+
"privatekey",
|
|
18
|
+
"credential",
|
|
19
|
+
"session",
|
|
20
|
+
];
|
|
21
|
+
const FILTERED_VALUE = "[Filtered]";
|
|
22
|
+
|
|
23
|
+
function isSensitiveKey(key) {
|
|
24
|
+
const normalized = key.toLowerCase().replace(/[-_]/g, "");
|
|
25
|
+
return SENSITIVE_KEY_PARTS.some((part) => normalized.includes(part));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Recurses only into plain objects and arrays so values like Date keep their JSON form.
|
|
29
|
+
// `ancestors` breaks reference cycles, which would otherwise make the request body unserializable.
|
|
30
|
+
function filterSensitive(value, ancestors = new WeakSet()) {
|
|
31
|
+
if (value === null || typeof value !== "object") {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (ancestors.has(value)) {
|
|
36
|
+
return "[Circular]";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isArray = Array.isArray(value);
|
|
40
|
+
const prototype = Object.getPrototypeOf(value);
|
|
41
|
+
|
|
42
|
+
if (!isArray && prototype !== Object.prototype && prototype !== null) {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
ancestors.add(value);
|
|
47
|
+
const filtered = isArray
|
|
48
|
+
? value.map((item) => filterSensitive(item, ancestors))
|
|
49
|
+
: Object.fromEntries(
|
|
50
|
+
Object.entries(value).map(([key, item]) => [
|
|
51
|
+
key,
|
|
52
|
+
isSensitiveKey(key) ? FILTERED_VALUE : filterSensitive(item, ancestors),
|
|
53
|
+
])
|
|
54
|
+
);
|
|
55
|
+
ancestors.delete(value);
|
|
56
|
+
|
|
57
|
+
return filtered;
|
|
58
|
+
}
|
|
59
|
+
|
|
1
60
|
class Orbitrace {
|
|
2
61
|
static #config = null;
|
|
62
|
+
static #pending = new Map();
|
|
63
|
+
static #inFlight = new Set();
|
|
64
|
+
static #pausedUntil = 0;
|
|
3
65
|
|
|
4
66
|
static getInstance(config) {
|
|
5
67
|
this.assertServerRuntime();
|
|
@@ -28,14 +90,6 @@ class Orbitrace {
|
|
|
28
90
|
}
|
|
29
91
|
}
|
|
30
92
|
|
|
31
|
-
static checkInitialized() {
|
|
32
|
-
if (!this.#config) {
|
|
33
|
-
throw new Error(
|
|
34
|
-
"Orbitrace must be initialized with Orbitrace.getInstance() before use"
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
93
|
static assertServerRuntime() {
|
|
40
94
|
if (typeof window !== "undefined" || typeof document !== "undefined") {
|
|
41
95
|
throw new Error(
|
|
@@ -45,57 +99,93 @@ class Orbitrace {
|
|
|
45
99
|
}
|
|
46
100
|
|
|
47
101
|
static async captureException(error, metadata = {}) {
|
|
48
|
-
|
|
102
|
+
const message = error?.message || error?.name || String(error);
|
|
103
|
+
|
|
104
|
+
return this.#capture("capture_exception", {
|
|
105
|
+
message,
|
|
106
|
+
stack: error?.stack || message,
|
|
107
|
+
metadata,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
static async captureMessage(message, metadata = {}) {
|
|
112
|
+
return this.#capture("capture_message", { message, metadata });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Sends counts still held by the dedupe window and waits for in-flight requests.
|
|
116
|
+
// Call it before a short-lived process or serverless invocation ends.
|
|
117
|
+
static async flush() {
|
|
118
|
+
await Promise.all([...this.#pending.keys()].map((key) => this.#flushPending(key)));
|
|
119
|
+
await Promise.all(this.#inFlight);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static async #capture(event, { metadata, ...fields }) {
|
|
123
|
+
if (!this.#config) {
|
|
124
|
+
console.error("Orbitrace: call Orbitrace.getInstance() before capturing events");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
49
127
|
|
|
50
128
|
if (this.#config.disabled) {
|
|
51
129
|
console.log("Orbitrace: Logging is disabled");
|
|
52
130
|
return;
|
|
53
131
|
}
|
|
54
132
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
133
|
+
const payload = {
|
|
134
|
+
...fields,
|
|
135
|
+
metadata: {
|
|
136
|
+
...filterSensitive(metadata),
|
|
137
|
+
env: this.#config.environment,
|
|
138
|
+
version: this.#config.version,
|
|
139
|
+
service: this.#config.service,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
const key = JSON.stringify([event, payload.message, payload.stack ?? null]);
|
|
143
|
+
const pending = this.#pending.get(key);
|
|
66
144
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
145
|
+
if (pending) {
|
|
146
|
+
pending.count++;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!this.#canSend()) {
|
|
151
|
+
return;
|
|
70
152
|
}
|
|
153
|
+
|
|
154
|
+
const entry = { event, payload, count: 0 };
|
|
155
|
+
entry.timer = setTimeout(() => this.#flushPending(key), DEDUPE_WINDOW_MS);
|
|
156
|
+
entry.timer.unref?.();
|
|
157
|
+
this.#pending.set(key, entry);
|
|
158
|
+
|
|
159
|
+
return this.#send(event, payload, 1);
|
|
71
160
|
}
|
|
72
161
|
|
|
73
|
-
static async
|
|
74
|
-
this.
|
|
162
|
+
static async #flushPending(key) {
|
|
163
|
+
const entry = this.#pending.get(key);
|
|
75
164
|
|
|
76
|
-
if (
|
|
77
|
-
console.log("Orbitrace: Logging is disabled");
|
|
165
|
+
if (!entry) {
|
|
78
166
|
return;
|
|
79
167
|
}
|
|
80
168
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
message,
|
|
84
|
-
metadata: {
|
|
85
|
-
...metadata,
|
|
86
|
-
env: this.#config.environment,
|
|
87
|
-
version: this.#config.version,
|
|
88
|
-
service: this.#config.service,
|
|
89
|
-
},
|
|
90
|
-
};
|
|
169
|
+
this.#pending.delete(key);
|
|
170
|
+
clearTimeout(entry.timer);
|
|
91
171
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
console.error("Orbitrace: Error capturing message", err);
|
|
172
|
+
if (entry.count > 0 && this.#canSend()) {
|
|
173
|
+
await this.#send(entry.event, entry.payload, entry.count);
|
|
95
174
|
}
|
|
96
175
|
}
|
|
97
176
|
|
|
98
|
-
static
|
|
177
|
+
static #canSend() {
|
|
178
|
+
return Date.now() >= this.#pausedUntil && this.#inFlight.size < MAX_IN_FLIGHT;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
static #send(event, payload, count) {
|
|
182
|
+
const request = this.#post(event, payload, count).finally(() => this.#inFlight.delete(request));
|
|
183
|
+
this.#inFlight.add(request);
|
|
184
|
+
|
|
185
|
+
return request;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static async #post(event, payload, count) {
|
|
99
189
|
try {
|
|
100
190
|
const response = await fetch(this.#config.endpoint, {
|
|
101
191
|
method: "POST",
|
|
@@ -107,18 +197,27 @@ class Orbitrace {
|
|
|
107
197
|
},
|
|
108
198
|
body: JSON.stringify({
|
|
109
199
|
event,
|
|
200
|
+
...(count > 1 && { count }),
|
|
110
201
|
payload,
|
|
111
202
|
}),
|
|
203
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
112
204
|
});
|
|
113
205
|
|
|
206
|
+
if (response.status === 429) {
|
|
207
|
+
const retryAfterSeconds = Number(response.headers.get("retry-after")) || DEFAULT_RETRY_AFTER_SECONDS;
|
|
208
|
+
this.#pausedUntil = Date.now() + retryAfterSeconds * 1000;
|
|
209
|
+
console.warn(`Orbitrace: rate limited, dropping events for ${retryAfterSeconds}s`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
114
213
|
if (!response.ok) {
|
|
115
|
-
|
|
214
|
+
console.error(`Orbitrace: API responded with status ${response.status}`);
|
|
215
|
+
return;
|
|
116
216
|
}
|
|
117
217
|
|
|
118
218
|
return await response.json();
|
|
119
219
|
} catch (error) {
|
|
120
|
-
console.error("Orbitrace: Error sending data to API", error);
|
|
121
|
-
throw error;
|
|
220
|
+
console.error("Orbitrace: Error sending data to API", error.message);
|
|
122
221
|
}
|
|
123
222
|
}
|
|
124
223
|
}
|