@iaportafolio/nextjs 0.1.1 → 0.3.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 +86 -8
- package/dist/chunk-I3FDJF4L.js +369 -0
- package/dist/client.cjs +365 -93
- package/dist/client.d.cts +191 -0
- package/dist/client.d.ts +191 -0
- package/dist/client.js +25 -5
- package/dist/index.cjs +354 -94
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +25 -5
- package/dist/server.cjs +1 -0
- package/dist/server.d.cts +42 -0
- package/dist/server.d.ts +42 -0
- package/package.json +61 -62
- package/src/browser-core.ts +394 -0
- package/src/browser-react.tsx +53 -0
- package/src/client.ts +63 -131
- package/dist/chunk-TYH3TMKC.js +0 -120
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# @iaportafolio/nextjs
|
|
2
2
|
|
|
3
|
-
SDK para Next.js
|
|
3
|
+
SDK para Next.js — App Router y Pages Router. Cubre ambos lados en un único paquete:
|
|
4
|
+
|
|
5
|
+
- **Server**: captura errores de Route Handlers, Server Actions y SSR. Usa [`@iaportafolio/node`](../node/) como dependencia peer.
|
|
6
|
+
- **Client (browser)**: RUM completo — Web Vitals (LCP/CLS/INP/FCP/TTFB), `window.error`, `unhandledrejection`, clicks/navegaciones como breadcrumbs, React `<FaroErrorBoundary>` y flush garantizado al cerrar el tab. Todo el código vive dentro de este mismo paquete.
|
|
4
7
|
|
|
5
8
|
```bash
|
|
6
9
|
npm install @iaportafolio/nextjs @iaportafolio/node
|
|
@@ -43,22 +46,34 @@ export async function POST(req: Request) {
|
|
|
43
46
|
}
|
|
44
47
|
```
|
|
45
48
|
|
|
46
|
-
## Client-side (
|
|
49
|
+
## Client-side (RUM completo)
|
|
47
50
|
|
|
48
51
|
```tsx
|
|
49
52
|
// app/faro-client.tsx
|
|
50
53
|
'use client';
|
|
51
54
|
import { useEffect } from 'react';
|
|
52
|
-
import {
|
|
55
|
+
import { usePathname, useSearchParams } from 'next/navigation';
|
|
56
|
+
import { initFaroClient, addBreadcrumb, setUser } from '@iaportafolio/nextjs/client';
|
|
53
57
|
|
|
54
58
|
export function FaroClient() {
|
|
59
|
+
const pathname = usePathname();
|
|
60
|
+
const search = useSearchParams();
|
|
61
|
+
|
|
55
62
|
useEffect(() => {
|
|
56
63
|
initFaroClient({
|
|
57
64
|
endpoint: process.env.NEXT_PUBLIC_FARO_ENDPOINT!,
|
|
58
65
|
token: process.env.NEXT_PUBLIC_FARO_TOKEN!,
|
|
59
66
|
service: 'mi-next-app-web',
|
|
67
|
+
// release se autodetecta desde NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA si no la pasas
|
|
60
68
|
});
|
|
61
69
|
}, []);
|
|
70
|
+
|
|
71
|
+
// Breadcrumb explícito por ruta — el SDK ya rastrea pushState/popstate,
|
|
72
|
+
// pero esto da una entrada limpia con el pathname de Next.
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
addBreadcrumb({ category: 'navigation', message: pathname, data: { pathname } });
|
|
75
|
+
}, [pathname, search]);
|
|
76
|
+
|
|
62
77
|
return null;
|
|
63
78
|
}
|
|
64
79
|
|
|
@@ -76,10 +91,67 @@ export default function RootLayout({ children }) {
|
|
|
76
91
|
}
|
|
77
92
|
```
|
|
78
93
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
### Identificar al usuario
|
|
95
|
+
|
|
96
|
+
Tras hacer login:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { setUser } from '@iaportafolio/nextjs/client';
|
|
100
|
+
setUser({ id: user.id, email: user.email });
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Todos los eventos siguientes incluyen `user.id`, `user.email`.
|
|
104
|
+
|
|
105
|
+
### React Error Boundary
|
|
106
|
+
|
|
107
|
+
Envuelve secciones críticas para capturar errores de render sin reventar la app entera:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
'use client';
|
|
111
|
+
import { FaroErrorBoundary } from '@iaportafolio/nextjs/client';
|
|
112
|
+
|
|
113
|
+
export default function CheckoutPage() {
|
|
114
|
+
return (
|
|
115
|
+
<FaroErrorBoundary
|
|
116
|
+
tags={{ module: 'checkout' }}
|
|
117
|
+
fallback={({ error, reset }) => (
|
|
118
|
+
<div>
|
|
119
|
+
<h1>Algo se rompió en el checkout</h1>
|
|
120
|
+
<pre>{error.message}</pre>
|
|
121
|
+
<button onClick={reset}>Reintentar</button>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
>
|
|
125
|
+
<Checkout />
|
|
126
|
+
</FaroErrorBoundary>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Qué captura automáticamente
|
|
132
|
+
|
|
133
|
+
| Cosa | Cómo |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| Errores no atrapados | `window.onerror` y `unhandledrejection` |
|
|
136
|
+
| Errores de React | `<FaroErrorBoundary>` (manual) |
|
|
137
|
+
| **Web Vitals** | LCP, CLS, INP, FCP, TTFB enviados como logs con `metric.name`/`metric.value` |
|
|
138
|
+
| Clicks | Breadcrumb con tag + id + texto del elemento |
|
|
139
|
+
| Navegaciones | Breadcrumb en cada `history.pushState`/`popstate` |
|
|
140
|
+
| Contexto | `browser.url`, `browser.userAgent`, `user.*` (si llamas `setUser`) |
|
|
141
|
+
| Flush al cerrar tab | `navigator.sendBeacon` en `pagehide`/`visibilitychange=hidden` |
|
|
142
|
+
|
|
143
|
+
### Apagar comportamientos
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
initFaroClient({
|
|
147
|
+
endpoint, token, service,
|
|
148
|
+
captureWebVitals: false,
|
|
149
|
+
captureClicks: false,
|
|
150
|
+
captureNavigation: false,
|
|
151
|
+
captureUnhandled: false, // si quieres reportar a mano
|
|
152
|
+
captureConsole: true, // por defecto false (puede meter ruido)
|
|
153
|
+
});
|
|
154
|
+
```
|
|
83
155
|
|
|
84
156
|
## Variables de entorno
|
|
85
157
|
|
|
@@ -88,4 +160,10 @@ El cliente:
|
|
|
88
160
|
| `FARO_ENDPOINT` | solo servidor | URL base |
|
|
89
161
|
| `FARO_TOKEN` | solo servidor | Token de proyecto (privado) |
|
|
90
162
|
| `NEXT_PUBLIC_FARO_ENDPOINT` | cliente + servidor | URL base para el navegador |
|
|
91
|
-
| `NEXT_PUBLIC_FARO_TOKEN` | cliente + servidor | **Mismo token de proyecto.**
|
|
163
|
+
| `NEXT_PUBLIC_FARO_TOKEN` | cliente + servidor | **Mismo token de proyecto.** Queda expuesto en el bundle — es deliberado, igual que en Sentry: el token solo permite ingerir, no leer datos del dashboard. |
|
|
164
|
+
|
|
165
|
+
## Changelog
|
|
166
|
+
|
|
167
|
+
- **v0.3.0**: el RUM del cliente se vuelve a fusionar dentro de `@iaportafolio/nextjs`. Ya no hay que instalar `@iaportafolio/browser` por separado (ese paquete queda deprecado). API pública sin cambios — sigue funcionando lo que estaba en v0.2.x.
|
|
168
|
+
- **v0.2.x**: RUM completo en un paquete aparte `@iaportafolio/browser` (retirado, no usar).
|
|
169
|
+
- **v0.1.x**: captura básica de errores en el cliente.
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// src/browser-core.ts
|
|
2
|
+
var FaroBrowser = class {
|
|
3
|
+
constructor(opts) {
|
|
4
|
+
this.queue = [];
|
|
5
|
+
this.breadcrumbs = [];
|
|
6
|
+
this.user = null;
|
|
7
|
+
this.timer = null;
|
|
8
|
+
this.cleanup = [];
|
|
9
|
+
this.closed = false;
|
|
10
|
+
this.opts = {
|
|
11
|
+
endpoint: opts.endpoint.replace(/\/$/, ""),
|
|
12
|
+
token: opts.token,
|
|
13
|
+
service: opts.service,
|
|
14
|
+
environment: opts.environment,
|
|
15
|
+
release: opts.release,
|
|
16
|
+
attributes: opts.attributes,
|
|
17
|
+
flushIntervalMs: opts.flushIntervalMs ?? 2e3,
|
|
18
|
+
maxBatchSize: opts.maxBatchSize ?? 100,
|
|
19
|
+
maxQueueSize: opts.maxQueueSize ?? 2e3,
|
|
20
|
+
maxBreadcrumbs: opts.maxBreadcrumbs ?? 30,
|
|
21
|
+
captureUnhandled: opts.captureUnhandled ?? true,
|
|
22
|
+
captureConsole: opts.captureConsole ?? false,
|
|
23
|
+
captureWebVitals: opts.captureWebVitals ?? true,
|
|
24
|
+
captureClicks: opts.captureClicks ?? true,
|
|
25
|
+
captureNavigation: opts.captureNavigation ?? true,
|
|
26
|
+
beforeSend: opts.beforeSend
|
|
27
|
+
};
|
|
28
|
+
if (typeof window === "undefined") {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.timer = setInterval(() => void this.flush(), this.opts.flushIntervalMs);
|
|
32
|
+
if (this.opts.captureUnhandled) this.installErrorHandlers();
|
|
33
|
+
if (this.opts.captureConsole) this.installConsoleCapture();
|
|
34
|
+
if (this.opts.captureWebVitals) this.installWebVitals();
|
|
35
|
+
if (this.opts.captureClicks) this.installClickTracking();
|
|
36
|
+
if (this.opts.captureNavigation) this.installNavigationTracking();
|
|
37
|
+
this.installLifecycleHooks();
|
|
38
|
+
}
|
|
39
|
+
setUser(user) {
|
|
40
|
+
this.user = user;
|
|
41
|
+
}
|
|
42
|
+
addBreadcrumb(crumb) {
|
|
43
|
+
if (this.breadcrumbs.length >= this.opts.maxBreadcrumbs) {
|
|
44
|
+
this.breadcrumbs.shift();
|
|
45
|
+
}
|
|
46
|
+
this.breadcrumbs.push({ ...crumb, timestamp: Date.now() });
|
|
47
|
+
}
|
|
48
|
+
log(entry) {
|
|
49
|
+
if (this.closed) return;
|
|
50
|
+
const attrs = this.composeAttributes(entry.attributes);
|
|
51
|
+
const evt = {
|
|
52
|
+
level: entry.level ?? "INFO",
|
|
53
|
+
message: entry.message,
|
|
54
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
55
|
+
attributes: attrs,
|
|
56
|
+
trace_id: entry.trace_id,
|
|
57
|
+
span_id: entry.span_id
|
|
58
|
+
};
|
|
59
|
+
this.enqueue(evt);
|
|
60
|
+
}
|
|
61
|
+
info(message, attrs) {
|
|
62
|
+
this.log({ level: "INFO", message, attributes: attrs });
|
|
63
|
+
}
|
|
64
|
+
warn(message, attrs) {
|
|
65
|
+
this.log({ level: "WARN", message, attributes: attrs });
|
|
66
|
+
}
|
|
67
|
+
error(message, attrs) {
|
|
68
|
+
this.log({ level: "ERROR", message, attributes: attrs });
|
|
69
|
+
}
|
|
70
|
+
captureException(err, ctx) {
|
|
71
|
+
const e = toError(err);
|
|
72
|
+
this.log({
|
|
73
|
+
level: "ERROR",
|
|
74
|
+
message: ctx?.message ?? `${e.name}: ${e.message}`,
|
|
75
|
+
attributes: {
|
|
76
|
+
"exception.type": e.name,
|
|
77
|
+
"exception.message": e.message,
|
|
78
|
+
"exception.stacktrace": e.stack ?? "",
|
|
79
|
+
...ctx?.tags ?? {}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async flush(useBeacon = false) {
|
|
84
|
+
if (this.queue.length === 0) return;
|
|
85
|
+
const batch = this.queue.splice(0, this.opts.maxBatchSize);
|
|
86
|
+
const body = JSON.stringify({ service: this.opts.service, logs: batch });
|
|
87
|
+
const url = `${this.opts.endpoint}/api/v1/ingest/logs`;
|
|
88
|
+
if (useBeacon && typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
89
|
+
const beaconUrl = `${url}?_token=${encodeURIComponent(this.opts.token)}`;
|
|
90
|
+
const ok = navigator.sendBeacon(beaconUrl, new Blob([body], { type: "application/json" }));
|
|
91
|
+
if (ok) return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(url, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
keepalive: true,
|
|
97
|
+
headers: {
|
|
98
|
+
"Authorization": `Bearer ${this.opts.token}`,
|
|
99
|
+
"Content-Type": "application/json"
|
|
100
|
+
},
|
|
101
|
+
body
|
|
102
|
+
});
|
|
103
|
+
if (!res.ok && res.status >= 500) {
|
|
104
|
+
this.queue.unshift(...batch);
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
this.queue.unshift(...batch);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
close() {
|
|
111
|
+
if (this.closed) return;
|
|
112
|
+
this.closed = true;
|
|
113
|
+
if (this.timer) clearInterval(this.timer);
|
|
114
|
+
this.timer = null;
|
|
115
|
+
for (const fn of this.cleanup) fn();
|
|
116
|
+
this.cleanup = [];
|
|
117
|
+
void this.flush(true);
|
|
118
|
+
}
|
|
119
|
+
enqueue(evt) {
|
|
120
|
+
const processed = this.opts.beforeSend ? this.opts.beforeSend(evt) : evt;
|
|
121
|
+
if (!processed) return;
|
|
122
|
+
if (this.queue.length >= this.opts.maxQueueSize) return;
|
|
123
|
+
this.queue.push(processed);
|
|
124
|
+
}
|
|
125
|
+
composeAttributes(extra) {
|
|
126
|
+
const attrs = {};
|
|
127
|
+
if (this.opts.attributes) {
|
|
128
|
+
for (const [k, v] of Object.entries(this.opts.attributes)) attrs[k] = String(v);
|
|
129
|
+
}
|
|
130
|
+
if (this.opts.environment) attrs["deployment.environment"] = this.opts.environment;
|
|
131
|
+
if (this.opts.release) attrs["service.version"] = this.opts.release;
|
|
132
|
+
if (typeof window !== "undefined") {
|
|
133
|
+
attrs["browser.url"] = window.location.href;
|
|
134
|
+
attrs["browser.userAgent"] = navigator.userAgent;
|
|
135
|
+
}
|
|
136
|
+
if (this.user) {
|
|
137
|
+
if (this.user.id) attrs["user.id"] = this.user.id;
|
|
138
|
+
if (this.user.email) attrs["user.email"] = this.user.email;
|
|
139
|
+
if (this.user.username) attrs["user.name"] = this.user.username;
|
|
140
|
+
}
|
|
141
|
+
if (this.breadcrumbs.length > 0) {
|
|
142
|
+
attrs["breadcrumbs"] = JSON.stringify(this.breadcrumbs.slice(-this.opts.maxBreadcrumbs));
|
|
143
|
+
}
|
|
144
|
+
if (extra) {
|
|
145
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
146
|
+
attrs[k] = typeof v === "string" ? v : JSON.stringify(v);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return attrs;
|
|
150
|
+
}
|
|
151
|
+
installErrorHandlers() {
|
|
152
|
+
const onError = (ev) => {
|
|
153
|
+
this.captureException(ev.error ?? ev.message, {
|
|
154
|
+
tags: { origin: "window.error", "source.file": ev.filename ?? "", "source.line": String(ev.lineno ?? 0) }
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
const onRejection = (ev) => {
|
|
158
|
+
this.captureException(ev.reason, { tags: { origin: "unhandledrejection" } });
|
|
159
|
+
};
|
|
160
|
+
window.addEventListener("error", onError);
|
|
161
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
162
|
+
this.cleanup.push(() => window.removeEventListener("error", onError));
|
|
163
|
+
this.cleanup.push(() => window.removeEventListener("unhandledrejection", onRejection));
|
|
164
|
+
}
|
|
165
|
+
installConsoleCapture() {
|
|
166
|
+
const orig = { error: console.error, warn: console.warn };
|
|
167
|
+
console.error = (...args) => {
|
|
168
|
+
this.addBreadcrumb({ category: "console", message: String(args[0] ?? ""), data: { level: "error" } });
|
|
169
|
+
this.log({ level: "ERROR", message: stringifyArgs(args), attributes: { "console.method": "error" } });
|
|
170
|
+
orig.error.apply(console, args);
|
|
171
|
+
};
|
|
172
|
+
console.warn = (...args) => {
|
|
173
|
+
this.addBreadcrumb({ category: "console", message: String(args[0] ?? ""), data: { level: "warn" } });
|
|
174
|
+
orig.warn.apply(console, args);
|
|
175
|
+
};
|
|
176
|
+
this.cleanup.push(() => {
|
|
177
|
+
console.error = orig.error;
|
|
178
|
+
console.warn = orig.warn;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
installWebVitals() {
|
|
182
|
+
void import("web-vitals").then(({ onLCP, onCLS, onINP, onFCP, onTTFB }) => {
|
|
183
|
+
const report = (name) => (m) => {
|
|
184
|
+
this.log({
|
|
185
|
+
level: "INFO",
|
|
186
|
+
message: `web-vital ${name}`,
|
|
187
|
+
attributes: {
|
|
188
|
+
"metric.name": name,
|
|
189
|
+
"metric.value": m.value,
|
|
190
|
+
"metric.rating": m.rating,
|
|
191
|
+
"metric.id": m.id
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
onLCP(report("LCP"));
|
|
196
|
+
onCLS(report("CLS"));
|
|
197
|
+
onINP(report("INP"));
|
|
198
|
+
onFCP(report("FCP"));
|
|
199
|
+
onTTFB(report("TTFB"));
|
|
200
|
+
}).catch(() => {
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
installClickTracking() {
|
|
204
|
+
const onClick = (ev) => {
|
|
205
|
+
const target = ev.target;
|
|
206
|
+
if (!target) return;
|
|
207
|
+
const tag = target.tagName?.toLowerCase() ?? "";
|
|
208
|
+
const id = target.id;
|
|
209
|
+
const text = (target.textContent ?? "").trim().slice(0, 60);
|
|
210
|
+
const data = { tag };
|
|
211
|
+
if (id) data.id = id;
|
|
212
|
+
if (text) data.text = text;
|
|
213
|
+
this.addBreadcrumb({ category: "click", message: `${tag}${id ? "#" + id : ""}`, data });
|
|
214
|
+
};
|
|
215
|
+
window.addEventListener("click", onClick, { capture: true, passive: true });
|
|
216
|
+
this.cleanup.push(() => window.removeEventListener("click", onClick, { capture: true }));
|
|
217
|
+
}
|
|
218
|
+
installNavigationTracking() {
|
|
219
|
+
const log2 = (from, to, method) => {
|
|
220
|
+
if (from === to) return;
|
|
221
|
+
this.addBreadcrumb({ category: "navigation", message: `${from} \u2192 ${to}`, data: { method, to } });
|
|
222
|
+
};
|
|
223
|
+
const origPush = history.pushState;
|
|
224
|
+
const origReplace = history.replaceState;
|
|
225
|
+
history.pushState = function(...args) {
|
|
226
|
+
const from = location.href;
|
|
227
|
+
const ret = origPush.apply(this, args);
|
|
228
|
+
log2(from, location.href, "pushState");
|
|
229
|
+
return ret;
|
|
230
|
+
};
|
|
231
|
+
history.replaceState = function(...args) {
|
|
232
|
+
const from = location.href;
|
|
233
|
+
const ret = origReplace.apply(this, args);
|
|
234
|
+
log2(from, location.href, "replaceState");
|
|
235
|
+
return ret;
|
|
236
|
+
};
|
|
237
|
+
const onPop = () => log2("", location.href, "popstate");
|
|
238
|
+
window.addEventListener("popstate", onPop);
|
|
239
|
+
this.cleanup.push(() => {
|
|
240
|
+
history.pushState = origPush;
|
|
241
|
+
history.replaceState = origReplace;
|
|
242
|
+
window.removeEventListener("popstate", onPop);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
installLifecycleHooks() {
|
|
246
|
+
const onHide = () => {
|
|
247
|
+
if (document.visibilityState === "hidden") void this.flush(true);
|
|
248
|
+
};
|
|
249
|
+
const onPageHide = () => void this.flush(true);
|
|
250
|
+
document.addEventListener("visibilitychange", onHide);
|
|
251
|
+
window.addEventListener("pagehide", onPageHide);
|
|
252
|
+
this.cleanup.push(() => document.removeEventListener("visibilitychange", onHide));
|
|
253
|
+
this.cleanup.push(() => window.removeEventListener("pagehide", onPageHide));
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
function toError(err) {
|
|
257
|
+
if (err instanceof Error) return err;
|
|
258
|
+
if (typeof err === "string") return new Error(err);
|
|
259
|
+
try {
|
|
260
|
+
return new Error(JSON.stringify(err));
|
|
261
|
+
} catch {
|
|
262
|
+
return new Error(String(err));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function stringifyArgs(args) {
|
|
266
|
+
return args.map((a) => typeof a === "string" ? a : a instanceof Error ? a.stack ?? a.message : safeJson(a)).join(" ");
|
|
267
|
+
}
|
|
268
|
+
function safeJson(v) {
|
|
269
|
+
try {
|
|
270
|
+
return JSON.stringify(v);
|
|
271
|
+
} catch {
|
|
272
|
+
return String(v);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
var singleton = null;
|
|
276
|
+
function init(opts) {
|
|
277
|
+
if (singleton) singleton.close();
|
|
278
|
+
singleton = new FaroBrowser(opts);
|
|
279
|
+
return singleton;
|
|
280
|
+
}
|
|
281
|
+
function getClient() {
|
|
282
|
+
if (!singleton) throw new Error("faro: init() must be called before use");
|
|
283
|
+
return singleton;
|
|
284
|
+
}
|
|
285
|
+
function log(entry) {
|
|
286
|
+
getClient().log(entry);
|
|
287
|
+
}
|
|
288
|
+
function info(msg, attrs) {
|
|
289
|
+
getClient().info(msg, attrs);
|
|
290
|
+
}
|
|
291
|
+
function warn(msg, attrs) {
|
|
292
|
+
getClient().warn(msg, attrs);
|
|
293
|
+
}
|
|
294
|
+
function error(msg, attrs) {
|
|
295
|
+
getClient().error(msg, attrs);
|
|
296
|
+
}
|
|
297
|
+
function captureException(err, ctx) {
|
|
298
|
+
getClient().captureException(err, ctx);
|
|
299
|
+
}
|
|
300
|
+
function setUser(user) {
|
|
301
|
+
getClient().setUser(user);
|
|
302
|
+
}
|
|
303
|
+
function addBreadcrumb(crumb) {
|
|
304
|
+
getClient().addBreadcrumb(crumb);
|
|
305
|
+
}
|
|
306
|
+
function flush() {
|
|
307
|
+
return getClient().flush();
|
|
308
|
+
}
|
|
309
|
+
function close() {
|
|
310
|
+
getClient().close();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/browser-react.tsx
|
|
314
|
+
import * as React from "react";
|
|
315
|
+
var FaroErrorBoundary = class extends React.Component {
|
|
316
|
+
constructor() {
|
|
317
|
+
super(...arguments);
|
|
318
|
+
this.state = { error: null };
|
|
319
|
+
this.reset = () => {
|
|
320
|
+
this.setState({ error: null });
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
static getDerivedStateFromError(error2) {
|
|
324
|
+
return { error: error2 };
|
|
325
|
+
}
|
|
326
|
+
componentDidCatch(error2, info2) {
|
|
327
|
+
captureException(error2, {
|
|
328
|
+
tags: {
|
|
329
|
+
origin: "react.error-boundary",
|
|
330
|
+
...this.props.tags ?? {}
|
|
331
|
+
},
|
|
332
|
+
message: error2.message
|
|
333
|
+
});
|
|
334
|
+
this.props.onError?.(error2, info2);
|
|
335
|
+
}
|
|
336
|
+
render() {
|
|
337
|
+
if (this.state.error) {
|
|
338
|
+
const fb = this.props.fallback;
|
|
339
|
+
if (typeof fb === "function") return fb({ error: this.state.error, reset: this.reset });
|
|
340
|
+
if (fb !== void 0) return fb;
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
return this.props.children;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// src/client.ts
|
|
348
|
+
function initFaroClient(opts) {
|
|
349
|
+
let release = opts.release;
|
|
350
|
+
if (!release && typeof process !== "undefined" && process.env) {
|
|
351
|
+
release = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_VERSION || void 0;
|
|
352
|
+
}
|
|
353
|
+
return init({ ...opts, release });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export {
|
|
357
|
+
getClient,
|
|
358
|
+
log,
|
|
359
|
+
info,
|
|
360
|
+
warn,
|
|
361
|
+
error,
|
|
362
|
+
captureException,
|
|
363
|
+
setUser,
|
|
364
|
+
addBreadcrumb,
|
|
365
|
+
flush,
|
|
366
|
+
close,
|
|
367
|
+
FaroErrorBoundary,
|
|
368
|
+
initFaroClient
|
|
369
|
+
};
|