@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.
Files changed (3) hide show
  1. package/README.md +27 -1
  2. package/dist/index.js +143 -44
  3. 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
- this.checkInitialized();
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
- try {
56
- const payload = {
57
- message: error.message,
58
- stack: error.stack,
59
- metadata: {
60
- ...metadata,
61
- env: this.#config.environment,
62
- version: this.#config.version,
63
- service: this.#config.service,
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
- return this.sendToApi("capture_exception", payload);
68
- } catch (err) {
69
- console.error("Orbitrace: Error capturing exception", err);
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 captureMessage(message, metadata = {}) {
74
- this.checkInitialized();
162
+ static async #flushPending(key) {
163
+ const entry = this.#pending.get(key);
75
164
 
76
- if (this.#config.disabled) {
77
- console.log("Orbitrace: Logging is disabled");
165
+ if (!entry) {
78
166
  return;
79
167
  }
80
168
 
81
- try {
82
- const payload = {
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
- return this.sendToApi("capture_message", payload);
93
- } catch (err) {
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 async sendToApi(event, payload) {
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
- throw new Error(`HTTP error! status: ${response.status}`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbitai/orbitrace",
3
- "version": "1.1.10",
3
+ "version": "1.1.12",
4
4
  "description": "Error tracking and monitoring for backend Node.js applications",
5
5
  "main": "dist/index.js",
6
6
  "files": [