@orbitai/orbitrace 1.3.0 → 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 +13 -0
- package/bin/orbitrace-sourcemaps.js +191 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -168,6 +168,19 @@ app.use(Orbitrace.vue);
|
|
|
168
168
|
|
|
169
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
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
|
+
|
|
171
184
|
## Data Collected
|
|
172
185
|
|
|
173
186
|
Each event stores:
|
|
@@ -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 };
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbitai/orbitrace",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Error tracking for Node.js services and browser apps: manual and automatic capture with breadcrumbs",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"orbitrace-sourcemaps": "./bin/orbitrace-sourcemaps.js"
|
|
8
|
+
},
|
|
6
9
|
"exports": {
|
|
7
10
|
".": "./dist/index.js",
|
|
8
11
|
"./browser": {
|
|
@@ -13,7 +16,8 @@
|
|
|
13
16
|
"./package.json": "./package.json"
|
|
14
17
|
},
|
|
15
18
|
"files": [
|
|
16
|
-
"dist"
|
|
19
|
+
"dist",
|
|
20
|
+
"bin"
|
|
17
21
|
],
|
|
18
22
|
"scripts": {
|
|
19
23
|
"test": "node --test test/*.test.js",
|