@bigbertha/sdk 0.2.0 → 0.2.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
@@ -97,6 +97,11 @@ init() // reads BERTHA_KEY, BERTHA_URL, BERTHA_RELEASE and NODE_ENV
97
97
  would (`handlers: false` turns that off). It also sends a heartbeat every 20 minutes so
98
98
  bertha can tell "no errors" from "not running" (`heartbeat: false`).
99
99
 
100
+ Code that Node runs as-is needs no source maps: the SDK reads the lines around each
101
+ frame in your own files from disk and sends them with the event (`contextLines: false`
102
+ turns that off). It also raises V8's default stack limit from 10 frames to 50, so a
103
+ deep framework call doesn't hide your code. A limit you set yourself is left alone.
104
+
100
105
  `wrapHandler(fn)` reports what a request handler throws and rethrows it.
101
106
 
102
107
  For bundled Node code, give the output debug ids and upload its maps after the build:
@@ -1,13 +1,16 @@
1
- import type { IngestException, IngestSdk } from "../generated/ingest.ts";
1
+ import type { IngestException, IngestFrame, IngestSdk } from "../generated/ingest.ts";
2
2
  import type { CaptureContext, IngestBreadcrumb, IngestEvent, IngestUser, Options, Transport } from "./types.ts";
3
3
  export declare const MAX_BREADCRUMBS = 100;
4
4
  export declare const BATCH = 100;
5
5
  type Context = IngestEvent["context"];
6
+ /** Adds source lines to frames, where the runtime can read the files. */
7
+ export type AddContext = (frames: IngestFrame[]) => void;
6
8
  export declare class Client {
7
9
  readonly options: Options;
8
10
  private readonly transport;
9
11
  private readonly sdk;
10
12
  private readonly context;
13
+ private readonly addContext;
11
14
  private user;
12
15
  private tags;
13
16
  private crumbs;
@@ -18,7 +21,7 @@ export declare class Client {
18
21
  private window;
19
22
  private pausedUntil;
20
23
  private warned;
21
- constructor(options: Options, transport: Transport, sdk: IngestSdk, context?: Context);
24
+ constructor(options: Options, transport: Transport, sdk: IngestSdk, context?: Context, addContext?: AddContext);
22
25
  captureException(error: unknown, ctx?: CaptureContext): string | undefined;
23
26
  captureMessage(message: string, ctx?: CaptureContext): string | undefined;
24
27
  setUser(user: IngestUser | null): void;
@@ -14,6 +14,7 @@ export class Client {
14
14
  transport;
15
15
  sdk;
16
16
  context;
17
+ addContext;
17
18
  user;
18
19
  tags;
19
20
  crumbs = [];
@@ -24,11 +25,12 @@ export class Client {
24
25
  window = { start: 0, count: 0 };
25
26
  pausedUntil = 0;
26
27
  warned = false;
27
- constructor(options, transport, sdk, context) {
28
+ constructor(options, transport, sdk, context, addContext) {
28
29
  this.options = options;
29
30
  this.transport = transport;
30
31
  this.sdk = sdk;
31
32
  this.context = context;
33
+ this.addContext = addContext;
32
34
  this.tags = { ...options.tags };
33
35
  }
34
36
  captureException(error, ctx = {}) {
@@ -38,7 +40,17 @@ export class Client {
38
40
  return undefined;
39
41
  this.seen.add(error);
40
42
  }
41
- return this.capture({ exception: toException(error, 0), level: ctx.level ?? "error" }, ctx);
43
+ const exception = toException(error, 0);
44
+ if (this.addContext) {
45
+ try {
46
+ for (let x = exception; x; x = x.cause)
47
+ this.addContext(x.frames);
48
+ }
49
+ catch (e) {
50
+ this.log("couldn't read source lines", e);
51
+ }
52
+ }
53
+ return this.capture({ exception, level: ctx.level ?? "error" }, ctx);
42
54
  }
43
55
  captureMessage(message, ctx = {}) {
44
56
  return this.capture({ message, level: ctx.level ?? "warning" }, ctx);
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.0";
1
+ export declare const VERSION = "0.2.1";
@@ -1 +1 @@
1
- export const VERSION = "0.2.0";
1
+ export const VERSION = "0.2.1";
@@ -108,6 +108,18 @@ export type IngestFrame = {
108
108
  * The SDK's guess; the server decides from the file path when absent.
109
109
  */
110
110
  inApp?: boolean;
111
+ /**
112
+ * Source lines around `line`, read by an SDK that can see the file (Node running
113
+ * its source as-is). Ignored when the frame maps through a source map.
114
+ */
115
+ context?: IngestFrameContext;
116
+ };
117
+ export type IngestFrameContext = {
118
+ /**
119
+ * The file's line number of `lines[0]`, from 1.
120
+ */
121
+ startLine: number;
122
+ lines: Array<string>;
111
123
  };
112
124
  /**
113
125
  * `POST /ingest/v1/heartbeat`: a long-running process saying it is alive, so
@@ -0,0 +1,10 @@
1
+ import type { IngestFrame } from "../generated/ingest.ts";
2
+ /** Lines either side of the frame's line, as bertha shows them. */
3
+ export declare const CONTEXT_LINES = 5;
4
+ /** Longer lines are cut, as bertha would. */
5
+ export declare const LINE_CHARS = 300;
6
+ /** Frames per exception that get lines: the ones nearest the throw matter most. */
7
+ export declare const CONTEXT_FRAMES = 20;
8
+ export declare function addContext(frames: IngestFrame[]): void;
9
+ /** For tests. */
10
+ export declare function clearContextCache(): void;
@@ -0,0 +1,69 @@
1
+ // Source lines for frames in the app's own files. Code that Node runs as-is (no bundler)
2
+ // has no source map, so the lines come from disk; bertha prefers a map when there is one.
3
+ import { readFileSync, statSync } from "node:fs";
4
+ import { isAbsolute } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ /** Lines either side of the frame's line, as bertha shows them. */
7
+ export const CONTEXT_LINES = 5;
8
+ /** Longer lines are cut, as bertha would. */
9
+ export const LINE_CHARS = 300;
10
+ /** Frames per exception that get lines: the ones nearest the throw matter most. */
11
+ export const CONTEXT_FRAMES = 20;
12
+ /** Bigger files are build output, not source worth reading. */
13
+ const FILE_BYTES = 1024 * 1024;
14
+ const CACHED_FILES = 100;
15
+ // File → its lines, or null when it can't be read. Oldest first, for eviction.
16
+ const cache = new Map();
17
+ export function addContext(frames) {
18
+ let done = 0;
19
+ for (const f of frames) {
20
+ if (done >= CONTEXT_FRAMES)
21
+ break;
22
+ if (f.inApp === false || f.line === undefined || f.context)
23
+ continue;
24
+ const lines = read(f.file);
25
+ if (!lines || f.line > lines.length)
26
+ continue;
27
+ const start = Math.max(1, f.line - CONTEXT_LINES);
28
+ f.context = { startLine: start, lines: lines.slice(start - 1, f.line + CONTEXT_LINES).map(cut) };
29
+ done++;
30
+ }
31
+ }
32
+ function read(file) {
33
+ const path = toPath(file);
34
+ if (!path)
35
+ return null;
36
+ const hit = cache.get(path);
37
+ if (hit !== undefined)
38
+ return hit;
39
+ let lines = null;
40
+ try {
41
+ if (statSync(path).size <= FILE_BYTES)
42
+ lines = readFileSync(path, "utf8").split(/\r?\n/);
43
+ }
44
+ catch {
45
+ // Gone, moved or unreadable: no lines, and no second try.
46
+ }
47
+ cache.set(path, lines);
48
+ if (cache.size > CACHED_FILES)
49
+ cache.delete(cache.keys().next().value);
50
+ return lines;
51
+ }
52
+ function toPath(file) {
53
+ if (file.startsWith("file://")) {
54
+ try {
55
+ return fileURLToPath(file);
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ return isAbsolute(file) ? file : undefined;
62
+ }
63
+ function cut(line) {
64
+ return line.length > LINE_CHARS ? `${line.slice(0, LINE_CHARS - 1)}…` : line;
65
+ }
66
+ /** For tests. */
67
+ export function clearContextCache() {
68
+ cache.clear();
69
+ }
@@ -7,8 +7,12 @@ export interface NodeOptions extends Partial<Options> {
7
7
  handlers?: boolean;
8
8
  /** Tell bertha the process is alive every 20 minutes. On by default. */
9
9
  heartbeat?: boolean;
10
+ /** Send the source lines around each frame in your code, read from disk. On by default. */
11
+ contextLines?: boolean;
10
12
  }
11
13
  export declare const HEARTBEAT_MS: number;
14
+ /** V8 keeps 10 frames, which a framework's own calls can use up before reaching yours. */
15
+ export declare const STACK_FRAMES = 50;
12
16
  /** Reads BERTHA_KEY, BERTHA_URL, BERTHA_RELEASE and NODE_ENV unless given. */
13
17
  export declare function init(options?: NodeOptions): Client | undefined;
14
18
  export declare function sendHeartbeat(o: Pick<Options, "url" | "key" | "release" | "environment">): Promise<boolean>;
@@ -1,5 +1,6 @@
1
- // @bigbertha/sdk/node: uncaught exceptions and unhandled rejections, a heartbeat so
2
- // quiet hours read as "no errors" rather than "no data", and a handler wrapper.
1
+ // @bigbertha/sdk/node: uncaught exceptions and unhandled rejections, source lines from
2
+ // disk, a heartbeat so quiet hours read as "no errors" rather than "no data", and a
3
+ // handler wrapper.
3
4
  import { arch, platform } from "node:os";
4
5
  import { gzipSync } from "node:zlib";
5
6
  import { Client } from "../core/client.js";
@@ -7,8 +8,12 @@ import { firstEnv } from "../core/env.js";
7
8
  import { getClient, setClient } from "../core/hub.js";
8
9
  import { endpoint, fetchTransport } from "../core/transport.js";
9
10
  import { VERSION } from "../core/version.js";
11
+ import { addContext } from "./context.js";
10
12
  export { addBreadcrumb, captureException, captureMessage, flush, getClient, setTag, setUser } from "../core/hub.js";
11
13
  export const HEARTBEAT_MS = 20 * 60 * 1000;
14
+ /** V8 keeps 10 frames, which a framework's own calls can use up before reaching yours. */
15
+ export const STACK_FRAMES = 50;
16
+ const V8_DEFAULT_FRAMES = 10;
12
17
  const EXIT_FLUSH_MS = 2000;
13
18
  let handlersInstalled = false;
14
19
  let heartbeat;
@@ -27,8 +32,11 @@ export function init(options = {}) {
27
32
  release: options.release ?? firstEnv("BERTHA_RELEASE"),
28
33
  environment: options.environment ?? firstEnv("BERTHA_ENVIRONMENT", "VERCEL_ENV", "NODE_ENV"),
29
34
  };
30
- const client = new Client(opts, fetchTransport({ url, key, gzip: async (body) => new Uint8Array(gzipSync(body)) }), { name: "bertha.node", version: VERSION }, { runtime: `Node ${process.versions.node}`, os: `${platform()} ${arch()}` });
35
+ const client = new Client(opts, fetchTransport({ url, key, gzip: async (body) => new Uint8Array(gzipSync(body)) }), { name: "bertha.node", version: VERSION }, { runtime: `Node ${process.versions.node}`, os: `${platform()} ${arch()}` }, options.contextLines === false ? undefined : addContext);
31
36
  setClient(client);
37
+ // Only V8's default: a limit the app set itself was a choice.
38
+ if (Error.stackTraceLimit === V8_DEFAULT_FRAMES)
39
+ Error.stackTraceLimit = STACK_FRAMES;
32
40
  if (options.handlers !== false && !handlersInstalled) {
33
41
  handlersInstalled = true;
34
42
  process.on("uncaughtException", sdkHandler((error) => void crash("uncaughtException", error)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigbertha/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Error reporting for bertha: browser, Node, edge and Next.js, with source maps by debug id.",
5
5
  "license": "MIT",
6
6
  "repository": {