@orbitai/orbitrace 1.1.10 → 1.1.11

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 +15 -0
  2. package/dist/index.js +92 -44
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -63,6 +63,21 @@ await orbitrace.captureMessage("User registered", {
63
63
  });
64
64
  ```
65
65
 
66
+ ### 4. Delivery Behavior
67
+
68
+ Orbitrace never throws and never blocks your application for long:
69
+
70
+ - Each request times out after 5 seconds. Failures are logged to the console and swallowed.
71
+ - 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.
72
+ - At most 10 requests are in flight at once; events beyond that are dropped.
73
+ - When the API answers `429`, events are dropped until its `Retry-After` period ends.
74
+
75
+ In short-lived processes (scripts, CI jobs, serverless functions), call `flush()` before exiting so locally counted repeats are sent:
76
+
77
+ ```javascript
78
+ await orbitrace.flush();
79
+ ```
80
+
66
81
  ## Environment Variables
67
82
 
68
83
  ```env
package/dist/index.js CHANGED
@@ -1,5 +1,16 @@
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
+
1
9
  class Orbitrace {
2
10
  static #config = null;
11
+ static #pending = new Map();
12
+ static #inFlight = new Set();
13
+ static #pausedUntil = 0;
3
14
 
4
15
  static getInstance(config) {
5
16
  this.assertServerRuntime();
@@ -28,14 +39,6 @@ class Orbitrace {
28
39
  }
29
40
  }
30
41
 
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
42
  static assertServerRuntime() {
40
43
  if (typeof window !== "undefined" || typeof document !== "undefined") {
41
44
  throw new Error(
@@ -45,57 +48,93 @@ class Orbitrace {
45
48
  }
46
49
 
47
50
  static async captureException(error, metadata = {}) {
48
- this.checkInitialized();
51
+ const message = error?.message || error?.name || String(error);
52
+
53
+ return this.#capture("capture_exception", {
54
+ message,
55
+ stack: error?.stack || message,
56
+ metadata,
57
+ });
58
+ }
59
+
60
+ static async captureMessage(message, metadata = {}) {
61
+ return this.#capture("capture_message", { message, metadata });
62
+ }
63
+
64
+ // Sends counts still held by the dedupe window and waits for in-flight requests.
65
+ // Call it before a short-lived process or serverless invocation ends.
66
+ static async flush() {
67
+ await Promise.all([...this.#pending.keys()].map((key) => this.#flushPending(key)));
68
+ await Promise.all(this.#inFlight);
69
+ }
70
+
71
+ static async #capture(event, { metadata, ...fields }) {
72
+ if (!this.#config) {
73
+ console.error("Orbitrace: call Orbitrace.getInstance() before capturing events");
74
+ return;
75
+ }
49
76
 
50
77
  if (this.#config.disabled) {
51
78
  console.log("Orbitrace: Logging is disabled");
52
79
  return;
53
80
  }
54
81
 
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
- };
82
+ const payload = {
83
+ ...fields,
84
+ metadata: {
85
+ ...metadata,
86
+ env: this.#config.environment,
87
+ version: this.#config.version,
88
+ service: this.#config.service,
89
+ },
90
+ };
91
+ const key = JSON.stringify([event, payload.message, payload.stack ?? null]);
92
+ const pending = this.#pending.get(key);
66
93
 
67
- return this.sendToApi("capture_exception", payload);
68
- } catch (err) {
69
- console.error("Orbitrace: Error capturing exception", err);
94
+ if (pending) {
95
+ pending.count++;
96
+ return;
97
+ }
98
+
99
+ if (!this.#canSend()) {
100
+ return;
70
101
  }
102
+
103
+ const entry = { event, payload, count: 0 };
104
+ entry.timer = setTimeout(() => this.#flushPending(key), DEDUPE_WINDOW_MS);
105
+ entry.timer.unref?.();
106
+ this.#pending.set(key, entry);
107
+
108
+ return this.#send(event, payload, 1);
71
109
  }
72
110
 
73
- static async captureMessage(message, metadata = {}) {
74
- this.checkInitialized();
111
+ static async #flushPending(key) {
112
+ const entry = this.#pending.get(key);
75
113
 
76
- if (this.#config.disabled) {
77
- console.log("Orbitrace: Logging is disabled");
114
+ if (!entry) {
78
115
  return;
79
116
  }
80
117
 
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
- };
118
+ this.#pending.delete(key);
119
+ clearTimeout(entry.timer);
91
120
 
92
- return this.sendToApi("capture_message", payload);
93
- } catch (err) {
94
- console.error("Orbitrace: Error capturing message", err);
121
+ if (entry.count > 0 && this.#canSend()) {
122
+ await this.#send(entry.event, entry.payload, entry.count);
95
123
  }
96
124
  }
97
125
 
98
- static async sendToApi(event, payload) {
126
+ static #canSend() {
127
+ return Date.now() >= this.#pausedUntil && this.#inFlight.size < MAX_IN_FLIGHT;
128
+ }
129
+
130
+ static #send(event, payload, count) {
131
+ const request = this.#post(event, payload, count).finally(() => this.#inFlight.delete(request));
132
+ this.#inFlight.add(request);
133
+
134
+ return request;
135
+ }
136
+
137
+ static async #post(event, payload, count) {
99
138
  try {
100
139
  const response = await fetch(this.#config.endpoint, {
101
140
  method: "POST",
@@ -107,18 +146,27 @@ class Orbitrace {
107
146
  },
108
147
  body: JSON.stringify({
109
148
  event,
149
+ ...(count > 1 && { count }),
110
150
  payload,
111
151
  }),
152
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
112
153
  });
113
154
 
155
+ if (response.status === 429) {
156
+ const retryAfterSeconds = Number(response.headers.get("retry-after")) || DEFAULT_RETRY_AFTER_SECONDS;
157
+ this.#pausedUntil = Date.now() + retryAfterSeconds * 1000;
158
+ console.warn(`Orbitrace: rate limited, dropping events for ${retryAfterSeconds}s`);
159
+ return;
160
+ }
161
+
114
162
  if (!response.ok) {
115
- throw new Error(`HTTP error! status: ${response.status}`);
163
+ console.error(`Orbitrace: API responded with status ${response.status}`);
164
+ return;
116
165
  }
117
166
 
118
167
  return await response.json();
119
168
  } catch (error) {
120
- console.error("Orbitrace: Error sending data to API", error);
121
- throw error;
169
+ console.error("Orbitrace: Error sending data to API", error.message);
122
170
  }
123
171
  }
124
172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbitai/orbitrace",
3
- "version": "1.1.10",
3
+ "version": "1.1.11",
4
4
  "description": "Error tracking and monitoring for backend Node.js applications",
5
5
  "main": "dist/index.js",
6
6
  "files": [