@orbitai/orbitrace 1.2.1 → 1.4.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # @orbitai/orbitrace
2
2
 
3
- Error tracking and monitoring for backend Node.js applications.
3
+ Error tracking and monitoring for Node.js services and browser apps.
4
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.
5
+ The package has two entries. `@orbitai/orbitrace` runs on servers with the project's API key, which is a secret. `@orbitai/orbitrace/browser` runs in browsers with the project's public key, which is accepted only from the origins allowed in the Orbitrace dashboard. Never put the API key in a browser bundle; the server entry throws if it is loaded in a browser.
6
6
 
7
7
  ## Installation
8
8
 
@@ -131,6 +131,56 @@ exports.checkout = onRequest(orbitrace.wrap(async (req, res) => {
131
131
  }));
132
132
  ```
133
133
 
134
+ ## Browser Setup
135
+
136
+ Install the same package and import the browser entry. It needs a browser with `fetch`, so every current browser.
137
+
138
+ ```javascript
139
+ import Orbitrace from "@orbitai/orbitrace/browser";
140
+
141
+ Orbitrace.init({
142
+ publicKey: "the project's public key",
143
+ orgId: "your-org-id",
144
+ projectId: "your-project-id",
145
+ endpoint: "your-endpoint",
146
+ environment: import.meta.env.MODE,
147
+ version: "1.0.0",
148
+ service: "webapp",
149
+ });
150
+ ```
151
+
152
+ The public key can be read by anyone who opens your site. That is fine: it only creates events, the API accepts it only from the origins allowed for the project (dashboard, Documentation, Browser apps), and the project's rate limit and your organization's quota bound what it can do. `init` throws if it is given an `apiKey`.
153
+
154
+ Automatic capture is on unless `autoCapture: false`. Uncaught errors and unhandled promise rejections are reported; breadcrumbs are recorded from `console` calls, `fetch` requests, clicks (the element, never its text) and navigation. Switch parts off with `autoCapture: { console: false, clicks: false }`; the parts are `exceptions`, `console`, `fetch`, `clicks` and `navigation`.
155
+
156
+ `captureException`, `captureMessage`, `addBreadcrumb`, `flush` and `close` work as on servers. There are no scopes: one tab is one scope.
157
+
158
+ ```javascript
159
+ // After sign-in, so issues show who was affected. Send a stable id, never personal data.
160
+ Orbitrace.setUser({ id: user.uid });
161
+ // On sign-out:
162
+ Orbitrace.setUser(null);
163
+
164
+ // Vue 3: errors Vue catches inside components are reported with the component name,
165
+ // then passed to the app's own errorHandler, or logged as Vue would.
166
+ app.use(Orbitrace.vue);
167
+ ```
168
+
169
+ Every browser event carries `page`: the URL (without query string or fragment), referrer, viewport and language. Requests use `keepalive`, so an error reported just before the page closes still arrives. Production stacks are minified; issues still group correctly across deploys because file hashes and line numbers are ignored, so keep `version` set to your release to see which build introduced a problem.
170
+
171
+ ## Source Maps
172
+
173
+ Production bundles are minified, so browser stacks name files like `index-39fc6623.js` and functions like `Xr`. Upload the build's source maps and Orbitrace resolves each event of that release to the original file, line and function, groups issues by them, and shows a few lines of source around each frame. Maps are stored privately, never served to browsers, and deleted after 90 days.
174
+
175
+ Build with hidden source maps (Vite: `build.sourcemap = "hidden"`), then run the command the package installs, from a build or deploy job that has the server API key:
176
+
177
+ ```bash
178
+ ORBITRACE_API_KEY=... ORBITRACE_ORG_ID=... ORBITRACE_PROJECT_ID=... ORBITRACE_ENDPOINT=... \
179
+ npx orbitrace-sourcemaps upload --release 1.22.9 --dir dist --delete
180
+ ```
181
+
182
+ `--release` must equal the `version` the browser SDK sends. `--dir` is searched for `.map` files. `--delete` removes them afterwards so a hosting deploy of the same directory cannot publish them. Upload before the bundle goes live: events that arrive before their release's maps keep the minified stack.
183
+
134
184
  ## Data Collected
135
185
 
136
186
  Each event stores:
@@ -138,7 +188,7 @@ Each event stores:
138
188
  - The error message (truncated to 200 characters) and stack trace, or the message text.
139
189
  - The `metadata` object you pass, plus `env`, `version` and `service` from your configuration.
140
190
  - Breadcrumbs, when there are any: the last 30 recorded before the event.
141
- - The IP address and user agent of the server that sent the request, with an approximate location derived from that IP.
191
+ - The IP address and user agent of the server that sent the request, with an approximate location derived from that IP. For browser events, the user agent and approximate location of the user; their IP address is not stored.
142
192
 
143
193
  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.
144
194
 
@@ -158,6 +208,6 @@ ORBITRACE_DISABLED=false
158
208
 
159
209
  ## Security Model
160
210
 
161
- 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.
211
+ Each project has two keys. The API key authenticates servers: keep it only in backend environments such as servers, workers, functions, and CI jobs, and never in `VITE_`, `NEXT_PUBLIC_` or other public frontend environment variables. The public key authenticates browsers: it may ship in bundles, and the API accepts it only from the project's allowed origins. Both keys can only create events.
162
212
 
163
- If this package is initialized in a browser runtime, it throws an error before sending events.
213
+ The server entry throws if it is initialized in a browser, and the browser entry throws if it is given an API key.
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ // Uploads a build's source maps to Orbitrace so browser stacks of that release resolve to original files.
3
+ //
4
+ // orbitrace-sourcemaps upload --release 1.22.9 --dir dist [--delete]
5
+ //
6
+ // Reads ORBITRACE_API_KEY, ORBITRACE_ORG_ID, ORBITRACE_PROJECT_ID and ORBITRACE_ENDPOINT from the
7
+ // environment (--org, --project and --endpoint override the last three). The API key is the project's
8
+ // secret server key, never the public key. --delete removes the maps from the directory afterwards, so a
9
+ // hosting deploy of that directory cannot publish them.
10
+ const fs = require("node:fs/promises");
11
+ const path = require("node:path");
12
+
13
+ async function findMaps(dir) {
14
+ const found = [];
15
+
16
+ for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
17
+ const full = path.join(dir, entry.name);
18
+
19
+ if (entry.isDirectory()) {
20
+ found.push(...(await findMaps(full)));
21
+ } else if (entry.isFile() && entry.name.endsWith(".map")) {
22
+ found.push(full);
23
+ }
24
+ }
25
+
26
+ return found;
27
+ }
28
+
29
+ const FLAGS = new Set(["delete"]);
30
+
31
+ function parseArgs(argv) {
32
+ const options = { _: [] };
33
+
34
+ for (let i = 0; i < argv.length; i++) {
35
+ const arg = argv[i];
36
+
37
+ if (arg.startsWith("--")) {
38
+ const [name, inline] = arg.slice(2).split("=");
39
+ const next = argv[i + 1];
40
+
41
+ if (FLAGS.has(name)) {
42
+ options[name] = true;
43
+ } else if (inline !== undefined) {
44
+ options[name] = inline;
45
+ } else if (next !== undefined && !next.startsWith("--")) {
46
+ options[name] = next;
47
+ i++;
48
+ } else {
49
+ options[name] = true;
50
+ }
51
+ } else {
52
+ options._.push(arg);
53
+ }
54
+ }
55
+
56
+ return options;
57
+ }
58
+
59
+ async function request(url, { apiKey, orgId, projectId }, body) {
60
+ const response = await fetch(url, {
61
+ method: "POST",
62
+ headers: {
63
+ "Content-Type": "application/json",
64
+ "x-orbitrace-api-key": apiKey,
65
+ "x-orbitrace-org-id": orgId,
66
+ "x-orbitrace-project-id": projectId,
67
+ },
68
+ body: JSON.stringify(body),
69
+ });
70
+ const text = await response.text();
71
+ let json;
72
+
73
+ try {
74
+ json = JSON.parse(text);
75
+ } catch {
76
+ json = { error: text };
77
+ }
78
+
79
+ if (!response.ok) {
80
+ throw new Error(`${url} answered ${response.status}: ${json.error || json.message || text}`);
81
+ }
82
+
83
+ return json;
84
+ }
85
+
86
+ async function upload({ release, dir, apiKey, orgId, projectId, endpoint, remove = false, log = console.log }) {
87
+ for (const [name, value] of Object.entries({ release, dir, apiKey, orgId, projectId, endpoint })) {
88
+ if (!value) {
89
+ throw new Error(`Missing ${name}`);
90
+ }
91
+ }
92
+
93
+ // The configured endpoint is the events URL (".../log"); the source map routes live next to it.
94
+ const base = String(endpoint).replace(/\/log\/?$/, "");
95
+ const credentials = { apiKey, orgId, projectId };
96
+ const paths = await findMaps(dir);
97
+
98
+ if (paths.length === 0) {
99
+ throw new Error(`No .map files found under ${dir}. Build with source maps enabled (Vite: build.sourcemap = "hidden").`);
100
+ }
101
+
102
+ const files = await Promise.all(
103
+ paths.map(async (file) => ({ path: file, name: path.basename(file), size: (await fs.stat(file)).size })),
104
+ );
105
+ const names = new Set();
106
+
107
+ for (const file of files) {
108
+ if (names.has(file.name)) {
109
+ throw new Error(`Two source maps are named ${file.name}; names must be unique within a release`);
110
+ }
111
+
112
+ names.add(file.name);
113
+ }
114
+
115
+ const declared = await request(`${base}/sourcemaps/releases`, credentials, {
116
+ release,
117
+ files: files.map(({ name, size }) => ({ name, size })),
118
+ });
119
+ const uploads = new Map(declared.uploads.map((item) => [item.name, item]));
120
+
121
+ for (const file of files) {
122
+ const target = uploads.get(file.name);
123
+
124
+ if (!target) {
125
+ throw new Error(`The API returned no upload URL for ${file.name}`);
126
+ }
127
+
128
+ // The signed URL only accepts the headers it was signed with.
129
+ const response = await fetch(target.url, {
130
+ method: "PUT",
131
+ headers: target.headers,
132
+ body: await fs.readFile(file.path),
133
+ });
134
+
135
+ if (!response.ok) {
136
+ throw new Error(`Uploading ${file.name} failed with status ${response.status}`);
137
+ }
138
+
139
+ log(`Uploaded ${file.name} (${file.size} bytes)`);
140
+ }
141
+
142
+ const completed = await request(`${base}/sourcemaps/releases/complete`, credentials, {
143
+ release,
144
+ files: files.map(({ name }) => name),
145
+ });
146
+
147
+ if (remove) {
148
+ await Promise.all(files.map((file) => fs.unlink(file.path)));
149
+ log(`Removed ${files.length} source map${files.length === 1 ? "" : "s"} from ${dir}`);
150
+ }
151
+
152
+ log(`Release ${release}: ${completed.files} source maps, ${completed.bytes} bytes`);
153
+
154
+ if (completed.deletedReleases?.length > 0) {
155
+ log(`Dropped older releases: ${completed.deletedReleases.join(", ")}`);
156
+ }
157
+
158
+ return completed;
159
+ }
160
+
161
+ async function main(argv) {
162
+ const options = parseArgs(argv);
163
+ const command = options._[0];
164
+
165
+ if (command !== "upload") {
166
+ console.error("Usage: orbitrace-sourcemaps upload --release <version> --dir <directory> [--delete]");
167
+ process.exitCode = 1;
168
+ return;
169
+ }
170
+
171
+ try {
172
+ await upload({
173
+ release: options.release,
174
+ dir: options.dir,
175
+ apiKey: process.env.ORBITRACE_API_KEY,
176
+ orgId: options.org || process.env.ORBITRACE_ORG_ID,
177
+ projectId: options.project || process.env.ORBITRACE_PROJECT_ID,
178
+ endpoint: options.endpoint || process.env.ORBITRACE_ENDPOINT,
179
+ remove: options.delete === true,
180
+ });
181
+ } catch (error) {
182
+ console.error(`orbitrace-sourcemaps: ${error.message}`);
183
+ process.exitCode = 1;
184
+ }
185
+ }
186
+
187
+ if (require.main === module) {
188
+ main(process.argv.slice(2));
189
+ }
190
+
191
+ module.exports = { upload, parseArgs };