@bigbertha/sdk 0.6.1 → 0.8.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
@@ -3,13 +3,16 @@
3
3
  Error reporting for bertha. Browser, React, Node, edge and Next.js, with no dependencies
4
4
  and source maps matched by debug id rather than by file path.
5
5
 
6
- - About 5.8 kB gzipped in the browser (`npm run size` enforces the budget).
6
+ - About 6.9 kB gzipped in the browser (`npm run size` enforces the budget).
7
7
  - Batches events, backs off when bertha says so, and sends what's queued as the page or
8
8
  process goes away.
9
9
  - Scrubs emails, bearer tokens and secret-looking query values by default.
10
10
  - Each event says how it surfaced (an uncaught error, a rejection, a boundary, a call),
11
11
  where (the page or request) and in what state, and links the page's requests to the
12
12
  server errors they caused by W3C `traceparent`.
13
+ - Works with OpenTelemetry: `otlp()` points an OTel exporter at bertha, an error inside
14
+ an active span is recorded on it, and the browser sends its page's requests as spans
15
+ once the page has an error. See [Traces](#traces).
13
16
  - The build plugins give each bundle a debug id, upload its map and delete the `.map`
14
17
  files, so maps never ship to browsers.
15
18
 
@@ -110,6 +113,11 @@ export default defineConfig({ plugins: [bertha()] })
110
113
  init({ key, url, tracePropagationTargets: ["https://api.example.com"] })
111
114
  ```
112
115
 
116
+ - **The page's requests as spans:** the page itself, its document load and each fetch
117
+ and XHR with its method, scrubbed URL and status. They wait in memory (the last 200)
118
+ and go to bertha only once the page has had an error, then for as long as it stays
119
+ open, so a page that never errors sends none. `spans: false` turns it off.
120
+
113
121
  Frames in browser extensions and in scripts Safari hides (`webkit-masked-url://`) are
114
122
  sent as not the app's code, so bertha can say when an error probably isn't yours.
115
123
 
@@ -184,6 +192,48 @@ For bundled Node code, give the output debug ids and upload its maps after the b
184
192
  npx bertha sourcemaps upload dist
185
193
  ```
186
194
 
195
+ ## Traces
196
+
197
+ bertha takes OpenTelemetry traces over OTLP/HTTP, so an error's page shows what the
198
+ whole request did across your services: where the time went, what failed first, and
199
+ calls into a service that never answered. It keeps spans for an hour and keeps a
200
+ trace for good once an error event names it, so only traces with an error stay.
201
+
202
+ The SDK doesn't record server spans itself. Use OpenTelemetry's own instrumentation and
203
+ point its exporter at bertha with `otlp()`, which gives the URL and the key's header
204
+ from `BERTHA_KEY` and `BERTHA_URL` (or `key` and `url`):
205
+
206
+ ```ts
207
+ // Next.js: instrumentation.ts (npm install @vercel/otel)
208
+ import { OTLPHttpProtoTraceExporter, registerOTel } from "@vercel/otel"
209
+ import { otlp, register as bertha } from "@bigbertha/sdk/next"
210
+ export { onRequestError } from "@bigbertha/sdk/next"
211
+
212
+ export function register() {
213
+ registerOTel({ serviceName: "shop", traceExporter: new OTLPHttpProtoTraceExporter(otlp()) })
214
+ return bertha()
215
+ }
216
+ ```
217
+
218
+ ```ts
219
+ // Node: before anything else loads
220
+ // (npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-proto)
221
+ import { NodeSDK } from "@opentelemetry/sdk-node"
222
+ import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"
223
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"
224
+ import { init, otlp } from "@bigbertha/sdk/node"
225
+
226
+ new NodeSDK({ traceExporter: new OTLPTraceExporter(otlp()), instrumentations: [getNodeAutoInstrumentations()] }).start()
227
+ init()
228
+ ```
229
+
230
+ Any other OTLP exporter works too: send to `<url>/ingest/v1/traces` with
231
+ `Authorization: Bearer <key>`, as protobuf or JSON.
232
+
233
+ With OpenTelemetry running, every event names the span that was active when it was
234
+ captured, so bertha places the error on that span. Without it, events still carry the
235
+ trace from an incoming `traceparent` and land on the request's outermost span.
236
+
187
237
  ## Edge
188
238
 
189
239
  ```ts
@@ -219,6 +269,7 @@ await flush()
219
269
  | `versions` | | More versions on every event, as `{ name: version }`. |
220
270
  | `sendIp` | `true` | Browser: store the visitor's IP. |
221
271
  | `tracePropagationTargets` | same origin only | Browser: other origins whose requests get a `traceparent`. |
272
+ | `spans` | `true` | Browser: record the page's requests as spans and send them once the page has an error. |
222
273
  | `breadcrumbs` | all on | Browser: `fetch`, `xhr`, `navigation`, `click`, `console`. |
223
274
  | `debug` | `false` | Log what the SDK does. |
224
275
 
@@ -13,6 +13,11 @@ export interface BrowserOptions extends Options {
13
13
  * must allow the header in CORS.
14
14
  */
15
15
  tracePropagationTargets?: (string | RegExp)[];
16
+ /**
17
+ * Record the page's requests as spans, and send them to the trace once the page has
18
+ * had an error, so its waterfall shows what the browser did. On by default.
19
+ */
20
+ spans?: boolean;
16
21
  }
17
22
  export declare function init(options: BrowserOptions): Client | undefined;
18
23
  /** What the page looked like when it failed. Called for every event. */
@@ -6,6 +6,7 @@ import { scrubUrl } from "../core/scrub.js";
6
6
  import { randomHex, traceparent } from "../core/trace.js";
7
7
  import { fetchTransport, streamGzip } from "../core/transport.js";
8
8
  import { VERSION } from "../core/version.js";
9
+ import { spans } from "./spans.js";
9
10
  import { userAgent } from "./user-agent.js";
10
11
  export { addBreadcrumb, captureException, captureMessage, flush, getClient, setTag, setUser } from "../core/hub.js";
11
12
  let installed = false;
@@ -19,14 +20,19 @@ export function init(options) {
19
20
  return undefined;
20
21
  }
21
22
  const nativeFetch = window.fetch.bind(window);
23
+ const beacon = (url, body) => !!navigator.sendBeacon?.(url, new Blob([body], { type: "text/plain" }));
24
+ const recorder = options.spans === false ? undefined : spans({ url: options.url, key: options.key, traceId, fetch: nativeFetch, beacon });
22
25
  const client = new Client(options, fetchTransport({
23
26
  url: options.url,
24
27
  key: options.key,
25
28
  keyInQuery: true,
26
29
  gzip: streamGzip,
27
30
  fetch: nativeFetch,
28
- beacon: (url, body) => !!navigator.sendBeacon?.(url, new Blob([body], { type: "text/plain" })),
29
- }), { name: "bertha.browser", version: VERSION }, { ...userAgent(navigator.userAgent), userAgent: navigator.userAgent }, undefined, (event) => enrich(event, options.sendIp !== false));
31
+ beacon,
32
+ }), { name: "bertha.browser", version: VERSION }, { ...userAgent(navigator.userAgent), userAgent: navigator.userAgent }, undefined, (event) => {
33
+ enrich(event, options.sendIp !== false);
34
+ recorder?.errored();
35
+ });
30
36
  setClient(client);
31
37
  if (installed)
32
38
  return client;
@@ -41,7 +47,11 @@ export function init(options) {
41
47
  });
42
48
  });
43
49
  window.addEventListener("unhandledrejection", (e) => client.captureException(e.reason, { mechanism: { handler: "onunhandledrejection", handled: false } }));
44
- addEventListener("pagehide", () => void client.flush(true));
50
+ const final = () => {
51
+ void client.flush(true);
52
+ recorder?.flush(true);
53
+ };
54
+ addEventListener("pagehide", final);
45
55
  const on = { fetch: true, xhr: true, navigation: true, click: true, console: true, ...options.breadcrumbs };
46
56
  const base = options.url.replace(/\/+$/, "");
47
57
  const crumb = (category, message, level, timestamp) => client.addBreadcrumb({ category, message, ...(level ? { level } : {}), ...(timestamp ? { timestamp } : {}) });
@@ -50,13 +60,14 @@ export function init(options) {
50
60
  if (on.navigation)
51
61
  crumb("navigation", hidden ? "Page hidden" : "Page visible");
52
62
  if (hidden)
53
- void client.flush(true);
63
+ final();
54
64
  });
55
65
  const propagate = propagateTo(options.tracePropagationTargets ?? []);
56
- if (on.fetch)
57
- instrumentFetch(base, crumb, propagate);
58
- if (on.xhr)
59
- instrumentXhr(base, crumb, propagate);
66
+ const quiet = () => undefined;
67
+ if (on.fetch || recorder)
68
+ instrumentFetch(base, on.fetch ? crumb : quiet, propagate, recorder);
69
+ if (on.xhr || recorder)
70
+ instrumentXhr(base, on.xhr ? crumb : quiet, propagate, recorder);
60
71
  if (on.navigation)
61
72
  instrumentHistory(crumb);
62
73
  if (on.click)
@@ -122,7 +133,7 @@ function requestLine(method, url, status, start, span) {
122
133
  const ms = Math.round(performance.now() - start);
123
134
  return `${method.toUpperCase()} ${scrubUrl(url)} ${status} in ${ms}ms${span ? `, span ${span}` : ""}`;
124
135
  }
125
- function instrumentFetch(base, crumb, propagate) {
136
+ function instrumentFetch(base, crumb, propagate, recorder) {
126
137
  const original = window.fetch;
127
138
  window.fetch = async function (input, init) {
128
139
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
@@ -137,18 +148,21 @@ function instrumentFetch(base, crumb, propagate) {
137
148
  init = { ...init, headers };
138
149
  }
139
150
  const start = performance.now();
151
+ const done = recorder?.request(method, url, span ?? randomHex(16));
140
152
  try {
141
153
  const res = await original.call(window, input, init);
142
154
  crumb("fetch", requestLine(method, url, res.status, start, span), res.status >= 400 ? "error" : undefined);
155
+ done?.(res.status);
143
156
  return res;
144
157
  }
145
158
  catch (e) {
146
159
  crumb("fetch", requestLine(method, url, "failed", start, span), "error");
160
+ done?.(0, e);
147
161
  throw e;
148
162
  }
149
163
  };
150
164
  }
151
- function instrumentXhr(base, crumb, propagate) {
165
+ function instrumentXhr(base, crumb, propagate, recorder) {
152
166
  const proto = XMLHttpRequest.prototype;
153
167
  const open = proto.open;
154
168
  const send = proto.send;
@@ -171,7 +185,11 @@ function instrumentXhr(base, crumb, propagate) {
171
185
  }
172
186
  }
173
187
  const start = performance.now();
174
- this.addEventListener("loadend", () => crumb("fetch", requestLine(m[0], m[1], this.status || "failed", start, span), !this.status || this.status >= 400 ? "error" : undefined));
188
+ const done = recorder?.request(m[0], m[1], span ?? randomHex(16));
189
+ this.addEventListener("loadend", () => {
190
+ crumb("fetch", requestLine(m[0], m[1], this.status || "failed", start, span), !this.status || this.status >= 400 ? "error" : undefined);
191
+ done?.(this.status, this.status ? undefined : "The request failed: no response");
192
+ });
175
193
  }
176
194
  return send.call(this, body);
177
195
  };
@@ -0,0 +1,14 @@
1
+ export interface Spans {
2
+ /** Starts a request span; call what it returns with the status or the error. */
3
+ request(method: string, url: string, spanId: string): (status: number, error?: unknown) => void;
4
+ /** The page has an event: send what it has, and keep sending. */
5
+ errored(): void;
6
+ flush(final: boolean): void;
7
+ }
8
+ export declare function spans(o: {
9
+ url: string;
10
+ key: string;
11
+ traceId: string;
12
+ fetch: typeof fetch;
13
+ beacon: (url: string, body: string) => boolean;
14
+ }): Spans;
@@ -0,0 +1,115 @@
1
+ // The page's own requests as OpenTelemetry spans, so a trace shows what the browser did
2
+ // around the server's spans. They wait in memory and are sent only once the page has
3
+ // had an error: bertha keeps a trace only when an error names it.
4
+ import { scrubUrl } from "../core/scrub.js";
5
+ import { randomHex } from "../core/trace.js";
6
+ import { endpoint } from "../core/transport.js";
7
+ import { VERSION } from "../core/version.js";
8
+ /** Spans a page holds before its first error, and sends in all. */
9
+ const HELD = 200;
10
+ const SENT = 1000;
11
+ /** Time for the requests in flight when the error happened to finish. */
12
+ const SETTLE_MS = 3000;
13
+ export function spans(o) {
14
+ const target = `${endpoint(o.url, "traces")}?key=${encodeURIComponent(o.key)}`;
15
+ const origin = performance.timeOrigin || Date.now() - performance.now();
16
+ const now = () => origin + performance.now();
17
+ const page = { spanId: randomHex(16), parentSpanId: "", name: `page ${location.pathname}`, kind: 1, start: origin, end: origin, attributes: { "url.full": scrubUrl(location.href) } };
18
+ let held = [];
19
+ let sent = 0;
20
+ let loaded = false;
21
+ let armed = false;
22
+ let timer;
23
+ const later = () => (timer ??= setTimeout(() => flush(false), SETTLE_MS));
24
+ function flush(final) {
25
+ clearTimeout(timer);
26
+ timer = undefined;
27
+ if (!armed)
28
+ return;
29
+ const batch = held.splice(0, SENT - sent);
30
+ const nav = performance.getEntriesByType?.("navigation")[0];
31
+ const end = nav && (nav.loadEventEnd || nav.domContentLoadedEventEnd);
32
+ if (!loaded && nav && end) {
33
+ loaded = true;
34
+ batch.push({ spanId: randomHex(16), parentSpanId: page.spanId, name: "document load", kind: 1, start: origin + nav.startTime, end: origin + end, attributes: { "navigation.type": nav.type } });
35
+ }
36
+ if (!batch.length && !final)
37
+ return;
38
+ sent += batch.length;
39
+ // The page span grows with the page; bertha keeps its longest copy.
40
+ page.end = now();
41
+ batch.unshift(page);
42
+ const body = JSON.stringify(otlpJson(o.traceId, batch));
43
+ if (final && o.beacon(target, body))
44
+ return;
45
+ o.fetch(target, { method: "POST", headers: { "content-type": "text/plain" }, body, keepalive: body.length < 60_000, credentials: "omit" }).catch(() => undefined);
46
+ }
47
+ return {
48
+ request(method, url, spanId) {
49
+ const start = now();
50
+ let abs;
51
+ try {
52
+ abs = new URL(url, location.href);
53
+ }
54
+ catch {
55
+ // Recorded as the app wrote it.
56
+ }
57
+ const where = !abs ? url : abs.origin === location.origin ? abs.pathname : abs.host + abs.pathname;
58
+ return (status, error) => {
59
+ if (sent + held.length >= SENT)
60
+ return;
61
+ const span = {
62
+ spanId,
63
+ parentSpanId: page.spanId,
64
+ name: `${method.toUpperCase()} ${where}`,
65
+ kind: 3,
66
+ start,
67
+ end: now(),
68
+ attributes: { "http.request.method": method.toUpperCase(), "url.full": scrubUrl(abs?.href ?? url), ...(status ? { "http.response.status_code": status } : {}) },
69
+ error: error !== undefined ? (error instanceof Error ? error.message : String(error)) : status >= 400 ? `HTTP ${status}` : undefined,
70
+ };
71
+ held.push(span);
72
+ if (!armed && held.length > HELD)
73
+ held = held.slice(-HELD);
74
+ if (armed)
75
+ later();
76
+ };
77
+ },
78
+ errored() {
79
+ armed = true;
80
+ later();
81
+ },
82
+ flush,
83
+ };
84
+ }
85
+ /** Epoch milliseconds as OTLP's nanoseconds, in a string: they're past 2^53. */
86
+ function nanos(ms) {
87
+ return `${Math.round(ms * 1000)}000`;
88
+ }
89
+ function otlpJson(traceId, batch) {
90
+ // bertha keeps every attribute as text.
91
+ const attr = (key, v) => ({ key, value: { stringValue: String(v) } });
92
+ return {
93
+ resourceSpans: [
94
+ {
95
+ resource: { attributes: [attr("telemetry.sdk.language", "webjs"), attr("telemetry.sdk.name", "@bigbertha/sdk"), attr("telemetry.sdk.version", VERSION)] },
96
+ scopeSpans: [
97
+ {
98
+ scope: { name: "@bigbertha/sdk", version: VERSION },
99
+ spans: batch.map((s) => ({
100
+ traceId,
101
+ spanId: s.spanId,
102
+ parentSpanId: s.parentSpanId,
103
+ name: s.name,
104
+ kind: s.kind,
105
+ startTimeUnixNano: nanos(s.start),
106
+ endTimeUnixNano: nanos(s.end),
107
+ attributes: Object.entries(s.attributes).map(([k, v]) => attr(k, v)),
108
+ status: s.error ? { code: 2, message: s.error } : {},
109
+ })),
110
+ },
111
+ ],
112
+ },
113
+ ],
114
+ };
115
+ }
@@ -1,6 +1,7 @@
1
1
  // One client per app: builds events, scrubs them, batches them and sends them,
2
2
  // backing off when bertha asks it to.
3
3
  import { builtRelease, debugIdsFor } from "./debug-ids.js";
4
+ import { activeSpan } from "./otel.js";
4
5
  import { scrubText, scrubUrl } from "./scrub.js";
5
6
  import { parseStack } from "./stack.js";
6
7
  import { versions } from "./versions.js";
@@ -118,7 +119,7 @@ export class Client {
118
119
  tags: { ...this.tags, ...ctx.tags },
119
120
  fingerprint: ctx.fingerprint,
120
121
  request: request && ctx.status !== undefined ? { ...request, status: ctx.status } : request,
121
- trace: ctx.trace ?? fromReq.trace,
122
+ trace: ctx.trace ?? activeSpan() ?? fromReq.trace,
122
123
  };
123
124
  try {
124
125
  this.enrich?.(event);
@@ -0,0 +1,3 @@
1
+ import type { IngestTrace } from "./types.ts";
2
+ /** The span the code is running in, so an error lands on it in the trace. */
3
+ export declare function activeSpan(): IngestTrace | undefined;
@@ -0,0 +1,18 @@
1
+ // The active OpenTelemetry span, when the app runs OpenTelemetry, read off the API's
2
+ // global so the SDK doesn't depend on @opentelemetry/api.
3
+ const API = Symbol.for("opentelemetry.js.api.1");
4
+ const SPAN = Symbol.for("OpenTelemetry Context Key SPAN");
5
+ const INVALID = /^0*$/;
6
+ /** The span the code is running in, so an error lands on it in the trace. */
7
+ export function activeSpan() {
8
+ try {
9
+ const api = globalThis[API];
10
+ const c = api?.context?.active?.().getValue?.(SPAN)?.spanContext?.();
11
+ if (!c?.traceId || !c.spanId || INVALID.test(c.traceId) || INVALID.test(c.spanId))
12
+ return undefined;
13
+ return { traceId: c.traceId, spanId: c.spanId };
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ }
@@ -0,0 +1,13 @@
1
+ export interface OtlpOptions {
2
+ url?: string;
3
+ key?: string;
4
+ }
5
+ /**
6
+ * Settings for an OTLP/HTTP trace exporter (protobuf or JSON) that sends to bertha:
7
+ * new OTLPTraceExporter(otlp())
8
+ * Reads BERTHA_URL and BERTHA_KEY unless given.
9
+ */
10
+ export declare function otlp(options?: OtlpOptions): {
11
+ url?: string;
12
+ headers?: Record<string, string>;
13
+ };
@@ -0,0 +1,17 @@
1
+ // Where an OpenTelemetry exporter sends spans to bertha.
2
+ import { firstEnv } from "./env.js";
3
+ import { endpoint } from "./transport.js";
4
+ /**
5
+ * Settings for an OTLP/HTTP trace exporter (protobuf or JSON) that sends to bertha:
6
+ * new OTLPTraceExporter(otlp())
7
+ * Reads BERTHA_URL and BERTHA_KEY unless given.
8
+ */
9
+ export function otlp(options = {}) {
10
+ const key = options.key ?? firstEnv("BERTHA_KEY", "NEXT_PUBLIC_BERTHA_KEY");
11
+ const url = options.url ?? firstEnv("BERTHA_URL", "NEXT_PUBLIC_BERTHA_URL");
12
+ if (!key || !url) {
13
+ console.warn("[bertha] Set BERTHA_KEY and BERTHA_URL (or pass key and url). No spans will reach bertha.");
14
+ return {};
15
+ }
16
+ return { url: endpoint(url, "traces"), headers: { authorization: `Bearer ${key}` } };
17
+ }
@@ -1 +1 @@
1
- export declare const VERSION = "0.6.1";
1
+ export declare const VERSION = "0.8.0";
@@ -1 +1 @@
1
- export const VERSION = "0.6.1";
1
+ export const VERSION = "0.8.0";
@@ -1,5 +1,7 @@
1
1
  import { Client } from "../core/client.ts";
2
2
  import type { Options } from "../core/types.ts";
3
3
  export { addBreadcrumb, captureException, captureMessage, flush, getClient, setTag, setUser } from "../core/hub.ts";
4
+ export { otlp } from "../core/otlp.ts";
5
+ export type { OtlpOptions } from "../core/otlp.ts";
4
6
  export type { CaptureContext, IngestEvent, Options } from "../core/types.ts";
5
7
  export declare function init(options?: Partial<Options>): Client | undefined;
@@ -7,6 +7,7 @@ import { fromAnyRequest } from "../core/request.js";
7
7
  import { fetchTransport, streamGzip } from "../core/transport.js";
8
8
  import { VERSION } from "../core/version.js";
9
9
  export { addBreadcrumb, captureException, captureMessage, flush, getClient, setTag, setUser } from "../core/hub.js";
10
+ export { otlp } from "../core/otlp.js";
10
11
  export function init(options = {}) {
11
12
  const key = options.key ?? firstEnv("BERTHA_KEY", "NEXT_PUBLIC_BERTHA_KEY");
12
13
  const url = options.url ?? firstEnv("BERTHA_URL", "NEXT_PUBLIC_BERTHA_URL");
@@ -1,3 +1,5 @@
1
+ export { otlp } from "../core/otlp.ts";
2
+ export type { OtlpOptions } from "../core/otlp.ts";
1
3
  export declare function register(): Promise<void>;
2
4
  interface NextRequestInfo {
3
5
  path: string;
@@ -13,4 +15,3 @@ interface NextErrorContext {
13
15
  }
14
16
  /** Next calls this for errors in server components, route handlers, actions and middleware. */
15
17
  export declare function onRequestError(error: unknown, request: NextRequestInfo, context: NextErrorContext): Promise<void>;
16
- export {};
@@ -2,6 +2,7 @@
2
2
  // export { register, onRequestError } from "@bigbertha/sdk/next"
3
3
  import { captureException, flush } from "../core/hub.js";
4
4
  import { fromRequest } from "../core/request.js";
5
+ export { otlp } from "../core/otlp.js";
5
6
  export async function register() {
6
7
  if (process.env.NEXT_RUNTIME === "nodejs")
7
8
  (await import("../node/index.js")).init();
@@ -1,6 +1,8 @@
1
1
  import { Client } from "../core/client.ts";
2
2
  import type { Options } from "../core/types.ts";
3
3
  export { addBreadcrumb, captureException, captureMessage, flush, getClient, setTag, setUser } from "../core/hub.ts";
4
+ export { otlp } from "../core/otlp.ts";
5
+ export type { OtlpOptions } from "../core/otlp.ts";
4
6
  export type { CaptureContext, IngestEvent, Options } from "../core/types.ts";
5
7
  export interface NodeOptions extends Partial<Options> {
6
8
  /** Report uncaught exceptions and unhandled rejections, then exit as Node would. On by default. */
@@ -12,6 +12,7 @@ import { endpoint, fetchTransport } from "../core/transport.js";
12
12
  import { VERSION } from "../core/version.js";
13
13
  import { addContext } from "./context.js";
14
14
  export { addBreadcrumb, captureException, captureMessage, flush, getClient, setTag, setUser } from "../core/hub.js";
15
+ export { otlp } from "../core/otlp.js";
15
16
  export const HEARTBEAT_MS = 20 * 60 * 1000;
16
17
  /** V8 keeps 10 frames, which a framework's own calls can use up before reaching yours. */
17
18
  export const STACK_FRAMES = 50;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigbertha/sdk",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
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": {