@devkitio/faultlens 1.0.1 → 1.1.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.
@@ -0,0 +1,31 @@
1
+ import { H as HttpErrorContext } from '../http-error-DTUBVanG.js';
2
+ import { E as ErrorMonitor } from '../types-hWS-GSV1.js';
3
+
4
+ interface AxiosLikeInterceptorManager {
5
+ use(onFulfilled?: (value: any) => any, onRejected?: (error: any) => any): number;
6
+ eject(id: number): void;
7
+ }
8
+ interface AxiosLikeClient {
9
+ interceptors: {
10
+ request: AxiosLikeInterceptorManager;
11
+ response: AxiosLikeInterceptorManager;
12
+ };
13
+ }
14
+ interface AxiosRequestConfigLike {
15
+ url?: string;
16
+ baseURL?: string;
17
+ method?: string;
18
+ headers?: unknown;
19
+ [key: string]: unknown;
20
+ }
21
+ interface AxiosHttpErrorContext extends HttpErrorContext {
22
+ error: unknown;
23
+ config?: AxiosRequestConfigLike;
24
+ }
25
+ interface AxiosInstrumentationOptions {
26
+ shouldPropagateTraceContext?: (url: URL) => boolean;
27
+ shouldCaptureHttpError?: (context: AxiosHttpErrorContext) => boolean;
28
+ }
29
+ declare function installAxiosInstrumentation(monitor: ErrorMonitor, client: AxiosLikeClient, options?: AxiosInstrumentationOptions): () => void;
30
+
31
+ export { type AxiosHttpErrorContext, type AxiosInstrumentationOptions, type AxiosLikeClient, type AxiosLikeInterceptorManager, type AxiosRequestConfigLike, installAxiosInstrumentation };
@@ -0,0 +1,123 @@
1
+ import { isHttpSuccessStatus, createHttpErrorContext, defaultShouldCaptureHttpError, captureHttpError } from '../chunk-GKS4FQKU.js';
2
+
3
+ // src/axios/index.ts
4
+ var installations = /* @__PURE__ */ new WeakMap();
5
+ function record(value) {
6
+ return typeof value === "object" && value !== null ? value : void 0;
7
+ }
8
+ function requestUrl(config) {
9
+ const raw = typeof config?.url === "string" ? config.url.trim() : "";
10
+ if (!raw) return void 0;
11
+ const configuredBase = typeof config?.baseURL === "string" ? config.baseURL.trim() : "";
12
+ const browserBase = typeof location !== "undefined" ? location.href : "";
13
+ try {
14
+ return new URL(raw, configuredBase || browserBase || "http://faultlens.invalid");
15
+ } catch {
16
+ return void 0;
17
+ }
18
+ }
19
+ function requestMethod(config) {
20
+ const method = typeof config?.method === "string" ? config.method : "GET";
21
+ return method.trim().toUpperCase().slice(0, 32) || "GET";
22
+ }
23
+ function responseStatus(error) {
24
+ const response = record(error?.response);
25
+ const status = Number(response?.status ?? 0);
26
+ return Number.isInteger(status) && status >= 100 && status <= 599 ? status : 0;
27
+ }
28
+ function defaultShouldPropagate(url) {
29
+ return typeof location !== "undefined" && url.origin === location.origin;
30
+ }
31
+ function setRequestHeader(config, name, value) {
32
+ const headers = record(config.headers);
33
+ const set = headers?.set;
34
+ if (typeof set === "function") {
35
+ set.call(config.headers, name, value);
36
+ return;
37
+ }
38
+ if (headers) headers[name] = value;
39
+ }
40
+ function installAxiosInstrumentation(monitor, client, options = {}) {
41
+ const existing = installations.get(client);
42
+ if (existing) {
43
+ existing.references += 1;
44
+ return releaseInstallation(client, existing);
45
+ }
46
+ const requestStates = /* @__PURE__ */ new WeakMap();
47
+ const requestInterceptorId = client.interceptors.request.use((config) => {
48
+ const configRecord = record(config);
49
+ const method = requestMethod(configRecord);
50
+ const url = requestUrl(configRecord);
51
+ const span = monitor.startSpan(`HTTP ${method}`, {
52
+ attributes: {
53
+ "http.method": method,
54
+ ...url ? { "url.path": url.pathname.slice(0, 2048) || "/" } : {}
55
+ }
56
+ });
57
+ requestStates.set(config, { method, span, ...url ? { url } : {} });
58
+ if (url && (options.shouldPropagateTraceContext ?? defaultShouldPropagate)(url)) {
59
+ for (const [name, value] of Object.entries(span.toTraceHeaders())) {
60
+ setRequestHeader(config, name, value);
61
+ }
62
+ }
63
+ return config;
64
+ });
65
+ const responseInterceptorId = client.interceptors.response.use(
66
+ (response) => {
67
+ const config = record(response)?.config;
68
+ const configObject = typeof config === "object" && config !== null ? config : void 0;
69
+ const state = configObject ? requestStates.get(configObject) : void 0;
70
+ if (state && configObject) {
71
+ const status = Number(record(response)?.status ?? 0);
72
+ if (Number.isInteger(status) && status > 0) {
73
+ state.span.setAttribute("http.status_code", status);
74
+ }
75
+ state.span.end(isHttpSuccessStatus(status) ? "ok" : "error");
76
+ requestStates.delete(configObject);
77
+ }
78
+ return response;
79
+ },
80
+ (error) => {
81
+ const errorRecord = record(error);
82
+ const configRecord = record(errorRecord?.config);
83
+ const config = configRecord;
84
+ const status = responseStatus(errorRecord);
85
+ const state = config ? requestStates.get(config) : void 0;
86
+ if (state && config) {
87
+ if (status > 0) state.span.setAttribute("http.status_code", status);
88
+ state.span.end(isHttpSuccessStatus(status) ? "ok" : "error");
89
+ requestStates.delete(config);
90
+ }
91
+ const context = createHttpErrorContext(
92
+ status > 0 ? "response" : "network",
93
+ state?.method ?? requestMethod(configRecord),
94
+ state?.url ?? requestUrl(configRecord),
95
+ status
96
+ );
97
+ const shouldCapture = options.shouldCaptureHttpError ? (httpContext) => options.shouldCaptureHttpError?.({
98
+ ...httpContext,
99
+ error,
100
+ ...config ? { config } : {}
101
+ }) === true : defaultShouldCaptureHttpError;
102
+ captureHttpError(monitor, "http.axios", error, context, shouldCapture);
103
+ return Promise.reject(error);
104
+ }
105
+ );
106
+ const installation = { requestInterceptorId, responseInterceptorId, references: 1 };
107
+ installations.set(client, installation);
108
+ return releaseInstallation(client, installation);
109
+ }
110
+ function releaseInstallation(client, installation) {
111
+ let released = false;
112
+ return () => {
113
+ if (released) return;
114
+ released = true;
115
+ installation.references -= 1;
116
+ if (installation.references > 0) return;
117
+ client.interceptors.request.eject(installation.requestInterceptorId);
118
+ client.interceptors.response.eject(installation.responseInterceptorId);
119
+ if (installations.get(client) === installation) installations.delete(client);
120
+ };
121
+ }
122
+
123
+ export { installAxiosInstrumentation };
@@ -0,0 +1,51 @@
1
+ // src/http-error.ts
2
+ var httpInstrumentationErrors = /* @__PURE__ */ new WeakSet();
3
+ function isHttpSuccessStatus(status) {
4
+ return status >= 200 && status < 400;
5
+ }
6
+ function isHttpInstrumentationError(error) {
7
+ return typeof error === "object" && error !== null && httpInstrumentationErrors.has(error);
8
+ }
9
+ function defaultShouldCaptureHttpError(context) {
10
+ return context.kind === "network" || context.status >= 500;
11
+ }
12
+ function createHttpErrorContext(kind, method, url, status) {
13
+ return {
14
+ kind,
15
+ method,
16
+ status,
17
+ ...url ? { path: url.pathname.slice(0, 2048) || "/" } : {}
18
+ };
19
+ }
20
+ function captureHttpError(monitor, mechanism, error, context, shouldCapture = defaultShouldCaptureHttpError) {
21
+ let accepted = false;
22
+ try {
23
+ accepted = shouldCapture(context);
24
+ } catch {
25
+ return;
26
+ }
27
+ if (!accepted) return;
28
+ const normalizedError = error instanceof Error ? error : new Error(
29
+ context.kind === "response" ? `HTTP \u8BF7\u6C42\u8FD4\u56DE ${context.status}` : "HTTP \u8BF7\u6C42\u53D1\u751F\u7F51\u7EDC\u9519\u8BEF"
30
+ );
31
+ httpInstrumentationErrors.add(normalizedError);
32
+ try {
33
+ monitor.captureException(normalizedError, {
34
+ handled: true,
35
+ mechanism,
36
+ tags: {
37
+ method: context.method,
38
+ status: String(context.status)
39
+ },
40
+ context: {
41
+ request: {
42
+ kind: context.kind,
43
+ ...context.path ? { path: context.path } : {}
44
+ }
45
+ }
46
+ });
47
+ } catch {
48
+ }
49
+ }
50
+
51
+ export { captureHttpError, createHttpErrorContext, defaultShouldCaptureHttpError, isHttpInstrumentationError, isHttpSuccessStatus };
@@ -1,4 +1,5 @@
1
- import { createMonitorRuntime } from './chunk-YQKZEUXA.js';
1
+ import { createMonitorRuntime } from './chunk-VTX5KF2X.js';
2
+ import { isHttpSuccessStatus, captureHttpError, createHttpErrorContext } from './chunk-GKS4FQKU.js';
2
3
 
3
4
  // src/browser-queue.ts
4
5
  var DATABASE_NAME = "faultlens";
@@ -170,6 +171,8 @@ function installGlobalHandlers(monitor, transportPath) {
170
171
  }
171
172
 
172
173
  // src/instrumentation.ts
174
+ var fetchInstallations = /* @__PURE__ */ new WeakMap();
175
+ var xhrInstallations = /* @__PURE__ */ new WeakMap();
173
176
  function requestUrl(input) {
174
177
  const raw = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
175
178
  try {
@@ -188,6 +191,11 @@ function defaultShouldPropagate(url) {
188
191
  }
189
192
  function installFetchInstrumentation(monitor, options = {}) {
190
193
  const target = options.target ?? globalThis;
194
+ const existing = fetchInstallations.get(target);
195
+ if (existing) {
196
+ existing.references += 1;
197
+ return releaseFetchInstallation(target, existing);
198
+ }
191
199
  const originalFetch = target.fetch;
192
200
  if (typeof originalFetch !== "function") return () => void 0;
193
201
  const instrumentedFetch = async function instrumentedFetch2(input, init) {
@@ -212,16 +220,41 @@ function installFetchInstrumentation(monitor, options = {}) {
212
220
  try {
213
221
  const response = await originalFetch.call(target, input, { ...init, headers });
214
222
  span.setAttribute("http.status_code", response.status);
215
- span.end(response.status >= 400 ? "error" : "ok");
223
+ span.end(isHttpSuccessStatus(response.status) ? "ok" : "error");
224
+ captureHttpError(
225
+ monitor,
226
+ "http.fetch",
227
+ void 0,
228
+ createHttpErrorContext("response", method, url, response.status),
229
+ options.shouldCaptureHttpError
230
+ );
216
231
  return response;
217
232
  } catch (error) {
218
233
  span.end("error");
234
+ captureHttpError(
235
+ monitor,
236
+ "http.fetch",
237
+ error,
238
+ createHttpErrorContext("network", method, url, 0),
239
+ options.shouldCaptureHttpError
240
+ );
219
241
  throw error;
220
242
  }
221
243
  };
222
244
  target.fetch = instrumentedFetch;
245
+ const installation = { instrumentedFetch, originalFetch, references: 1 };
246
+ fetchInstallations.set(target, installation);
247
+ return releaseFetchInstallation(target, installation);
248
+ }
249
+ function releaseFetchInstallation(target, installation) {
250
+ let released = false;
223
251
  return () => {
224
- if (target.fetch === instrumentedFetch) target.fetch = originalFetch;
252
+ if (released) return;
253
+ released = true;
254
+ installation.references -= 1;
255
+ if (installation.references > 0) return;
256
+ if (target.fetch === installation.instrumentedFetch) target.fetch = installation.originalFetch;
257
+ if (fetchInstallations.get(target) === installation) fetchInstallations.delete(target);
225
258
  };
226
259
  }
227
260
  function installXhrInstrumentation(monitor, options = {}) {
@@ -229,6 +262,11 @@ function installXhrInstrumentation(monitor, options = {}) {
229
262
  const target = options.target ?? (defaultTarget.XMLHttpRequest ? defaultTarget : void 0);
230
263
  if (!target?.XMLHttpRequest) return () => void 0;
231
264
  const prototype = target.XMLHttpRequest.prototype;
265
+ const existing = xhrInstallations.get(prototype);
266
+ if (existing) {
267
+ existing.references += 1;
268
+ return releaseXhrInstallation(prototype, existing);
269
+ }
232
270
  const originalOpen = prototype.open;
233
271
  const originalSend = prototype.send;
234
272
  const originalSetRequestHeader = prototype.setRequestHeader;
@@ -257,27 +295,53 @@ function installXhrInstrumentation(monitor, options = {}) {
257
295
  }
258
296
  }
259
297
  let finished = false;
260
- const finish = () => {
298
+ const finish = (kind, error) => {
261
299
  if (finished) return;
262
300
  finished = true;
263
301
  if (this.status > 0) span.setAttribute("http.status_code", this.status);
264
- span.end(this.status === 0 || this.status >= 400 ? "error" : "ok");
302
+ span.end(isHttpSuccessStatus(this.status) ? "ok" : "error");
303
+ captureHttpError(
304
+ monitor,
305
+ "http.xhr",
306
+ error,
307
+ createHttpErrorContext(kind, state.method, state.url, this.status),
308
+ options.shouldCaptureHttpError
309
+ );
265
310
  };
266
- this.addEventListener("loadend", finish, { once: true });
267
- this.addEventListener("error", finish, { once: true });
268
- this.addEventListener("abort", finish, { once: true });
311
+ this.addEventListener("loadend", () => finish(this.status === 0 ? "network" : "response"), { once: true });
312
+ this.addEventListener("error", () => finish("network"), { once: true });
313
+ this.addEventListener("abort", () => finish("network"), { once: true });
269
314
  try {
270
315
  return originalSend.apply(this, args);
271
316
  } catch (error) {
272
- finish();
317
+ finish("network", error);
273
318
  throw error;
274
319
  }
275
320
  };
276
- prototype.open = instrumentedOpen;
277
- prototype.send = instrumentedSend;
321
+ const installedOpen = instrumentedOpen;
322
+ const installedSend = instrumentedSend;
323
+ prototype.open = installedOpen;
324
+ prototype.send = installedSend;
325
+ const installation = {
326
+ instrumentedOpen: installedOpen,
327
+ instrumentedSend: installedSend,
328
+ originalOpen,
329
+ originalSend,
330
+ references: 1
331
+ };
332
+ xhrInstallations.set(prototype, installation);
333
+ return releaseXhrInstallation(prototype, installation);
334
+ }
335
+ function releaseXhrInstallation(prototype, installation) {
336
+ let released = false;
278
337
  return () => {
279
- if (prototype.open === instrumentedOpen) prototype.open = originalOpen;
280
- if (prototype.send === instrumentedSend) prototype.send = originalSend;
338
+ if (released) return;
339
+ released = true;
340
+ installation.references -= 1;
341
+ if (installation.references > 0) return;
342
+ if (prototype.open === installation.instrumentedOpen) prototype.open = installation.originalOpen;
343
+ if (prototype.send === installation.instrumentedSend) prototype.send = installation.originalSend;
344
+ if (xhrInstallations.get(prototype) === installation) xhrInstallations.delete(prototype);
281
345
  };
282
346
  }
283
347
 
@@ -1,3 +1,5 @@
1
+ import { isHttpInstrumentationError } from './chunk-GKS4FQKU.js';
2
+
1
3
  // ../security/dist/redaction.js
2
4
  var REDACTED = "[\u5DF2\u8131\u654F]";
3
5
  var MAX_STRING_LENGTH = 32768;
@@ -411,7 +413,7 @@ function traceContextHeaders(context) {
411
413
  }
412
414
 
413
415
  // src/monitor.ts
414
- var SDK_VERSION = "1.0.1";
416
+ var SDK_VERSION = "1.1.0";
415
417
  var PROTOCOL_VERSION = "1.0";
416
418
  var REVOKED_CREDENTIAL_CODES = /* @__PURE__ */ new Set(["invalid_dsn", "relay_key_revoked", "credential_revoked"]);
417
419
  function eventId() {
@@ -934,14 +936,16 @@ function createMonitorRuntime(options, overrides = {}) {
934
936
  }
935
937
  seenErrors.add(error);
936
938
  }
937
- const fingerprint = shallowFingerprint(event);
938
- const seenAt = recentFingerprints.get(fingerprint);
939
- const now = dependencies.now().getTime();
940
- if (seenAt !== void 0 && now - seenAt < 2e3) {
941
- drop("duplicate");
942
- return true;
939
+ if (!isHttpInstrumentationError(error)) {
940
+ const fingerprint = shallowFingerprint(event);
941
+ const seenAt = recentFingerprints.get(fingerprint);
942
+ const now = dependencies.now().getTime();
943
+ if (seenAt !== void 0 && now - seenAt < 2e3) {
944
+ drop("duplicate");
945
+ return true;
946
+ }
947
+ recentFingerprints.set(fingerprint, now);
943
948
  }
944
- recentFingerprints.set(fingerprint, now);
945
949
  if (dependencies.random() >= sampleRate) {
946
950
  drop("sampled");
947
951
  return true;
package/dist/cli/index.js CHANGED
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { realpathSync } from 'fs';
3
- import { lstat, readdir, readFile, writeFile, unlink } from 'fs/promises';
3
+ import { lstat, writeFile, unlink, readdir, readFile } from 'fs/promises';
4
4
  import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { randomUUID, createHash } from 'crypto';
7
+ import { promisify } from 'util';
8
+ import { gzip, brotliCompress } from 'zlib';
7
9
 
10
+ var gzipAsync = promisify(gzip);
11
+ var brotliCompressAsync = promisify(brotliCompress);
8
12
  var DEBUG_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
9
13
  var SOURCE_MAP_REFERENCE_PATTERN = /(?:\/\*[#@]\s*sourceMappingURL=.*?\*\/|\/\/[#@]\s*sourceMappingURL=.*?$)/gm;
10
14
  var DEBUG_ID_COMMENT_PATTERN = /^\/\/#\s*debugId=[0-9a-f-]{36}\s*$/gim;
@@ -73,13 +77,16 @@ async function prepareOne(root, absolutePath, dryRun) {
73
77
  const preparedBundle = `${cleanedBundle}
74
78
  ${runtimeInjection(debugId)}
75
79
  `;
80
+ const bundleBody = Buffer.from(preparedBundle, "utf8");
76
81
  if (!dryRun) {
77
82
  await writeFile(absolutePath, body, { mode: 384 });
78
- await writeFile(bundlePath, preparedBundle, { mode: 384 });
83
+ await writeFile(bundlePath, bundleBody, { mode: 384 });
79
84
  }
80
85
  return {
81
86
  absolutePath,
82
87
  relativePath,
88
+ bundleAbsolutePath: bundlePath,
89
+ bundleBody,
83
90
  sha256: createHash("sha256").update(body).digest("hex"),
84
91
  size: body.byteLength,
85
92
  debugId,
@@ -92,6 +99,37 @@ async function prepareSourceMapArtifacts(root, dryRun) {
92
99
  if (!rootStats.isDirectory() || rootStats.isSymbolicLink()) throw new Error("--root \u5FC5\u987B\u662F\u771F\u5B9E\u76EE\u5F55");
93
100
  return Promise.all((await sourceMapFiles(resolvedRoot)).map((file) => prepareOne(resolvedRoot, file, dryRun)));
94
101
  }
102
+ async function compressedSiblingExists(absolutePath) {
103
+ try {
104
+ const stats = await lstat(absolutePath);
105
+ if (!stats.isFile() || stats.isSymbolicLink()) {
106
+ throw new Error(`\u538B\u7F29\u8FD0\u884C\u5236\u54C1\u5FC5\u987B\u662F\u771F\u5B9E\u6587\u4EF6\uFF1A${absolutePath}`);
107
+ }
108
+ return true;
109
+ } catch (error) {
110
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
111
+ return false;
112
+ }
113
+ throw error;
114
+ }
115
+ }
116
+ async function synchronizeCompressedBundles(artifacts, dryRun) {
117
+ const synchronizedBundles = /* @__PURE__ */ new Set();
118
+ for (const artifact of artifacts) {
119
+ if (synchronizedBundles.has(artifact.bundleAbsolutePath)) continue;
120
+ synchronizedBundles.add(artifact.bundleAbsolutePath);
121
+ const gzipPath = `${artifact.bundleAbsolutePath}.gz`;
122
+ const brotliPath = `${artifact.bundleAbsolutePath}.br`;
123
+ if (await compressedSiblingExists(gzipPath)) {
124
+ const compressed = await gzipAsync(artifact.bundleBody);
125
+ if (!dryRun) await writeFile(gzipPath, compressed, { mode: 384 });
126
+ }
127
+ if (await compressedSiblingExists(brotliPath)) {
128
+ const compressed = await brotliCompressAsync(artifact.bundleBody);
129
+ if (!dryRun) await writeFile(brotliPath, compressed, { mode: 384 });
130
+ }
131
+ }
132
+ }
95
133
  async function removeUploadedSourceMaps(artifacts) {
96
134
  for (const artifact of artifacts) await unlink(artifact.absolutePath);
97
135
  }
@@ -99,7 +137,10 @@ async function removeUploadedSourceMaps(artifacts) {
99
137
  // src/cli/index.ts
100
138
  function argument(args, name) {
101
139
  const index = args.indexOf(name);
102
- return index >= 0 ? args[index + 1] : void 0;
140
+ if (index < 0) return void 0;
141
+ const value = args[index + 1];
142
+ if (!value || value.startsWith("--")) throw new Error(`\u7F3A\u5C11 ${name} \u7684\u53C2\u6570\u503C`);
143
+ return value;
103
144
  }
104
145
  function required(value, label) {
105
146
  if (!value) throw new Error(`\u7F3A\u5C11 ${label}`);
@@ -171,7 +212,7 @@ async function upload(config, artifacts) {
171
212
  }
172
213
  console.info(`\u4E0A\u4F20\u5B8C\u6210\uFF1A${artifacts.length} \u4E2A Source Map \u6587\u4EF6`);
173
214
  }
174
- async function verify(config, artifacts) {
215
+ async function verify(config, artifacts, deleteAfterVerify = config.deleteAfterVerify) {
175
216
  const token = required(config.token, "--token \u6216 FAULTLENS_SOURCEMAP_TOKEN");
176
217
  const response = await fetch(`${config.endpoint.replace(/\/$/, "")}/v1/admin/source-maps/verify`, {
177
218
  method: "POST",
@@ -193,7 +234,7 @@ async function verify(config, artifacts) {
193
234
  redirect: "error"
194
235
  });
195
236
  if (!response.ok) throw new Error(`\u5236\u54C1\u6E05\u5355\u6821\u9A8C\u5931\u8D25\uFF0C\u72B6\u6001\u7801 ${response.status}`);
196
- if (config.deleteAfterVerify) await removeUploadedSourceMaps(artifacts);
237
+ if (deleteAfterVerify) await removeUploadedSourceMaps(artifacts);
197
238
  console.info("Source Map \u5236\u54C1\u6E05\u5355\u4E0E\u670D\u52A1\u7AEF\u4E00\u81F4");
198
239
  }
199
240
  async function doctor(config) {
@@ -208,13 +249,27 @@ async function runFaultLensCli(args, env = process.env) {
208
249
  const subcommand = args[1];
209
250
  const config = resolveCliOptions(args, env);
210
251
  if (command === "doctor") return doctor(config);
211
- if (command !== "sourcemaps" || !["upload", "verify"].includes(subcommand ?? "")) {
212
- throw new Error("\u7528\u6CD5\uFF1Afaultlens sourcemaps <upload|verify> [\u53C2\u6570]\uFF0C\u6216 faultlens doctor [\u53C2\u6570]");
252
+ if (command !== "sourcemaps" || !["upload", "verify", "release"].includes(subcommand ?? "")) {
253
+ throw new Error("\u7528\u6CD5\uFF1Afaultlens sourcemaps <upload|verify|release> [\u53C2\u6570]\uFF0C\u6216 faultlens doctor [\u53C2\u6570]");
213
254
  }
255
+ if (!config.dryRun) required(config.token, "--token \u6216 FAULTLENS_SOURCEMAP_TOKEN");
214
256
  const artifacts = await prepareSourceMapArtifacts(config.root, config.dryRun);
215
257
  if (subcommand === "upload") return upload(config, artifacts);
216
258
  if (config.dryRun) {
217
- console.info(`\u6F14\u7EC3\u5B8C\u6210\uFF1A\u5C06\u6821\u9A8C ${artifacts.length} \u4E2A Source Map \u6587\u4EF6`);
259
+ if (subcommand === "release") {
260
+ await synchronizeCompressedBundles(artifacts, true);
261
+ console.info(`\u6F14\u7EC3\u5B8C\u6210\uFF1A\u5C06\u4E0A\u4F20\u3001\u6821\u9A8C\u5E76\u5220\u9664 ${artifacts.length} \u4E2A Source Map \u6587\u4EF6`);
262
+ } else {
263
+ console.info(`\u6F14\u7EC3\u5B8C\u6210\uFF1A\u5C06\u6821\u9A8C ${artifacts.length} \u4E2A Source Map \u6587\u4EF6`);
264
+ }
265
+ return;
266
+ }
267
+ if (subcommand === "release") {
268
+ await upload(config, artifacts);
269
+ await synchronizeCompressedBundles(artifacts, false);
270
+ await verify(config, artifacts, false);
271
+ await removeUploadedSourceMaps(artifacts);
272
+ console.info("Source Map \u53D1\u5E03\u5B8C\u6210\uFF0C\u672C\u5730\u6620\u5C04\u6587\u4EF6\u5DF2\u5220\u9664");
218
273
  return;
219
274
  }
220
275
  return verify(config, artifacts);
@@ -0,0 +1,10 @@
1
+ interface HttpErrorContext {
2
+ kind: "response" | "network";
3
+ method: string;
4
+ status: number;
5
+ path?: string;
6
+ }
7
+ type ShouldCaptureHttpError = (context: HttpErrorContext) => boolean;
8
+ declare function defaultShouldCaptureHttpError(context: HttpErrorContext): boolean;
9
+
10
+ export { type HttpErrorContext as H, type ShouldCaptureHttpError as S, defaultShouldCaptureHttpError as d };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { T as TelemetryTransport, I as IngestConfig, a as TelemetryEnvelope, b as TelemetryTransportOutcome, E as ErrorMonitor, M as MonitorOptions } from './types-hWS-GSV1.js';
2
2
  export { B as Breadcrumb, C as CaptureOptions, c as ContextMode, d as MonitorStatus, S as Span, e as SpanOptions, f as TelemetryOptions, g as TraceContext, W as WebVitalInput, h as formatTraceParent, p as parseTraceParent, t as traceContextHeaders } from './types-hWS-GSV1.js';
3
+ import { S as ShouldCaptureHttpError } from './http-error-DTUBVanG.js';
4
+ export { H as HttpErrorContext, d as defaultShouldCaptureHttpError } from './http-error-DTUBVanG.js';
3
5
 
4
6
  declare class FetchTelemetryTransport implements TelemetryTransport {
5
7
  private readonly ingest;
@@ -16,6 +18,7 @@ interface FetchInstrumentationOptions {
16
18
  target?: FetchInstrumentationTarget;
17
19
  shouldTrace?: (input: RequestInfo | URL, init?: RequestInit) => boolean;
18
20
  shouldPropagateTraceContext?: (url: URL) => boolean;
21
+ shouldCaptureHttpError?: ShouldCaptureHttpError;
19
22
  }
20
23
  interface XhrInstrumentationTarget {
21
24
  XMLHttpRequest: typeof XMLHttpRequest;
@@ -24,10 +27,11 @@ interface XhrInstrumentationOptions {
24
27
  target?: XhrInstrumentationTarget;
25
28
  shouldTrace?: (method: string, url: URL | undefined) => boolean;
26
29
  shouldPropagateTraceContext?: (url: URL) => boolean;
30
+ shouldCaptureHttpError?: ShouldCaptureHttpError;
27
31
  }
28
32
  declare function installFetchInstrumentation(monitor: ErrorMonitor, options?: FetchInstrumentationOptions): () => void;
29
33
  declare function installXhrInstrumentation(monitor: ErrorMonitor, options?: XhrInstrumentationOptions): () => void;
30
34
 
31
35
  declare function createErrorMonitor(options: MonitorOptions): ErrorMonitor;
32
36
 
33
- export { ErrorMonitor, type FetchInstrumentationOptions, type FetchInstrumentationTarget, FetchTelemetryTransport, IngestConfig, MonitorOptions, type XhrInstrumentationOptions, type XhrInstrumentationTarget, createErrorMonitor, installFetchInstrumentation, installXhrInstrumentation };
37
+ export { ErrorMonitor, type FetchInstrumentationOptions, type FetchInstrumentationTarget, FetchTelemetryTransport, IngestConfig, MonitorOptions, ShouldCaptureHttpError, type XhrInstrumentationOptions, type XhrInstrumentationTarget, createErrorMonitor, installFetchInstrumentation, installXhrInstrumentation };
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
- export { createErrorMonitor, installFetchInstrumentation, installXhrInstrumentation } from './chunk-DQD732MN.js';
2
- export { FetchTelemetryTransport, formatTraceParent, parseTraceParent, traceContextHeaders } from './chunk-YQKZEUXA.js';
1
+ export { createErrorMonitor, installFetchInstrumentation, installXhrInstrumentation } from './chunk-RFH3EN3N.js';
2
+ export { FetchTelemetryTransport, formatTraceParent, parseTraceParent, traceContextHeaders } from './chunk-VTX5KF2X.js';
3
+ export { defaultShouldCaptureHttpError } from './chunk-GKS4FQKU.js';
@@ -1,4 +1,6 @@
1
1
  import { P as PersistentEventQueue, i as PersistedQueueItem, E as ErrorMonitor, M as MonitorOptions, T as TelemetryTransport, a as TelemetryEnvelope, b as TelemetryTransportOutcome, j as EventTransport, k as EventEnvelope, l as TransportOutcome, R as RemoteSdkConfig, m as RemoteConfigLoadResult, n as RuntimeDependencies } from '../types-hWS-GSV1.js';
2
+ import { S as ShouldCaptureHttpError } from '../http-error-DTUBVanG.js';
3
+ export { H as HttpErrorContext, d as defaultShouldCaptureHttpError } from '../http-error-DTUBVanG.js';
2
4
  import { ClientRequest } from 'node:http';
3
5
 
4
6
  interface NodeQueueEncryptionKey {
@@ -45,6 +47,7 @@ interface NodeHttpInstrumentationOptions {
45
47
  modules?: NodeHttpInstrumentationModule[];
46
48
  shouldTrace?: (method: string, url: URL | undefined) => boolean;
47
49
  shouldPropagateTraceContext?: (url: URL | undefined) => boolean;
50
+ shouldCaptureHttpError?: ShouldCaptureHttpError;
48
51
  }
49
52
  declare function installNodeHttpInstrumentation(monitor: ErrorMonitor, options?: NodeHttpInstrumentationOptions): () => void;
50
53
 
@@ -90,4 +93,4 @@ declare function createNodeErrorMonitor(options: NodeMonitorOptions, overrides?:
90
93
  transport?: EventTransport;
91
94
  } & Partial<RuntimeDependencies>): ErrorMonitor;
92
95
 
93
- export { NodeEncryptedFileQueue, type NodeEncryptedFileQueueOptions, type NodeHttpInstrumentationModule, type NodeHttpInstrumentationOptions, type NodeMonitorOptions, type NodeQueueEncryptionKey, type NodeRelayIngestConfig, NodeRelayTelemetryTransport, NodeRelayTransport, type NodeRelayTransportOptions, createNodeErrorMonitor, installNodeHttpInstrumentation };
96
+ export { NodeEncryptedFileQueue, type NodeEncryptedFileQueueOptions, type NodeHttpInstrumentationModule, type NodeHttpInstrumentationOptions, type NodeMonitorOptions, type NodeQueueEncryptionKey, type NodeRelayIngestConfig, NodeRelayTelemetryTransport, NodeRelayTransport, type NodeRelayTransportOptions, ShouldCaptureHttpError, createNodeErrorMonitor, installNodeHttpInstrumentation };
@@ -1,4 +1,6 @@
1
- import { parseTransportResponse, parseRemoteSdkConfig, parseTelemetryTransportResponse, createMonitorRuntime } from '../chunk-YQKZEUXA.js';
1
+ import { parseTransportResponse, parseRemoteSdkConfig, parseTelemetryTransportResponse, createMonitorRuntime } from '../chunk-VTX5KF2X.js';
2
+ import { captureHttpError, createHttpErrorContext, isHttpSuccessStatus } from '../chunk-GKS4FQKU.js';
3
+ export { defaultShouldCaptureHttpError } from '../chunk-GKS4FQKU.js';
2
4
  import { createSignedRelayHeaders } from '../chunk-RXQF7F2E.js';
3
5
  import { AsyncLocalStorage } from 'async_hooks';
4
6
  import { createDecipheriv, randomBytes, createCipheriv } from 'crypto';
@@ -216,6 +218,7 @@ var NodeEncryptedFileQueue = class {
216
218
  }
217
219
  }
218
220
  };
221
+ var nodeHttpInstallations = /* @__PURE__ */ new WeakMap();
219
222
  function builtInModules() {
220
223
  const require2 = createRequire(import.meta.url);
221
224
  return [
@@ -267,6 +270,13 @@ function instrumentedArguments(args, headers) {
267
270
  function installNodeHttpInstrumentation(monitor, options = {}) {
268
271
  const restorers = [];
269
272
  for (const module of options.modules ?? builtInModules()) {
273
+ const existing = nodeHttpInstallations.get(module);
274
+ if (existing) {
275
+ existing.references += 1;
276
+ restorers.push(releaseNodeHttpInstallation(module, existing));
277
+ continue;
278
+ }
279
+ const moduleRestorers = [];
270
280
  for (const key of ["request", "get"]) {
271
281
  const original = module[key];
272
282
  if (typeof original !== "function") continue;
@@ -292,35 +302,82 @@ function installNodeHttpInstrumentation(monitor, options = {}) {
292
302
  request = original.apply(module, instrumentedArguments(args, headers));
293
303
  } catch (error) {
294
304
  span.end("error");
305
+ captureHttpError(
306
+ monitor,
307
+ "http.node",
308
+ error,
309
+ createHttpErrorContext("network", method, url, 0),
310
+ options.shouldCaptureHttpError
311
+ );
295
312
  throw error;
296
313
  }
297
- let finished = false;
298
- const finish = (status, response) => {
299
- if (finished) return;
300
- finished = true;
314
+ let spanFinished = false;
315
+ const finishSpan = (status, response) => {
316
+ if (spanFinished) return false;
317
+ spanFinished = true;
301
318
  if (response?.statusCode) span.setAttribute("http.status_code", response.statusCode);
302
319
  span.end(status);
320
+ return true;
321
+ };
322
+ let networkCaptured = false;
323
+ const captureNetwork = (error, response) => {
324
+ if (networkCaptured) return;
325
+ networkCaptured = true;
326
+ finishSpan("error", response);
327
+ captureHttpError(
328
+ monitor,
329
+ "http.node",
330
+ error,
331
+ createHttpErrorContext("network", method, url, response?.statusCode ?? 0),
332
+ options.shouldCaptureHttpError
333
+ );
303
334
  };
304
335
  request.once("response", (response) => {
305
336
  const status = response.statusCode ?? 0;
306
- response.once("end", () => finish(status >= 400 ? "error" : "ok", response));
307
- response.once("aborted", () => finish("error", response));
308
- response.once("error", () => finish("error", response));
337
+ finishSpan(isHttpSuccessStatus(status) ? "ok" : "error", response);
338
+ captureHttpError(
339
+ monitor,
340
+ "http.node",
341
+ void 0,
342
+ createHttpErrorContext("response", method, url, status),
343
+ options.shouldCaptureHttpError
344
+ );
345
+ response.once("aborted", () => captureNetwork(void 0, response));
346
+ response.once("error", (error) => captureNetwork(error, response));
309
347
  });
310
- request.once("error", () => finish("error"));
311
- request.once("abort", () => finish("error"));
348
+ request.once("error", (error) => captureNetwork(error));
349
+ request.once("abort", () => captureNetwork());
312
350
  return request;
313
351
  };
314
352
  module[key] = instrumented;
315
- restorers.push(() => {
353
+ moduleRestorers.push(() => {
316
354
  if (module[key] === instrumented) module[key] = original;
317
355
  });
318
356
  }
357
+ const installation = {
358
+ references: 1,
359
+ restore() {
360
+ for (const restore of moduleRestorers.reverse()) restore();
361
+ }
362
+ };
363
+ nodeHttpInstallations.set(module, installation);
364
+ restorers.push(releaseNodeHttpInstallation(module, installation));
319
365
  }
320
366
  return () => {
321
367
  for (const restore of restorers.reverse()) restore();
322
368
  };
323
369
  }
370
+ function releaseNodeHttpInstallation(module, installation) {
371
+ let released = false;
372
+ return () => {
373
+ if (released) return;
374
+ released = true;
375
+ installation.references -= 1;
376
+ if (installation.references > 0) return;
377
+ installation.restore();
378
+ if (nodeHttpInstallations.get(module) === installation) nodeHttpInstallations.delete(module);
379
+ };
380
+ }
324
381
 
325
382
  // src/node/index.ts
326
383
  function delay(timeoutMs) {
@@ -1,4 +1,5 @@
1
- import { clientRedact, SDK_VERSION, parseErrorStack } from '../../chunk-YQKZEUXA.js';
1
+ import { clientRedact, SDK_VERSION, parseErrorStack } from '../../chunk-VTX5KF2X.js';
2
+ import '../../chunk-GKS4FQKU.js';
2
3
  import { createSignedRelayHeaders } from '../../chunk-RXQF7F2E.js';
3
4
  import { randomUUID } from 'crypto';
4
5
 
@@ -1,6 +1,7 @@
1
- import { createErrorMonitor } from '../../chunk-DQD732MN.js';
1
+ import { createErrorMonitor } from '../../chunk-RFH3EN3N.js';
2
2
  import { installVueErrorMonitor } from '../../chunk-P6Q63P6L.js';
3
- import '../../chunk-YQKZEUXA.js';
3
+ import '../../chunk-VTX5KF2X.js';
4
+ import '../../chunk-GKS4FQKU.js';
4
5
  import { defineNuxtPlugin, useRuntimeConfig } from '#app';
5
6
 
6
7
  var plugin_default = defineNuxtPlugin({
@@ -1,5 +1,6 @@
1
- import { createErrorMonitor } from '../chunk-DQD732MN.js';
2
- import '../chunk-YQKZEUXA.js';
1
+ import { createErrorMonitor } from '../chunk-RFH3EN3N.js';
2
+ import '../chunk-VTX5KF2X.js';
3
+ import '../chunk-GKS4FQKU.js';
3
4
  import { Component, createElement } from 'react';
4
5
 
5
6
  function createReactErrorMonitor(options) {
@@ -1,4 +1,5 @@
1
- import { createMonitorRuntime } from '../chunk-YQKZEUXA.js';
1
+ import { createMonitorRuntime } from '../chunk-VTX5KF2X.js';
2
+ import '../chunk-GKS4FQKU.js';
2
3
 
3
4
  // src/testing/index.ts
4
5
  var TestTransport = class {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devkitio/faultlens",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "FaultLens 的 Browser、Node、Vue、Nuxt、React 与 Web 框架错误采集 SDK",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -28,6 +28,10 @@
28
28
  "types": "./dist/node/index.d.ts",
29
29
  "import": "./dist/node/index.js"
30
30
  },
31
+ "./axios": {
32
+ "types": "./dist/axios/index.d.ts",
33
+ "import": "./dist/axios/index.js"
34
+ },
31
35
  "./web-vitals": {
32
36
  "types": "./dist/web-vitals/index.d.ts",
33
37
  "import": "./dist/web-vitals/index.js"
@@ -59,9 +63,15 @@
59
63
  "registry": "https://registry.npmjs.org/",
60
64
  "access": "public"
61
65
  },
66
+ "scripts": {
67
+ "build": "tsup",
68
+ "check": "tsc -p tsconfig.json --noEmit",
69
+ "prepack": "pnpm build"
70
+ },
62
71
  "peerDependencies": {
63
72
  "@nuxt/kit": ">=4.1.0 <5",
64
73
  "@nuxt/schema": ">=4.1.0 <5",
74
+ "axios": ">=0.21.1 <2",
65
75
  "express": ">=4.18.0 <6",
66
76
  "fastify": ">=4.0.0 <6",
67
77
  "h3": ">=1.15.0 <2",
@@ -75,6 +85,9 @@
75
85
  "@nuxt/schema": {
76
86
  "optional": true
77
87
  },
88
+ "axios": {
89
+ "optional": true
90
+ },
78
91
  "express": {
79
92
  "optional": true
80
93
  },
@@ -92,20 +105,18 @@
92
105
  }
93
106
  },
94
107
  "devDependencies": {
108
+ "@faultlens/security": "workspace:*",
95
109
  "@nuxt/kit": "4.1.0",
96
110
  "@nuxt/schema": "4.1.0",
97
111
  "@types/react": "19.1.11",
112
+ "axios": "1.19.0",
113
+ "axios-legacy": "npm:axios@0.21.4",
98
114
  "h3": "1.15.9",
99
115
  "react": "19.1.1",
100
116
  "tsup": "8.5.0",
101
- "vue": "3.5.20",
102
- "@faultlens/security": "1.0.1"
117
+ "vue": "3.5.20"
103
118
  },
104
119
  "dependencies": {
105
120
  "web-vitals": "6.2.0"
106
- },
107
- "scripts": {
108
- "build": "tsup",
109
- "check": "tsc -p tsconfig.json --noEmit"
110
121
  }
111
- }
122
+ }