@bigbertha/sdk 0.5.0 → 0.5.1

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
@@ -156,7 +156,26 @@ deep framework call doesn't hide your code. A limit you set yourself is left alo
156
156
  request: its URL, its headers (never `authorization`, `proxy-authorization`, `cookie`,
157
157
  `set-cookie` or `x-api-key`), the client IP from the first `x-forwarded-for` hop or the
158
158
  socket, and the trace from an incoming `traceparent`. Next.js's `onRequestError` sends
159
- the same. Server events also name the host (Heroku's `DYNO`, else the hostname) and the
159
+ the same.
160
+
161
+ An app with its own error middleware passes the framework's request as `req` and gets
162
+ the same request, IP and trace:
163
+
164
+ ```ts
165
+ // Koa
166
+ app.on("error", (err, ctx) =>
167
+ captureException(err, { req: ctx, status: err.status ?? 500, mechanism: { handler: "koa", handled: false } }),
168
+ )
169
+ // Express, after the routes
170
+ app.use((err, req, res, next) => {
171
+ captureException(err, { req, status: 500, mechanism: { handler: "express", handled: false } })
172
+ next(err)
173
+ })
174
+ ```
175
+
176
+ `req` takes Node's `IncomingMessage`, Express's `req`, Koa's `ctx` or a Fetch `Request`
177
+ (the edge entry too). `mechanism` says the error escaped the app's own code, so bertha
178
+ doesn't read it as one the app caught. Server events also name the host (Heroku's `DYNO`, else the hostname) and the
160
179
  Node and Next.js versions.
161
180
 
162
181
  For bundled Node code, give the output debug ids and upload its maps after the build:
@@ -7,6 +7,8 @@ type Context = IngestEvent["context"];
7
7
  export type AddContext = (frames: IngestFrame[]) => void;
8
8
  /** Adds what the runtime knows at capture time (the page, its state). Runs before scrubbing. */
9
9
  export type Enrich = (event: IngestEvent) => void;
10
+ /** Reads a framework's request passed as `req`. Server entries only, to keep the browser small. */
11
+ export type ReadRequest = (req: unknown) => CaptureContext;
10
12
  export declare class Client {
11
13
  readonly options: Options;
12
14
  private readonly transport;
@@ -14,6 +16,7 @@ export declare class Client {
14
16
  private readonly context;
15
17
  private readonly addContext;
16
18
  private readonly enrich;
19
+ private readonly readRequest;
17
20
  private user;
18
21
  private tags;
19
22
  private crumbs;
@@ -24,7 +27,7 @@ export declare class Client {
24
27
  private window;
25
28
  private pausedUntil;
26
29
  private warned;
27
- constructor(options: Options, transport: Transport, sdk: IngestSdk, context?: Context, addContext?: AddContext, enrich?: Enrich);
30
+ constructor(options: Options, transport: Transport, sdk: IngestSdk, context?: Context, addContext?: AddContext, enrich?: Enrich, readRequest?: ReadRequest);
28
31
  captureException(error: unknown, ctx?: CaptureContext): string | undefined;
29
32
  captureMessage(message: string, ctx?: CaptureContext): string | undefined;
30
33
  setUser(user: IngestUser | null): void;
@@ -21,6 +21,7 @@ export class Client {
21
21
  context;
22
22
  addContext;
23
23
  enrich;
24
+ readRequest;
24
25
  user;
25
26
  tags;
26
27
  crumbs = [];
@@ -31,13 +32,14 @@ export class Client {
31
32
  window = { start: 0, count: 0 };
32
33
  pausedUntil = 0;
33
34
  warned = false;
34
- constructor(options, transport, sdk, context, addContext, enrich) {
35
+ constructor(options, transport, sdk, context, addContext, enrich, readRequest) {
35
36
  this.options = options;
36
37
  this.transport = transport;
37
38
  this.sdk = sdk;
38
39
  this.context = context;
39
40
  this.addContext = addContext;
40
41
  this.enrich = enrich;
42
+ this.readRequest = readRequest;
41
43
  this.tags = { ...options.tags };
42
44
  }
43
45
  captureException(error, ctx = {}) {
@@ -101,6 +103,9 @@ export class Client {
101
103
  if (++this.window.count > PER_MINUTE)
102
104
  return undefined;
103
105
  const id = uuid();
106
+ const fromReq = (ctx.req !== undefined && this.readRequest?.(ctx.req)) || {};
107
+ const request = ctx.request ?? fromReq.request;
108
+ const user = fromReq.user || ctx.user ? { ...fromReq.user, ...ctx.user } : undefined;
104
109
  let event = {
105
110
  ...base,
106
111
  id,
@@ -108,12 +113,12 @@ export class Client {
108
113
  release: o.release ?? builtRelease(),
109
114
  environment: o.environment,
110
115
  breadcrumbs: this.crumbs.slice(),
111
- user: ctx.user ? { ...this.user, ...ctx.user } : this.user,
116
+ user: user ? { ...this.user, ...user } : this.user,
112
117
  context: { ...this.context, versions: { ...versions(), ...o.versions, ...this.context?.versions } },
113
118
  tags: { ...this.tags, ...ctx.tags },
114
119
  fingerprint: ctx.fingerprint,
115
- request: ctx.request,
116
- trace: ctx.trace,
120
+ request: request && ctx.status !== undefined ? { ...request, status: ctx.status } : request,
121
+ trace: ctx.trace ?? fromReq.trace,
117
122
  };
118
123
  try {
119
124
  this.enrich?.(event);
@@ -4,3 +4,9 @@ import type { CaptureContext } from "./types.ts";
4
4
  * (the first proxy hop, else the socket) and the trace from an incoming `traceparent`.
5
5
  */
6
6
  export declare function fromRequest(method: string, url: string, headers: Record<string, unknown>, remoteAddress?: string): CaptureContext;
7
+ /**
8
+ * The same from a framework's own request: Node's `IncomingMessage`, Express's `req`,
9
+ * Koa's `ctx` or `ctx.req`, or a Fetch `Request` (edge, Hono's `c.req.raw`). Anything
10
+ * else gives nothing.
11
+ */
12
+ export declare function fromAnyRequest(req: unknown): CaptureContext;
@@ -29,3 +29,26 @@ export function fromRequest(method, url, headers, remoteAddress) {
29
29
  ctx.trace = trace;
30
30
  return ctx;
31
31
  }
32
+ /**
33
+ * The same from a framework's own request: Node's `IncomingMessage`, Express's `req`,
34
+ * Koa's `ctx` or `ctx.req`, or a Fetch `Request` (edge, Hono's `c.req.raw`). Anything
35
+ * else gives nothing.
36
+ */
37
+ export function fromAnyRequest(req) {
38
+ const r = req;
39
+ if (!r || typeof r !== "object" || typeof r.method !== "string" || !r.headers || typeof r.headers !== "object")
40
+ return {};
41
+ let headers;
42
+ if (typeof r.headers.forEach === "function") {
43
+ headers = {};
44
+ r.headers.forEach((v, k) => (headers[k] = v));
45
+ }
46
+ else
47
+ headers = r.headers;
48
+ const path = typeof r.originalUrl === "string" ? r.originalUrl : typeof r.url === "string" ? r.url : "/";
49
+ const first = (v) => (typeof v === "string" ? v.split(",")[0]?.trim() : undefined);
50
+ const host = first(headers.host) ?? first(headers.Host);
51
+ const proto = first(headers["x-forwarded-proto"]) ?? "http";
52
+ const url = host && path.startsWith("/") ? `${proto}://${host}${path}` : path;
53
+ return fromRequest(r.method, url, headers, r.socket?.remoteAddress);
54
+ }
@@ -27,6 +27,14 @@ export interface CaptureContext {
27
27
  tags?: Record<string, string>;
28
28
  fingerprint?: string[];
29
29
  request?: IngestEvent["request"];
30
+ /**
31
+ * The framework's request: Node's `IncomingMessage`, Express's `req`, Koa's `ctx`, or a
32
+ * Fetch `Request`. Its method, URL, headers (never credentials), the client IP and an
33
+ * incoming `traceparent` are sent. `request`, `user` and `trace` win over what it gives.
34
+ */
35
+ req?: unknown;
36
+ /** The response status, sent with the request. */
37
+ status?: number;
30
38
  /** Merged over the user from `setUser`. */
31
39
  user?: IngestUser;
32
40
  /** How the error reached the SDK. Defaults to `captureException`, handled. */
@@ -1 +1 @@
1
- export declare const VERSION = "0.5.0";
1
+ export declare const VERSION = "0.5.1";
@@ -1 +1 @@
1
- export const VERSION = "0.5.0";
1
+ export const VERSION = "0.5.1";
@@ -3,6 +3,7 @@
3
3
  import { Client } from "../core/client.js";
4
4
  import { firstEnv } from "../core/env.js";
5
5
  import { setClient } from "../core/hub.js";
6
+ import { fromAnyRequest } from "../core/request.js";
6
7
  import { fetchTransport, streamGzip } from "../core/transport.js";
7
8
  import { VERSION } from "../core/version.js";
8
9
  export { addBreadcrumb, captureException, captureMessage, flush, getClient, setTag, setUser } from "../core/hub.js";
@@ -19,7 +20,7 @@ export function init(options = {}) {
19
20
  url,
20
21
  release: options.release ?? firstEnv("BERTHA_RELEASE"),
21
22
  environment: options.environment ?? firstEnv("BERTHA_ENVIRONMENT", "VERCEL_ENV", "NODE_ENV"),
22
- }, fetchTransport({ url, key, gzip: streamGzip }), { name: "bertha.edge", version: VERSION }, { runtime: "Edge" });
23
+ }, fetchTransport({ url, key, gzip: streamGzip }), { name: "bertha.edge", version: VERSION }, { runtime: "Edge" }, undefined, undefined, fromAnyRequest);
23
24
  setClient(client);
24
25
  return client;
25
26
  }
@@ -7,7 +7,7 @@ import { gzipSync } from "node:zlib";
7
7
  import { Client } from "../core/client.js";
8
8
  import { envCommit, firstEnv } from "../core/env.js";
9
9
  import { getClient, setClient } from "../core/hub.js";
10
- import { fromRequest } from "../core/request.js";
10
+ import { fromAnyRequest } from "../core/request.js";
11
11
  import { endpoint, fetchTransport } from "../core/transport.js";
12
12
  import { VERSION } from "../core/version.js";
13
13
  import { addContext } from "./context.js";
@@ -39,7 +39,7 @@ export function init(options = {}) {
39
39
  os: `${platform()} ${arch()}`,
40
40
  host: process.env.DYNO ?? safe(hostname),
41
41
  versions: installedVersions(),
42
- }, options.contextLines === false ? undefined : addContext);
42
+ }, options.contextLines === false ? undefined : addContext, undefined, fromAnyRequest);
43
43
  setClient(client);
44
44
  // Only V8's default: a limit the app set itself was a choice.
45
45
  if (Error.stackTraceLimit === V8_DEFAULT_FRAMES)
@@ -106,7 +106,7 @@ export async function sendHeartbeat(o) {
106
106
  export function wrapHandler(handler) {
107
107
  return function (...args) {
108
108
  const report = (error) => {
109
- getClient()?.captureException(error, requestContext(args[0]));
109
+ getClient()?.captureException(error, { req: args[0], mechanism: { handler: "wrapHandler", handled: false } });
110
110
  throw error;
111
111
  };
112
112
  try {
@@ -118,16 +118,6 @@ export function wrapHandler(handler) {
118
118
  }
119
119
  };
120
120
  }
121
- function requestContext(req) {
122
- const r = req;
123
- if (!r || typeof r.method !== "string")
124
- return {};
125
- const path = r.originalUrl ?? r.url ?? "/";
126
- const host = typeof r.headers?.host === "string" ? r.headers.host : undefined;
127
- const proto = typeof r.headers?.["x-forwarded-proto"] === "string" ? r.headers["x-forwarded-proto"] : "http";
128
- const url = host && path.startsWith("/") ? `${proto}://${host}${path}` : path;
129
- return fromRequest(r.method, url, r.headers ?? {}, r.socket?.remoteAddress);
130
- }
131
121
  /** Node's version, and Next.js's when the app has it installed. */
132
122
  function installedVersions() {
133
123
  const out = { node: process.versions.node };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigbertha/sdk",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Error reporting for bertha: browser, React, Node, edge and Next.js, with source maps by debug id.",
5
5
  "license": "MIT",
6
6
  "repository": {