@orbitai/orbitrace 1.1.9 → 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 +30 -135
  2. package/dist/index.js +96 -49
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @orbitai/orbitrace
2
2
 
3
- Error tracking and monitoring for Node.js and browser applications.
3
+ Error tracking and monitoring for backend Node.js applications.
4
+
5
+ Orbitrace is server-side only. Do not initialize this package in browser or frontend code, because the Orbitrace API key is a backend secret and must not be exposed in public bundles.
4
6
 
5
7
  ## Installation
6
8
 
@@ -17,8 +19,6 @@ npm install @orbitai/orbitrace
17
19
  const Orbitrace = require("@orbitai/orbitrace");
18
20
 
19
21
  function getOrbitraceInstance({ service }) {
20
- console.log("Initializing Orbitrace for service:", service);
21
-
22
22
  Orbitrace.getInstance({
23
23
  apiKey: process.env.ORBITRACE_API_KEY,
24
24
  orgId: process.env.ORBITRACE_ORG_ID,
@@ -27,7 +27,7 @@ function getOrbitraceInstance({ service }) {
27
27
  environment: process.env.APP_ENV,
28
28
  service,
29
29
  version: process.env.APP_VERSION,
30
- disabled: process.env.APP_ENV !== "production",
30
+ disabled: process.env.ORBITRACE_DISABLED === "true",
31
31
  });
32
32
 
33
33
  return Orbitrace;
@@ -38,165 +38,60 @@ module.exports = {
38
38
  };
39
39
  ```
40
40
 
41
- ### 2. Usage
41
+ ### 2. Capture Exceptions
42
42
 
43
43
  ```javascript
44
- const { getOrbitraceInstance } = require("/path/to/orbitrace");
44
+ const { getOrbitraceInstance } = require("./orbitrace");
45
45
  const orbitrace = getOrbitraceInstance({ service: "api" });
46
46
 
47
- // Capture exceptions
48
47
  try {
49
48
  // Your code here
50
49
  } catch (error) {
51
50
  await orbitrace.captureException(error, {
52
51
  endpoint: "/users",
52
+ action: "createUser",
53
53
  });
54
54
  }
55
-
56
- // Capture messages
57
- await orbitrace.captureMessage("User registered", {
58
- userId: "123",
59
- email: "user@example.com",
60
- });
61
- ```
62
-
63
- ## Frontend Setup (Vue 3 + Pinia)
64
-
65
- ### 1. Initialize Orbitrace
66
-
67
- ```javascript
68
- // orbitrace.js
69
- import { provide, inject } from "vue";
70
- import Orbitrace from "@orbitai/orbitrace";
71
-
72
- export const ORBITRACE_KEY = Symbol("orbitrace");
73
-
74
- export function getOrbitraceInstance({ service }) {
75
- Orbitrace.getInstance({
76
- apiKey: import.meta.env.VITE_ORBITRACE_API_KEY,
77
- orgId: import.meta.env.VITE_ORBITRACE_ORG_ID,
78
- projectId: import.meta.env.VITE_ORBITRACE_PROJECT_ID,
79
- endpoint: import.meta.env.VITE_ORBITRACE_ENDPOINT,
80
- environment: import.meta.env.VITE_APP_ENV,
81
- service,
82
- version: import.meta.env.VITE_APP_VERSION,
83
- disabled: process.env.APP_ENV !== "production",
84
- });
85
-
86
- return orbitrace;
87
- }
88
-
89
- export function useOrbitrace() {
90
- const orbitrace = inject(ORBITRACE_KEY);
91
- if (!orbitrace) {
92
- throw new Error("Orbitrace not properly initialized");
93
- }
94
- return orbitrace;
95
- }
96
55
  ```
97
56
 
98
- ### 2. Setup in Main App
57
+ ### 3. Capture Messages
99
58
 
100
59
  ```javascript
101
- // main.js
102
- import { createApp } from "vue";
103
- import { createPinia } from "pinia";
104
- import App from "./App.vue";
105
- import { getOrbitraceInstance, ORBITRACE_KEY } from "/path/to/orbitrace";
106
-
107
- const app = createApp(App);
108
- const pinia = createPinia();
109
-
110
- // Initialize Orbitrace
111
- const orbitrace = getOrbitraceInstance({ service: "webapp" });
112
-
113
- // Make Orbitrace available everywhere
114
- app.provide(ORBITRACE_KEY, orbitrace);
115
-
116
- // Add Orbitrace to Pinia
117
- pinia.use(({ store }) => {
118
- store.orbitrace = orbitrace;
60
+ await orbitrace.captureMessage("User registered", {
61
+ userId: "123",
62
+ email: "user@example.com",
119
63
  });
120
-
121
- app.use(pinia);
122
- app.mount("#app");
123
64
  ```
124
65
 
125
- ### 3. Usage in Vue Components
66
+ ### 4. Delivery Behavior
126
67
 
127
- ```vue
128
- <script setup>
129
- import { useOrbitrace } from "/path/to/orbitrace";
68
+ Orbitrace never throws and never blocks your application for long:
130
69
 
131
- const orbitrace = useOrbitrace();
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.
132
74
 
133
- async function handleUserAction() {
134
- try {
135
- // Your code here
136
- } catch (error) {
137
- await orbitrace.captureException(error, {
138
- component: "UserProfile",
139
- action: "updateProfile",
140
- });
141
- }
142
- }
143
-
144
- // Log custom events
145
- async function trackEvent() {
146
- await orbitrace.captureMessage("User completed onboarding", {
147
- userId: "user123",
148
- completedSteps: ["profile", "preferences"],
149
- });
150
- }
151
- </script>
152
- ```
153
-
154
- ### 4. Usage in Pinia Stores
75
+ In short-lived processes (scripts, CI jobs, serverless functions), call `flush()` before exiting so locally counted repeats are sent:
155
76
 
156
77
  ```javascript
157
- // stores/userStore.js
158
- import { defineStore } from "pinia";
159
-
160
- export const useUserStore = defineStore("user", {
161
- state: () => ({
162
- user: null,
163
- }),
164
- actions: {
165
- async fetchUser() {
166
- try {
167
- // Fetch user logic
168
- } catch (error) {
169
- await this.orbitrace.captureException(error, {
170
- store: "userStore",
171
- action: "fetchUser",
172
- });
173
- throw error;
174
- }
175
- },
176
- },
177
- });
78
+ await orbitrace.flush();
178
79
  ```
179
80
 
180
- ### Environment Variables
181
-
182
- For Vite:
81
+ ## Environment Variables
183
82
 
184
83
  ```env
185
- VITE_ORBITRACE_API_KEY=your-api-key
186
- VITE_ORBITRACE_ORG_ID=your-org-id
187
- VITE_ORBITRACE_PROJECT_ID=your-project-id
188
- VITE_ORBITRACE_ENDPOINT=your-endpoint
189
- VITE_APP_ENV=your-environment
190
- VITE_APP_VERSION=your-version
191
- ```
192
-
193
- For Node.js:
194
-
195
- ```env
196
- ORBITRACE_API_KEY=your-api-key
84
+ ORBITRACE_API_KEY=your-backend-api-key
197
85
  ORBITRACE_ORG_ID=your-org-id
198
86
  ORBITRACE_PROJECT_ID=your-project-id
199
87
  ORBITRACE_ENDPOINT=your-endpoint
200
- APP_ENV=your-environment
201
- APP_VERSION=your-version
88
+ APP_ENV=production
89
+ APP_VERSION=1.0.0
90
+ ORBITRACE_DISABLED=false
202
91
  ```
92
+
93
+ ## Security Model
94
+
95
+ Orbitrace authenticates ingestion requests with `ORBITRACE_API_KEY`. Keep this key only in backend environments such as servers, workers, functions, and CI jobs. Do not use `VITE_`, `NEXT_PUBLIC_`, or other public frontend environment variables for Orbitrace credentials.
96
+
97
+ If this package is initialized in a browser runtime, it throws an error before sending events.
package/dist/index.js CHANGED
@@ -1,7 +1,19 @@
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) {
16
+ this.assertServerRuntime();
5
17
  this.validateConfig(config);
6
18
 
7
19
  this.#config = {
@@ -27,72 +39,104 @@ class Orbitrace {
27
39
  }
28
40
  }
29
41
 
30
- static checkInitialized() {
31
- if (!this.#config) {
42
+ static assertServerRuntime() {
43
+ if (typeof window !== "undefined" || typeof document !== "undefined") {
32
44
  throw new Error(
33
- "Orbitrace must be initialized with Orbitrace.getInstance() before use"
45
+ "Orbitrace can only be initialized in backend/server runtimes"
34
46
  );
35
47
  }
36
48
  }
37
49
 
38
- static isBrowser() {
39
- return typeof window !== "undefined" && Boolean(window.location?.origin);
50
+ static async captureException(error, metadata = {}) {
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
+ });
40
58
  }
41
59
 
42
- static async captureException(error, metadata = {}) {
43
- this.checkInitialized();
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
+ }
44
76
 
45
77
  if (this.#config.disabled) {
46
78
  console.log("Orbitrace: Logging is disabled");
47
79
  return;
48
80
  }
49
81
 
50
- try {
51
- const payload = {
52
- message: error.message,
53
- stack: error.stack,
54
- metadata: {
55
- ...metadata,
56
- env: this.#config.environment,
57
- version: this.#config.version,
58
- service: this.#config.service,
59
- },
60
- };
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);
93
+
94
+ if (pending) {
95
+ pending.count++;
96
+ return;
97
+ }
61
98
 
62
- return this.sendToApi("capture_exception", payload);
63
- } catch (err) {
64
- console.error("Orbitrace: Error capturing exception", err);
99
+ if (!this.#canSend()) {
100
+ return;
65
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);
66
109
  }
67
110
 
68
- static async captureMessage(message, metadata = {}) {
69
- this.checkInitialized();
111
+ static async #flushPending(key) {
112
+ const entry = this.#pending.get(key);
70
113
 
71
- if (this.#config.disabled) {
72
- console.log("Orbitrace: Logging is disabled");
114
+ if (!entry) {
73
115
  return;
74
116
  }
75
117
 
76
- try {
77
- const payload = {
78
- message,
79
- metadata: {
80
- ...metadata,
81
- env: this.#config.environment,
82
- version: this.#config.version,
83
- service: this.#config.service,
84
- },
85
- };
118
+ this.#pending.delete(key);
119
+ clearTimeout(entry.timer);
86
120
 
87
- return this.sendToApi("capture_message", payload);
88
- } catch (err) {
89
- console.error("Orbitrace: Error capturing message", err);
121
+ if (entry.count > 0 && this.#canSend()) {
122
+ await this.#send(entry.event, entry.payload, entry.count);
90
123
  }
91
124
  }
92
125
 
93
- 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) {
94
138
  try {
95
- const fetchOptions = {
139
+ const response = await fetch(this.#config.endpoint, {
96
140
  method: "POST",
97
141
  headers: {
98
142
  "Content-Type": "application/json",
@@ -102,24 +146,27 @@ class Orbitrace {
102
146
  },
103
147
  body: JSON.stringify({
104
148
  event,
149
+ ...(count > 1 && { count }),
105
150
  payload,
106
151
  }),
107
- };
108
-
109
- if (this.isBrowser()) {
110
- fetchOptions.referrerPolicy = "origin";
152
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
153
+ });
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;
111
160
  }
112
161
 
113
- const response = await fetch(this.#config.endpoint, fetchOptions);
114
-
115
162
  if (!response.ok) {
116
- throw new Error(`HTTP error! status: ${response.status}`);
163
+ console.error(`Orbitrace: API responded with status ${response.status}`);
164
+ return;
117
165
  }
118
166
 
119
167
  return await response.json();
120
168
  } catch (error) {
121
- console.error("Orbitrace: Error sending data to API", error);
122
- throw error;
169
+ console.error("Orbitrace: Error sending data to API", error.message);
123
170
  }
124
171
  }
125
172
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orbitai/orbitrace",
3
- "version": "1.1.9",
4
- "description": "Error tracking and monitoring for Node.js and browser applications",
3
+ "version": "1.1.11",
4
+ "description": "Error tracking and monitoring for backend Node.js applications",
5
5
  "main": "dist/index.js",
6
6
  "files": [
7
7
  "dist"