vantmetry 0.0.1 → 0.0.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vantmetry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,25 @@
1
- # Vantmetry
1
+ # vantmetry
2
2
 
3
- High-performance, self-hosted frontend error logger.
3
+ Lightweight browser error tracking. Captures JavaScript errors, console errors, and unhandled promise rejections with zero blocking of the main thread.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i vantmetry
9
+ ```
10
+
11
+ Or use the CDN script tag — see [vantmetry.com/docs](https://vantmetry.com/docs).
12
+
13
+ ## Quick start
14
+
15
+ ```ts
16
+ import { init } from 'vantmetry';
17
+
18
+ init({ publicKey: 'vpk_your_key' });
19
+ ```
20
+
21
+ That's it. Errors are auto-captured from that point on. See [vantmetry.com/docs](https://vantmetry.com/docs) for full configuration, React/Next.js integrations, manual logging, and PII masking details.
22
+
23
+ ## Source
24
+
25
+ The full TypeScript source is in the `src/` directory of this package and on [GitHub](https://github.com/vantmetry/tracker).
package/core/init.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { VantmetryConfig } from './types';
2
+ export declare function init(config: VantmetryConfig): void;
@@ -0,0 +1,2 @@
1
+ import { VantmetryTracker } from './tracker';
2
+ export declare function initGlobalListeners(tracker: VantmetryTracker): void;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Masks sensitive patterns in a string.
3
+ */
4
+ export declare function maskPII(input: string): string;
5
+ /**
6
+ * Deeply masks PII in an object or array.
7
+ * Handles sensitive keys by fully redacting their values.
8
+ */
9
+ export declare function maskObjectPII<T>(obj: T, seen?: WeakSet<object>): T;
@@ -0,0 +1,4 @@
1
+ import { VantmetryTracker } from './tracker';
2
+ export declare function setInstance(tracker: VantmetryTracker): void;
3
+ export declare function getInstance(): VantmetryTracker;
4
+ export declare function isInitialized(): boolean;
@@ -0,0 +1,21 @@
1
+ import { LogPayload, VantmetryInstance, LogDetails, VantmetryConfig } from './types';
2
+ export declare class VantmetryTracker implements VantmetryInstance {
3
+ private buffer;
4
+ private flushTimer;
5
+ private transport;
6
+ isReady: boolean;
7
+ private sentErrors;
8
+ private eventsThisSecond;
9
+ private lastResetTime;
10
+ private readonly TTL_MS;
11
+ private readonly MAX_EVENTS_PER_SEC;
12
+ constructor(config: VantmetryConfig);
13
+ error(message: string | unknown, details?: LogDetails): void;
14
+ warn(message: string, details?: LogDetails): void;
15
+ info(message: string, details?: LogDetails): void;
16
+ debug(message: string, details?: LogDetails): void;
17
+ flush(): Promise<void>;
18
+ private addToBuffer;
19
+ captureAutoError(payload: LogPayload): void;
20
+ private getSignature;
21
+ }
@@ -0,0 +1,10 @@
1
+ import { VantmetryConfig } from './types';
2
+ export declare class TransportManager {
3
+ private wtSession;
4
+ private readonly endpoint;
5
+ private readonly wtEndpoint;
6
+ private readonly debug;
7
+ constructor(config: VantmetryConfig);
8
+ private initWT;
9
+ send(payload: string): Promise<void>;
10
+ }
@@ -0,0 +1,41 @@
1
+ export declare const LogLevel: {
2
+ readonly ERROR: "ERROR";
3
+ readonly INFO: "INFO";
4
+ readonly WARN: "WARN";
5
+ readonly DEBUG: "DEBUG";
6
+ };
7
+ export type VantmetryLogLevel = (typeof LogLevel)[keyof typeof LogLevel];
8
+ export type LogDetails = Record<string, unknown>;
9
+ export interface VantmetryInstance {
10
+ isReady: boolean;
11
+ error: (message: string | unknown, details?: LogDetails) => void;
12
+ warn: (message: string, details?: LogDetails) => void;
13
+ info: (message: string, details?: LogDetails) => void;
14
+ debug: (message: string, details?: LogDetails) => void;
15
+ flush: () => Promise<void>;
16
+ }
17
+ declare global {
18
+ interface Window {
19
+ Vantmetry?: VantmetryInstance;
20
+ onVantmetryReady?: (instance: VantmetryInstance) => void;
21
+ }
22
+ }
23
+ export interface LogPayload {
24
+ message: string | Event | unknown;
25
+ severity: VantmetryLogLevel;
26
+ type?: string;
27
+ stack?: string;
28
+ loc?: string;
29
+ trace_id?: string;
30
+ details?: LogDetails;
31
+ }
32
+ export interface LogItem extends LogPayload {
33
+ count: number;
34
+ ts: number;
35
+ url: string;
36
+ ua: string;
37
+ }
38
+ export interface VantmetryConfig {
39
+ publicKey: string;
40
+ ingestorUrl?: string;
41
+ }
package/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { VantmetryInstance } from './core/types';
2
+ export { init } from './core/init';
3
+ export { VantmetryTracker } from './core/tracker';
4
+ export { initGlobalListeners } from './core/listeners';
5
+ export type { VantmetryConfig, LogDetails, VantmetryInstance, VantmetryLogLevel } from './core/types';
6
+ export declare const logger: VantmetryInstance;
package/index.js CHANGED
@@ -1 +1,28 @@
1
- module.exports = {};
1
+ import { g as t } from "./init-DdlNmKDT.js";
2
+ import { V as i, i as g, a as f } from "./init-DdlNmKDT.js";
3
+ const n = {
4
+ get isReady() {
5
+ return t().isReady;
6
+ },
7
+ error(r, e) {
8
+ t().error(r, e);
9
+ },
10
+ warn(r, e) {
11
+ t().warn(r, e);
12
+ },
13
+ info(r, e) {
14
+ t().info(r, e);
15
+ },
16
+ debug(r, e) {
17
+ t().debug(r, e);
18
+ },
19
+ flush() {
20
+ return t().flush();
21
+ }
22
+ };
23
+ export {
24
+ i as VantmetryTracker,
25
+ g as init,
26
+ f as initGlobalListeners,
27
+ n as logger
28
+ };
@@ -0,0 +1,270 @@
1
+ let f = null;
2
+ function E(n) {
3
+ f = n;
4
+ }
5
+ function A() {
6
+ if (!f)
7
+ throw new Error("[Vantmetry] Not initialized. Call init() before using logger.");
8
+ return f;
9
+ }
10
+ function T() {
11
+ return f !== null;
12
+ }
13
+ const c = {
14
+ ERROR: "ERROR",
15
+ INFO: "INFO",
16
+ WARN: "WARN",
17
+ DEBUG: "DEBUG"
18
+ }, b = "https://ingestor.vantmetry.com:4433";
19
+ class S {
20
+ wtSession = null;
21
+ endpoint;
22
+ wtEndpoint;
23
+ debug;
24
+ constructor(e) {
25
+ const t = (e.ingestorUrl ?? b).replace(/\/$/, "");
26
+ this.endpoint = `${t}/api/ingestor/push/tcp?public_key=${e.publicKey}`, this.wtEndpoint = `${t}/api/ingestor/push/udp?public_key=${e.publicKey}`;
27
+ try {
28
+ this.debug = typeof window < "u" && !!window.localStorage?.getItem("vantmetry_debug");
29
+ } catch {
30
+ this.debug = !1;
31
+ }
32
+ this.initWT();
33
+ }
34
+ async initWT() {
35
+ if ("WebTransport" in window) {
36
+ await new Promise((e) => setTimeout(e, 200));
37
+ try {
38
+ this.wtSession = new WebTransport(this.wtEndpoint), await this.wtSession.ready, this.debug && console.log("WT: Connected");
39
+ } catch (e) {
40
+ this.debug && console.warn("WT: Failed, falling back to beacon", e), this.wtSession = null;
41
+ }
42
+ }
43
+ }
44
+ async send(e) {
45
+ if (this.wtSession)
46
+ try {
47
+ const s = (await this.wtSession.createUnidirectionalStream()).getWriter();
48
+ await s.write(new TextEncoder().encode(e)), await s.close();
49
+ return;
50
+ } catch (t) {
51
+ this.debug && console.warn("WT: Failed, falling back to beacon", t), this.wtSession = null;
52
+ }
53
+ if (typeof navigator < "u" && navigator.sendBeacon) {
54
+ const t = new Blob([e], { type: "application/json" });
55
+ if (navigator.sendBeacon(this.endpoint, t))
56
+ return;
57
+ }
58
+ fetch(this.endpoint, {
59
+ method: "POST",
60
+ body: e,
61
+ keepalive: !0,
62
+ headers: { "Content-Type": "application/json" }
63
+ }).catch((t) => {
64
+ this.debug && console.error("Vantmetry send failed", t);
65
+ });
66
+ }
67
+ }
68
+ const l = {
69
+ // Matches standard email formats
70
+ email: /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/g,
71
+ // Matches standard CC groupings: 4-4-4-[3-4] with required separators, or Amex 4-6-5 format.
72
+ creditCard: /\b(?:\d{4}[-\s]){3}\d{3,4}\b|\b\d{4}[-\s]\d{6}[-\s]\d{5}\b/g,
73
+ // US Social Security Numbers (SSN): 3-2-4 format
74
+ ssn: /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/g,
75
+ // JWT Tokens (Header.Payload.Signature format starts with 'ey')
76
+ jwt: /\bey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
77
+ // Generic secret detection (Bearer tokens, Basic auth, API keys)
78
+ authHeader: /\b(Bearer|Basic|Token)\s+[A-Za-z0-9\-._~+/=]+/gi
79
+ }, p = /* @__PURE__ */ new Set([
80
+ "password",
81
+ "passwd",
82
+ "pwd",
83
+ "secret",
84
+ "token",
85
+ "authorization",
86
+ "api_key",
87
+ "apikey",
88
+ "access_token",
89
+ "refresh_token",
90
+ "session",
91
+ "cookie",
92
+ "credentials",
93
+ "client_secret",
94
+ "auth"
95
+ ]);
96
+ function d(n) {
97
+ let e = n;
98
+ return e = e.replace(l.email, (t) => {
99
+ const s = t.split("@");
100
+ if (s.length !== 2)
101
+ return t;
102
+ const [r, a] = s;
103
+ return `${r.charAt(0)}***@${a}`;
104
+ }), e = e.replace(l.creditCard, (t) => {
105
+ const s = t.replace(/[-\s]/g, ""), r = s.slice(-4);
106
+ return "*".repeat(s.length - 4) + r;
107
+ }), e = e.replace(l.ssn, (t) => `***-**-${t.slice(-4)}`), e = e.replace(l.jwt, "[JWT REDACTED]"), e = e.replace(l.authHeader, "$1 [TOKEN REDACTED]"), e;
108
+ }
109
+ function h(n, e = /* @__PURE__ */ new WeakSet()) {
110
+ if (typeof n == "string")
111
+ return d(n);
112
+ if (!n || typeof n != "object")
113
+ return n;
114
+ if (e.has(n))
115
+ return "[Circular]";
116
+ if (e.add(n), Array.isArray(n))
117
+ return n.map((s) => h(s, e));
118
+ const t = {};
119
+ for (const [s, r] of Object.entries(n)) {
120
+ const a = s.toLowerCase();
121
+ if (p.has(a) || Array.from(p).some((i) => a.includes(i))) {
122
+ t[s] = "[REDACTED]";
123
+ continue;
124
+ }
125
+ typeof r == "string" ? t[s] = d(r) : typeof r == "object" && r !== null ? t[s] = h(r, e) : t[s] = r;
126
+ }
127
+ return t;
128
+ }
129
+ const R = 50, k = 2e3;
130
+ class v {
131
+ buffer = [];
132
+ flushTimer = null;
133
+ transport;
134
+ isReady = !0;
135
+ // Deduplication & Circuit Breaker
136
+ sentErrors = /* @__PURE__ */ new Map();
137
+ eventsThisSecond = 0;
138
+ lastResetTime = Date.now();
139
+ TTL_MS = 6e4;
140
+ MAX_EVENTS_PER_SEC = 100;
141
+ constructor(e) {
142
+ this.transport = new S(e);
143
+ }
144
+ // --- Public API ---
145
+ error(e, t) {
146
+ this.addToBuffer({ severity: c.ERROR, type: "manual", message: e, details: t });
147
+ }
148
+ warn(e, t) {
149
+ this.addToBuffer({ severity: c.WARN, type: "manual", message: e, details: t });
150
+ }
151
+ info(e, t) {
152
+ this.addToBuffer({ severity: c.INFO, type: "manual", message: e, details: t });
153
+ }
154
+ debug(e, t) {
155
+ this.addToBuffer({ severity: c.DEBUG, type: "manual", message: e, details: t });
156
+ }
157
+ async flush() {
158
+ if (this.buffer.length === 0)
159
+ return;
160
+ const e = Date.now();
161
+ for (const s of this.buffer)
162
+ this.sentErrors.set(this.getSignature(s), e);
163
+ for (const [s, r] of this.sentErrors.entries())
164
+ e - r >= this.TTL_MS && this.sentErrors.delete(s);
165
+ const t = JSON.stringify(this.buffer);
166
+ this.buffer = [], this.flushTimer && (clearTimeout(this.flushTimer), this.flushTimer = null), await this.transport.send(t);
167
+ }
168
+ // --- Internal Logic ---
169
+ addToBuffer(e) {
170
+ if (!this.isReady)
171
+ return;
172
+ const t = Date.now();
173
+ if (t - this.lastResetTime > 1e3 && (this.eventsThisSecond = 0, this.lastResetTime = t), this.eventsThisSecond++, this.eventsThisSecond > this.MAX_EVENTS_PER_SEC) {
174
+ this.isReady = !1, console.error("[Vantmetry] Logging disabled to save browser CPU due to infinite loop detection.");
175
+ return;
176
+ }
177
+ const s = this.getSignature(e), r = this.sentErrors.get(s);
178
+ if (r && t - r < this.TTL_MS)
179
+ return;
180
+ const a = this.buffer.find((m) => this.getSignature(m) === s);
181
+ if (a) {
182
+ a.count = (a.count || 1) + 1;
183
+ return;
184
+ }
185
+ let { message: i, stack: o } = e;
186
+ const { details: u } = e;
187
+ i instanceof Error && (o = o ?? i.stack, i = i.message || String(i));
188
+ const g = typeof i == "string" ? d(i) : i, y = typeof u == "object" ? h(u) : u, w = typeof o == "string" ? d(o) : o;
189
+ this.buffer.push({
190
+ ...e,
191
+ message: g,
192
+ details: y,
193
+ stack: w,
194
+ count: 1,
195
+ ts: Date.now(),
196
+ url: window.location.href,
197
+ ua: navigator.userAgent
198
+ }), this.buffer.length >= R ? this.flush() : this.flushTimer || (this.flushTimer = setTimeout(() => {
199
+ this.flush();
200
+ }, k));
201
+ }
202
+ captureAutoError(e) {
203
+ this.addToBuffer(e);
204
+ }
205
+ getSignature(e) {
206
+ return `${e.type}:${e.severity}:${e.message}`;
207
+ }
208
+ }
209
+ function _(n) {
210
+ const e = console.error;
211
+ console.error = function(...t) {
212
+ if (e.apply(console, t), !n._isCapturingConsoleError) {
213
+ n._isCapturingConsoleError = !0;
214
+ try {
215
+ let s, r;
216
+ const a = t.findIndex((o) => o instanceof Error), i = t[a];
217
+ if (i) {
218
+ const o = t.slice(0, a).filter((u) => typeof u == "string").join(" ");
219
+ s = o ? `${o}: ${i.message || String(i)}` : i.message || String(i), r = i.stack;
220
+ } else
221
+ s = t.map((o) => {
222
+ if (typeof o == "string") return o;
223
+ try {
224
+ return JSON.stringify(o);
225
+ } catch {
226
+ return String(o);
227
+ }
228
+ }).join(" ");
229
+ n.captureAutoError({
230
+ type: "console.error",
231
+ message: s || "Unknown console.error",
232
+ stack: r,
233
+ severity: c.ERROR
234
+ });
235
+ } finally {
236
+ n._isCapturingConsoleError = !1;
237
+ }
238
+ }
239
+ }, window.addEventListener("error", function(t) {
240
+ n.captureAutoError({
241
+ type: "crash",
242
+ message: t.message || "Script error.",
243
+ stack: t.error?.stack,
244
+ loc: `${t.filename}:${t.lineno}:${t.colno}`,
245
+ severity: c.ERROR
246
+ });
247
+ }, { capture: !0 }), window.addEventListener("unhandledrejection", function(t) {
248
+ const s = t.reason, r = s instanceof Error;
249
+ n.captureAutoError({
250
+ type: "promise",
251
+ message: r ? s.message : String(s),
252
+ stack: r ? s.stack : new Error(`Unhandled rejection: ${String(s)}`).stack,
253
+ severity: c.ERROR
254
+ });
255
+ }, { capture: !0 }), document.addEventListener("visibilitychange", function() {
256
+ document.visibilityState === "hidden" && n.flush();
257
+ });
258
+ }
259
+ function C(n) {
260
+ if (T())
261
+ return;
262
+ const e = new v(n);
263
+ E(e), _(e);
264
+ }
265
+ export {
266
+ v as V,
267
+ _ as a,
268
+ A as g,
269
+ C as i
270
+ };
@@ -0,0 +1,11 @@
1
+ import { VantmetryConfig } from '../index';
2
+ /**
3
+ * Drop this into your Next.js layout or _document to load the tracker via CDN.
4
+ * Uses next/script so Next.js controls placement and loading strategy.
5
+ */
6
+ export declare function VantmetryScript({ publicKey, ingestorUrl }: VantmetryConfig): import("react/jsx-runtime").JSX.Element;
7
+ /**
8
+ * SSR-safe wrapper around init(). Use this in _app.tsx, app/layout.tsx client
9
+ * components, or anywhere module-level code may run on the server.
10
+ */
11
+ export declare function initVantmetry(config: VantmetryConfig): void;
package/next/index.js ADDED
@@ -0,0 +1,21 @@
1
+ import { jsx as i } from "react/jsx-runtime";
2
+ import n from "next/script";
3
+ import { i as e } from "../init-DdlNmKDT.js";
4
+ function m({ publicKey: t, ingestorUrl: r }) {
5
+ return /* @__PURE__ */ i(
6
+ n,
7
+ {
8
+ strategy: "afterInteractive",
9
+ "data-public-key": t,
10
+ "data-ingestor-url": r,
11
+ src: "https://cdn.vantmetry.com/tracker.js"
12
+ }
13
+ );
14
+ }
15
+ function f(t) {
16
+ typeof window > "u" || e(t);
17
+ }
18
+ export {
19
+ m as VantmetryScript,
20
+ f as initVantmetry
21
+ };
package/package.json CHANGED
@@ -1,11 +1,64 @@
1
1
  {
2
2
  "name": "vantmetry",
3
- "version": "0.0.1",
4
- "description": "High-performance, self-hosted frontend error logger. Coming soon.",
5
- "main": "index.js",
3
+ "version": "0.0.3",
4
+ "description": "Lightweight frontend error tracking with minimal browser impact.",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "types": "./index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./index.js",
11
+ "types": "./index.d.ts"
12
+ },
13
+ "./react": {
14
+ "import": "./react/index.js",
15
+ "types": "./react/index.d.ts"
16
+ },
17
+ "./next": {
18
+ "import": "./next/index.js",
19
+ "types": "./next/index.d.ts"
20
+ }
21
+ },
22
+ "sideEffects": false,
23
+ "files": [
24
+ "*.js",
25
+ "*.d.ts",
26
+ "core",
27
+ "react",
28
+ "next",
29
+ "src",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
6
33
  "keywords": [
7
- "error-logging"
34
+ "error-tracking",
35
+ "observability",
36
+ "frontend",
37
+ "browser",
38
+ "privacy"
8
39
  ],
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/vantmetry/tracker.git"
43
+ },
44
+ "homepage": "https://vantmetry.com",
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "registry": "https://registry.npmjs.org"
48
+ },
9
49
  "author": "Vantmetry Team",
10
- "license": "MIT"
11
- }
50
+ "license": "MIT",
51
+ "dependencies": {},
52
+ "peerDependencies": {
53
+ "react": ">=18.0.0",
54
+ "next": ">=13.0.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "react": {
58
+ "optional": true
59
+ },
60
+ "next": {
61
+ "optional": true
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,32 @@
1
+ import { default as React, Component, ReactNode } from 'react';
2
+ import { VantmetryConfig, VantmetryInstance } from '../index';
3
+ interface VantmetryProviderProps extends VantmetryConfig {
4
+ children: ReactNode;
5
+ }
6
+ /**
7
+ * Initializes Vantmetry and makes the logger available via {@link useLogger}.
8
+ */
9
+ export declare function VantmetryProvider({ publicKey, ingestorUrl, children }: VantmetryProviderProps): import("react/jsx-runtime").JSX.Element;
10
+ /**
11
+ * Returns the Vantmetry logger. Must be called inside a {@link VantmetryProvider}.
12
+ */
13
+ export declare function useLogger(): VantmetryInstance;
14
+ interface ErrorBoundaryProps {
15
+ children: ReactNode;
16
+ /** Rendered when a render error is caught. Defaults to null (renders nothing). */
17
+ fallback?: ReactNode;
18
+ }
19
+ interface ErrorBoundaryState {
20
+ hasError: boolean;
21
+ }
22
+ /**
23
+ * Catches React render errors, logs them via Vantmetry, and renders the fallback.
24
+ * Place around any subtree you want to protect.
25
+ */
26
+ export declare class VantmetryErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
27
+ constructor(props: ErrorBoundaryProps);
28
+ static getDerivedStateFromError(_error: Error): ErrorBoundaryState;
29
+ componentDidCatch(error: Error, info: React.ErrorInfo): void;
30
+ render(): ReactNode;
31
+ }
32
+ export {};
package/react/index.js ADDED
@@ -0,0 +1,37 @@
1
+ import { jsx as s } from "react/jsx-runtime";
2
+ import { createContext as i, Component as a, useEffect as u, useContext as c } from "react";
3
+ import { logger as o } from "../index.js";
4
+ import { i as m } from "../init-DdlNmKDT.js";
5
+ const n = i(null);
6
+ function h({ publicKey: t, ingestorUrl: r, children: e }) {
7
+ return u(() => {
8
+ m({ publicKey: t, ingestorUrl: r });
9
+ }, []), /* @__PURE__ */ s(n.Provider, { value: o, children: e });
10
+ }
11
+ function y() {
12
+ const t = c(n);
13
+ if (t === null)
14
+ throw new Error(
15
+ 'useLogger() was called outside of VantmetryProvider. Wrap your app with <VantmetryProvider publicKey="..." /> to use this hook.'
16
+ );
17
+ return t;
18
+ }
19
+ class g extends a {
20
+ constructor(r) {
21
+ super(r), this.state = { hasError: !1 };
22
+ }
23
+ static getDerivedStateFromError(r) {
24
+ return { hasError: !0 };
25
+ }
26
+ componentDidCatch(r, e) {
27
+ o.error(r, { componentStack: e.componentStack ?? void 0 });
28
+ }
29
+ render() {
30
+ return this.state.hasError ? this.props.fallback ?? null : this.props.children;
31
+ }
32
+ }
33
+ export {
34
+ g as VantmetryErrorBoundary,
35
+ h as VantmetryProvider,
36
+ y as useLogger
37
+ };
@@ -0,0 +1,13 @@
1
+ import { VantmetryTracker } from './tracker';
2
+ import { initGlobalListeners } from './listeners';
3
+ import { setInstance, isInitialized } from './singleton';
4
+ import type { VantmetryConfig } from './types';
5
+
6
+ export function init(config: VantmetryConfig): void {
7
+ if (isInitialized()) {
8
+ return;
9
+ }
10
+ const tracker = new VantmetryTracker(config);
11
+ setInstance(tracker);
12
+ initGlobalListeners(tracker);
13
+ }
@@ -0,0 +1,76 @@
1
+ import { VantmetryTracker } from './tracker';
2
+ import { LogLevel } from './types';
3
+
4
+ export function initGlobalListeners(tracker: VantmetryTracker) {
5
+ const originalConsoleError = console.error;
6
+ console.error = function (...args: unknown[]) {
7
+ originalConsoleError.apply(console, args);
8
+
9
+ if ((tracker as unknown as Record<string, unknown>)['_isCapturingConsoleError']) return;
10
+ (tracker as unknown as Record<string, unknown>)['_isCapturingConsoleError'] = true;
11
+
12
+ try {
13
+ let message: string;
14
+ let stack: string | undefined;
15
+
16
+ const errorIndex = args.findIndex((arg) => arg instanceof Error);
17
+ const errorObj = args[errorIndex] as Error | undefined;
18
+ if (errorObj) {
19
+ const prefix = args
20
+ .slice(0, errorIndex)
21
+ .filter((a) => typeof a === 'string')
22
+ .join(' ');
23
+ message = prefix ? `${prefix}: ${errorObj.message || String(errorObj)}` : errorObj.message || String(errorObj);
24
+ stack = errorObj.stack;
25
+ } else {
26
+ message = args
27
+ .map((arg) => {
28
+ if (typeof arg === 'string') return arg;
29
+ try {
30
+ return JSON.stringify(arg);
31
+ } catch {
32
+ return String(arg);
33
+ }
34
+ })
35
+ .join(' ');
36
+ }
37
+
38
+ tracker.captureAutoError({
39
+ type: 'console.error',
40
+ message: message || 'Unknown console.error',
41
+ stack: stack,
42
+ severity: LogLevel.ERROR,
43
+ });
44
+ } finally {
45
+ (tracker as unknown as Record<string, unknown>)['_isCapturingConsoleError'] = false;
46
+ }
47
+ };
48
+
49
+ window.addEventListener('error', function (event: ErrorEvent) {
50
+ tracker.captureAutoError({
51
+ type: 'crash',
52
+ message: event.message || 'Script error.',
53
+ stack: event.error?.stack,
54
+ loc: `${event.filename}:${event.lineno}:${event.colno}`,
55
+ severity: LogLevel.ERROR,
56
+ });
57
+ }, { capture: true });
58
+
59
+ window.addEventListener('unhandledrejection', function (event: PromiseRejectionEvent) {
60
+ const reason = event.reason;
61
+ const isError = reason instanceof Error;
62
+ tracker.captureAutoError({
63
+ type: 'promise',
64
+ message: isError ? reason.message : String(reason),
65
+ stack: isError ? reason.stack : new Error(`Unhandled rejection: ${String(reason)}`).stack,
66
+ severity: LogLevel.ERROR,
67
+ });
68
+ }, { capture: true });
69
+
70
+ // Flush on page unload
71
+ document.addEventListener('visibilitychange', function () {
72
+ if (document.visibilityState === 'hidden') {
73
+ void tracker.flush();
74
+ }
75
+ });
76
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Regex collection for PII and credential masking.
3
+ */
4
+ const PATTERNS = {
5
+ // Matches standard email formats
6
+ email: /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/g,
7
+
8
+ // Matches standard CC groupings: 4-4-4-[3-4] with required separators, or Amex 4-6-5 format.
9
+ creditCard: /\b(?:\d{4}[-\s]){3}\d{3,4}\b|\b\d{4}[-\s]\d{6}[-\s]\d{5}\b/g,
10
+
11
+ // US Social Security Numbers (SSN): 3-2-4 format
12
+ ssn: /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/g,
13
+
14
+ // JWT Tokens (Header.Payload.Signature format starts with 'ey')
15
+ jwt: /\bey[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
16
+
17
+ // Generic secret detection (Bearer tokens, Basic auth, API keys)
18
+ authHeader: /\b(Bearer|Basic|Token)\s+[A-Za-z0-9\-._~+/=]+/gi,
19
+ };
20
+
21
+ // Keys that suggest the value is highly sensitive and should be completely redacted
22
+ const SENSITIVE_KEYS = new Set([
23
+ 'password',
24
+ 'passwd',
25
+ 'pwd',
26
+ 'secret',
27
+ 'token',
28
+ 'authorization',
29
+ 'api_key',
30
+ 'apikey',
31
+ 'access_token',
32
+ 'refresh_token',
33
+ 'session',
34
+ 'cookie',
35
+ 'credentials',
36
+ 'client_secret',
37
+ 'auth',
38
+ ]);
39
+
40
+ /**
41
+ * Masks sensitive patterns in a string.
42
+ */
43
+ export function maskPII(input: string): string {
44
+ let masked = input;
45
+
46
+ // Mask emails: user@example.com -> u***@example.com
47
+ masked = masked.replace(PATTERNS.email, (match) => {
48
+ const parts = match.split('@');
49
+ if (parts.length !== 2) {
50
+ return match;
51
+ }
52
+ const [user, domain] = parts;
53
+ const visibleUser = user.charAt(0);
54
+ return `${visibleUser}***@${domain}`;
55
+ });
56
+
57
+ // Mask credit cards: replaces all but the last 4 digits.
58
+ masked = masked.replace(PATTERNS.creditCard, (match) => {
59
+ const cleaned = match.replace(/[-\s]/g, '');
60
+ const last4 = cleaned.slice(-4);
61
+ return '*'.repeat(cleaned.length - 4) + last4;
62
+ });
63
+
64
+ // Mask SSNs: replaces first 5 digits -> ***-**-1234
65
+ masked = masked.replace(PATTERNS.ssn, (match) => {
66
+ const last4 = match.slice(-4);
67
+ return `***-**-${last4}`;
68
+ });
69
+
70
+ // Mask JWTs: eyJhb... -> [JWT REDACTED]
71
+ masked = masked.replace(PATTERNS.jwt, '[JWT REDACTED]');
72
+
73
+ // Mask Auth Headers
74
+ masked = masked.replace(PATTERNS.authHeader, '$1 [TOKEN REDACTED]');
75
+
76
+ return masked;
77
+ }
78
+
79
+ /**
80
+ * Deeply masks PII in an object or array.
81
+ * Handles sensitive keys by fully redacting their values.
82
+ */
83
+ export function maskObjectPII<T>(obj: T, seen = new WeakSet()): T {
84
+ // Handle primitives and nulls
85
+ if (typeof obj === 'string') {
86
+ return maskPII(obj) as unknown as T;
87
+ }
88
+
89
+ if (!obj || typeof obj !== 'object') {
90
+ return obj;
91
+ }
92
+
93
+ // Prevent circular reference infinite loops
94
+ if (seen.has(obj as object)) {
95
+ return '[Circular]' as unknown as T;
96
+ }
97
+ seen.add(obj as object);
98
+
99
+ if (Array.isArray(obj)) {
100
+ return obj.map((item) => maskObjectPII(item, seen)) as unknown as T;
101
+ }
102
+
103
+ const result: Record<string, unknown> = {};
104
+ for (const [key, value] of Object.entries(obj)) {
105
+ const lowerKey = key.toLowerCase();
106
+
107
+ // Completely redact known sensitive keys
108
+ if (SENSITIVE_KEYS.has(lowerKey) || Array.from(SENSITIVE_KEYS).some((k) => lowerKey.includes(k))) {
109
+ result[key] = '[REDACTED]';
110
+ continue;
111
+ }
112
+
113
+ if (typeof value === 'string') {
114
+ result[key] = maskPII(value);
115
+ } else if (typeof value === 'object' && value !== null) {
116
+ result[key] = maskObjectPII(value, seen);
117
+ } else {
118
+ result[key] = value;
119
+ }
120
+ }
121
+
122
+ return result as T;
123
+ }
@@ -0,0 +1,18 @@
1
+ import type { VantmetryTracker } from './tracker';
2
+
3
+ let instance: VantmetryTracker | null = null;
4
+
5
+ export function setInstance(tracker: VantmetryTracker): void {
6
+ instance = tracker;
7
+ }
8
+
9
+ export function getInstance(): VantmetryTracker {
10
+ if (!instance) {
11
+ throw new Error('[Vantmetry] Not initialized. Call init() before using logger.');
12
+ }
13
+ return instance;
14
+ }
15
+
16
+ export function isInitialized(): boolean {
17
+ return instance !== null;
18
+ }
@@ -0,0 +1,146 @@
1
+ import type { LogItem, LogPayload, VantmetryInstance, LogDetails, VantmetryConfig } from './types';
2
+ import { LogLevel } from './types';
3
+ import { TransportManager } from './transport';
4
+ import { maskPII, maskObjectPII } from './privacy';
5
+
6
+ const BATCH_LIMIT = 50;
7
+ const FLUSH_INTERVAL = 2000;
8
+
9
+ export class VantmetryTracker implements VantmetryInstance {
10
+ private buffer: Array<LogItem> = [];
11
+ private flushTimer: number | null = null;
12
+ private transport: TransportManager;
13
+ public isReady = true;
14
+
15
+ // Deduplication & Circuit Breaker
16
+ private sentErrors = new Map<string, number>();
17
+ private eventsThisSecond = 0;
18
+ private lastResetTime = Date.now();
19
+ private readonly TTL_MS = 60000;
20
+ private readonly MAX_EVENTS_PER_SEC = 100;
21
+
22
+ constructor(config: VantmetryConfig) {
23
+ this.transport = new TransportManager(config);
24
+ }
25
+
26
+ // --- Public API ---
27
+
28
+ public error(message: string | unknown, details?: LogDetails) {
29
+ this.addToBuffer({ severity: LogLevel.ERROR, type: 'manual', message, details });
30
+ }
31
+
32
+ public warn(message: string, details?: LogDetails) {
33
+ this.addToBuffer({ severity: LogLevel.WARN, type: 'manual', message, details });
34
+ }
35
+
36
+ public info(message: string, details?: LogDetails) {
37
+ this.addToBuffer({ severity: LogLevel.INFO, type: 'manual', message, details });
38
+ }
39
+
40
+ public debug(message: string, details?: LogDetails) {
41
+ this.addToBuffer({ severity: LogLevel.DEBUG, type: 'manual', message, details });
42
+ }
43
+
44
+ public async flush() {
45
+ if (this.buffer.length === 0) {
46
+ return;
47
+ }
48
+
49
+ const now = Date.now();
50
+ for (const item of this.buffer) {
51
+ this.sentErrors.set(this.getSignature(item), now);
52
+ }
53
+
54
+ // Clean up expired TTLs
55
+ for (const [key, timestamp] of this.sentErrors.entries()) {
56
+ if (now - timestamp >= this.TTL_MS) {
57
+ this.sentErrors.delete(key);
58
+ }
59
+ }
60
+
61
+ const dataPayload = JSON.stringify(this.buffer);
62
+ this.buffer = []; // Clear immediately
63
+
64
+ if (this.flushTimer) {
65
+ clearTimeout(this.flushTimer);
66
+ this.flushTimer = null;
67
+ }
68
+
69
+ await this.transport.send(dataPayload);
70
+ }
71
+
72
+ // --- Internal Logic ---
73
+
74
+ private addToBuffer(payload: LogPayload) {
75
+ if (!this.isReady) {
76
+ return;
77
+ }
78
+
79
+ const now = Date.now();
80
+
81
+ // Emergency Circuit Breaker (Infinite Loop Protection)
82
+ if (now - this.lastResetTime > 1000) {
83
+ this.eventsThisSecond = 0;
84
+ this.lastResetTime = now;
85
+ }
86
+ this.eventsThisSecond++;
87
+
88
+ if (this.eventsThisSecond > this.MAX_EVENTS_PER_SEC) {
89
+ this.isReady = false;
90
+ console.error('[Vantmetry] Logging disabled to save browser CPU due to infinite loop detection.');
91
+ return;
92
+ }
93
+
94
+ const signature = this.getSignature(payload);
95
+
96
+ // Cross-flush Suppression
97
+ const lastSent = this.sentErrors.get(signature);
98
+ if (lastSent && now - lastSent < this.TTL_MS) {
99
+ return; // Silently drop exact duplicates within TTL
100
+ }
101
+
102
+ // Intra-buffer Deduplication
103
+ const existing = this.buffer.find((item) => this.getSignature(item) === signature);
104
+ if (existing) {
105
+ existing.count = (existing.count || 1) + 1;
106
+ return;
107
+ }
108
+
109
+ let { message, stack } = payload;
110
+ const { details } = payload;
111
+
112
+ if (message instanceof Error) {
113
+ stack = stack ?? message.stack;
114
+ message = message.message || String(message);
115
+ }
116
+
117
+ const maskedMessage = typeof message === 'string' ? maskPII(message) : message;
118
+ const maskedDetails = typeof details === 'object' ? maskObjectPII(details) : details;
119
+ const maskedStack = typeof stack === 'string' ? maskPII(stack) : stack;
120
+
121
+ this.buffer.push({
122
+ ...payload,
123
+ message: maskedMessage,
124
+ details: maskedDetails,
125
+ stack: maskedStack,
126
+ count: 1,
127
+ ts: Date.now(),
128
+ url: window.location.href,
129
+ ua: navigator.userAgent,
130
+ });
131
+
132
+ if (this.buffer.length >= BATCH_LIMIT) {
133
+ void this.flush();
134
+ } else if (!this.flushTimer) {
135
+ this.flushTimer = setTimeout(() => void this.flush(), FLUSH_INTERVAL) as unknown as number;
136
+ }
137
+ }
138
+
139
+ public captureAutoError(payload: LogPayload) {
140
+ this.addToBuffer(payload);
141
+ }
142
+
143
+ private getSignature(item: { type?: string; severity: string; message: unknown }): string {
144
+ return `${item.type}:${item.severity}:${item.message}`;
145
+ }
146
+ }
@@ -0,0 +1,82 @@
1
+ import type { VantmetryConfig } from './types';
2
+
3
+ const DEFAULT_INGESTOR_URL = 'https://ingestor.vantmetry.com:4433';
4
+
5
+ export class TransportManager {
6
+ private wtSession: WebTransport | null = null;
7
+ private readonly endpoint: string;
8
+ private readonly wtEndpoint: string;
9
+ private readonly debug: boolean;
10
+
11
+ constructor(config: VantmetryConfig) {
12
+ const base = (config.ingestorUrl ?? DEFAULT_INGESTOR_URL).replace(/\/$/, '');
13
+ this.endpoint = `${base}/api/ingestor/push/tcp?public_key=${config.publicKey}`;
14
+ this.wtEndpoint = `${base}/api/ingestor/push/udp?public_key=${config.publicKey}`;
15
+
16
+ try {
17
+ this.debug = typeof window !== 'undefined' && !!window.localStorage?.getItem('vantmetry_debug');
18
+ } catch {
19
+ this.debug = false;
20
+ }
21
+
22
+ void this.initWT();
23
+ }
24
+
25
+ private async initWT() {
26
+ if (!('WebTransport' in window)) {
27
+ return;
28
+ }
29
+
30
+ // Give browser a short moment to process Alt-Svc from previous visits
31
+ await new Promise((r) => setTimeout(r, 200));
32
+
33
+ // Now attempt WebTransport - browser should know to use HTTP/3
34
+ try {
35
+ this.wtSession = new WebTransport(this.wtEndpoint);
36
+ await this.wtSession.ready;
37
+ if (this.debug) {
38
+ console.log('WT: Connected');
39
+ }
40
+ } catch (err) {
41
+ if (this.debug) {
42
+ console.warn('WT: Failed, falling back to beacon', err);
43
+ }
44
+ this.wtSession = null;
45
+ }
46
+ }
47
+
48
+ public async send(payload: string): Promise<void> {
49
+ if (this.wtSession) {
50
+ try {
51
+ const stream = await this.wtSession.createUnidirectionalStream();
52
+ const writer = stream.getWriter();
53
+ await writer.write(new TextEncoder().encode(payload));
54
+ await writer.close();
55
+ return;
56
+ } catch (err) {
57
+ if (this.debug) {
58
+ console.warn('WT: Failed, falling back to beacon', err);
59
+ }
60
+ this.wtSession = null;
61
+ }
62
+ }
63
+
64
+ if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
65
+ const blob = new Blob([payload], { type: 'application/json' });
66
+ if (navigator.sendBeacon(this.endpoint, blob)) {
67
+ return;
68
+ }
69
+ }
70
+
71
+ fetch(this.endpoint, {
72
+ method: 'POST',
73
+ body: payload,
74
+ keepalive: true,
75
+ headers: { 'Content-Type': 'application/json' },
76
+ }).catch((error) => {
77
+ if (this.debug) {
78
+ console.error('Vantmetry send failed', error);
79
+ }
80
+ });
81
+ }
82
+ }
@@ -0,0 +1,47 @@
1
+ export const LogLevel = {
2
+ ERROR: 'ERROR',
3
+ INFO: 'INFO',
4
+ WARN: 'WARN',
5
+ DEBUG: 'DEBUG',
6
+ } as const;
7
+
8
+ export type VantmetryLogLevel = (typeof LogLevel)[keyof typeof LogLevel];
9
+ export type LogDetails = Record<string, unknown>;
10
+
11
+ export interface VantmetryInstance {
12
+ isReady: boolean;
13
+ error: (message: string | unknown, details?: LogDetails) => void;
14
+ warn: (message: string, details?: LogDetails) => void;
15
+ info: (message: string, details?: LogDetails) => void;
16
+ debug: (message: string, details?: LogDetails) => void;
17
+ flush: () => Promise<void>;
18
+ }
19
+
20
+ declare global {
21
+ interface Window {
22
+ Vantmetry?: VantmetryInstance;
23
+ onVantmetryReady?: (instance: VantmetryInstance) => void;
24
+ }
25
+ }
26
+
27
+ export interface LogPayload {
28
+ message: string | Event | unknown;
29
+ severity: VantmetryLogLevel;
30
+ type?: string;
31
+ stack?: string;
32
+ loc?: string;
33
+ trace_id?: string;
34
+ details?: LogDetails;
35
+ }
36
+
37
+ export interface LogItem extends LogPayload {
38
+ count: number;
39
+ ts: number;
40
+ url: string;
41
+ ua: string;
42
+ }
43
+
44
+ export interface VantmetryConfig {
45
+ publicKey: string;
46
+ ingestorUrl?: string;
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { getInstance } from './core/singleton';
2
+ import type { VantmetryInstance } from './core/types';
3
+
4
+ export { init } from './core/init';
5
+ export { VantmetryTracker } from './core/tracker';
6
+ export { initGlobalListeners } from './core/listeners';
7
+ export type { VantmetryConfig, LogDetails, VantmetryInstance, VantmetryLogLevel } from './core/types';
8
+
9
+ export const logger: VantmetryInstance = {
10
+ get isReady() {
11
+ return getInstance().isReady;
12
+ },
13
+ error(message, details) {
14
+ getInstance().error(message, details);
15
+ },
16
+ warn(message, details) {
17
+ getInstance().warn(message, details);
18
+ },
19
+ info(message, details) {
20
+ getInstance().info(message, details);
21
+ },
22
+ debug(message, details) {
23
+ getInstance().debug(message, details);
24
+ },
25
+ flush() {
26
+ return getInstance().flush();
27
+ },
28
+ };
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import Script from 'next/script';
3
+ import { init, type VantmetryConfig } from '../index';
4
+
5
+ /**
6
+ * Drop this into your Next.js layout or _document to load the tracker via CDN.
7
+ * Uses next/script so Next.js controls placement and loading strategy.
8
+ */
9
+ export function VantmetryScript({ publicKey, ingestorUrl }: VantmetryConfig) {
10
+ return (
11
+ <Script
12
+ strategy="afterInteractive"
13
+ data-public-key={publicKey}
14
+ data-ingestor-url={ingestorUrl}
15
+ src="https://cdn.vantmetry.com/tracker.js"
16
+ />
17
+ );
18
+ }
19
+
20
+ /**
21
+ * SSR-safe wrapper around init(). Use this in _app.tsx, app/layout.tsx client
22
+ * components, or anywhere module-level code may run on the server.
23
+ */
24
+ export function initVantmetry(config: VantmetryConfig): void {
25
+ if (typeof window === 'undefined') {
26
+ return;
27
+ }
28
+ init(config);
29
+ }
@@ -0,0 +1,16 @@
1
+ declare module 'next/script' {
2
+ import type { FC } from 'react';
3
+
4
+ interface ScriptProps {
5
+ id?: string;
6
+ src?: string;
7
+ strategy?: 'beforeInteractive' | 'afterInteractive' | 'lazyOnload' | 'worker';
8
+ onLoad?: () => void;
9
+ onReady?: () => void;
10
+ onError?: (error: Error) => void;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ const Script: FC<ScriptProps>;
15
+ export default Script;
16
+ }
@@ -0,0 +1,68 @@
1
+ import React, { createContext, useContext, useEffect, Component, type ReactNode } from 'react';
2
+ import { init, logger, type VantmetryConfig, type VantmetryInstance } from '../index';
3
+
4
+ const VantmetryContext = createContext<VantmetryInstance | null>(null);
5
+
6
+ interface VantmetryProviderProps extends VantmetryConfig {
7
+ children: ReactNode;
8
+ }
9
+
10
+ /**
11
+ * Initializes Vantmetry and makes the logger available via {@link useLogger}.
12
+ */
13
+ export function VantmetryProvider({ publicKey, ingestorUrl, children }: VantmetryProviderProps) {
14
+ useEffect(() => {
15
+ init({ publicKey, ingestorUrl });
16
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
17
+
18
+ return <VantmetryContext.Provider value={logger}>{children}</VantmetryContext.Provider>;
19
+ }
20
+
21
+ /**
22
+ * Returns the Vantmetry logger. Must be called inside a {@link VantmetryProvider}.
23
+ */
24
+ export function useLogger(): VantmetryInstance {
25
+ const context = useContext(VantmetryContext);
26
+ if (context === null) {
27
+ throw new Error(
28
+ 'useLogger() was called outside of VantmetryProvider. Wrap your app with <VantmetryProvider publicKey="..." /> to use this hook.',
29
+ );
30
+ }
31
+ return context;
32
+ }
33
+
34
+ interface ErrorBoundaryProps {
35
+ children: ReactNode;
36
+ /** Rendered when a render error is caught. Defaults to null (renders nothing). */
37
+ fallback?: ReactNode;
38
+ }
39
+
40
+ interface ErrorBoundaryState {
41
+ hasError: boolean;
42
+ }
43
+
44
+ /**
45
+ * Catches React render errors, logs them via Vantmetry, and renders the fallback.
46
+ * Place around any subtree you want to protect.
47
+ */
48
+ export class VantmetryErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
49
+ constructor(props: ErrorBoundaryProps) {
50
+ super(props);
51
+ this.state = { hasError: false };
52
+ }
53
+
54
+ static getDerivedStateFromError(_error: Error): ErrorBoundaryState {
55
+ return { hasError: true };
56
+ }
57
+
58
+ override componentDidCatch(error: Error, info: React.ErrorInfo): void {
59
+ logger.error(error, { componentStack: info.componentStack ?? undefined });
60
+ }
61
+
62
+ override render(): ReactNode {
63
+ if (this.state.hasError) {
64
+ return this.props.fallback ?? null;
65
+ }
66
+ return this.props.children;
67
+ }
68
+ }