@iaportafolio/nextjs 0.1.1 → 0.2.2
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 +77 -9
- package/dist/chunk-YWAGEOOH.js +39 -0
- package/dist/client.cjs +32 -115
- package/dist/client.d.cts +59 -0
- package/dist/client.d.ts +59 -0
- package/dist/client.js +25 -5
- package/dist/index.cjs +32 -115
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -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 +16 -18
- package/src/client.ts +68 -131
- package/dist/chunk-TYH3TMKC.js +0 -120
package/README.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# @iaportafolio/nextjs
|
|
2
2
|
|
|
3
|
-
SDK para Next.js
|
|
3
|
+
SDK para Next.js — App Router y Pages Router. Cubre ambos lados:
|
|
4
|
+
|
|
5
|
+
- **Server**: captura errores de Route Handlers, Server Actions y SSR. Vive sobre [`@iaportafolio/node`](../node/).
|
|
6
|
+
- **Client (browser)**: RUM completo — Web Vitals (LCP/CLS/INP/FCP/TTFB), `window.error`, `unhandledrejection`, clicks/navegaciones como breadcrumbs, React `<ErrorBoundary>` y flush garantizado al cerrar el tab. Vive sobre [`@iaportafolio/browser`](../browser/).
|
|
4
7
|
|
|
5
8
|
```bash
|
|
6
|
-
npm install @iaportafolio/nextjs @iaportafolio/node
|
|
9
|
+
npm install @iaportafolio/nextjs @iaportafolio/node @iaportafolio/browser
|
|
7
10
|
```
|
|
8
11
|
|
|
9
12
|
## Server-side
|
|
@@ -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,58 @@ 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
|
+
`initFaroClient({ captureWebVitals: false, captureClicks: false, ... })` — todos los flags están en [`@iaportafolio/browser`](../browser/).
|
|
83
146
|
|
|
84
147
|
## Variables de entorno
|
|
85
148
|
|
|
@@ -88,4 +151,9 @@ El cliente:
|
|
|
88
151
|
| `FARO_ENDPOINT` | solo servidor | URL base |
|
|
89
152
|
| `FARO_TOKEN` | solo servidor | Token de proyecto (privado) |
|
|
90
153
|
| `NEXT_PUBLIC_FARO_ENDPOINT` | cliente + servidor | URL base para el navegador |
|
|
91
|
-
| `NEXT_PUBLIC_FARO_TOKEN` | cliente + servidor | **Mismo token de proyecto.**
|
|
154
|
+
| `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. |
|
|
155
|
+
|
|
156
|
+
## Changelog
|
|
157
|
+
|
|
158
|
+
- **v0.2.0**: RUM completo (Web Vitals, breadcrumbs, ErrorBoundary, setUser, navigation tracking). El cliente ahora se apoya en `@iaportafolio/browser`.
|
|
159
|
+
- **v0.1.x**: captura básica de errores en el cliente.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import {
|
|
3
|
+
init as initBrowser
|
|
4
|
+
} from "@iaportafolio/browser";
|
|
5
|
+
import {
|
|
6
|
+
log,
|
|
7
|
+
info,
|
|
8
|
+
warn,
|
|
9
|
+
error,
|
|
10
|
+
captureException,
|
|
11
|
+
setUser,
|
|
12
|
+
addBreadcrumb,
|
|
13
|
+
flush,
|
|
14
|
+
close,
|
|
15
|
+
getClient
|
|
16
|
+
} from "@iaportafolio/browser";
|
|
17
|
+
import { FaroErrorBoundary } from "@iaportafolio/browser/react";
|
|
18
|
+
function initFaroClient(opts) {
|
|
19
|
+
let release = opts.release;
|
|
20
|
+
if (!release && typeof process !== "undefined" && process.env) {
|
|
21
|
+
release = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_VERSION || void 0;
|
|
22
|
+
}
|
|
23
|
+
return initBrowser({ ...opts, release });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
initFaroClient,
|
|
28
|
+
log,
|
|
29
|
+
info,
|
|
30
|
+
warn,
|
|
31
|
+
error,
|
|
32
|
+
captureException,
|
|
33
|
+
setUser,
|
|
34
|
+
addBreadcrumb,
|
|
35
|
+
flush,
|
|
36
|
+
close,
|
|
37
|
+
getClient,
|
|
38
|
+
FaroErrorBoundary
|
|
39
|
+
};
|
package/dist/client.cjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
var __defProp = Object.defineProperty;
|
|
2
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -19,126 +20,42 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
20
|
// src/client.ts
|
|
20
21
|
var client_exports = {};
|
|
21
22
|
__export(client_exports, {
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
FaroErrorBoundary: () => import_react.FaroErrorBoundary,
|
|
24
|
+
addBreadcrumb: () => import_browser2.addBreadcrumb,
|
|
25
|
+
captureException: () => import_browser2.captureException,
|
|
26
|
+
close: () => import_browser2.close,
|
|
27
|
+
error: () => import_browser2.error,
|
|
28
|
+
flush: () => import_browser2.flush,
|
|
29
|
+
getClient: () => import_browser2.getClient,
|
|
30
|
+
info: () => import_browser2.info,
|
|
31
|
+
initFaroClient: () => initFaroClient,
|
|
32
|
+
log: () => import_browser2.log,
|
|
33
|
+
setUser: () => import_browser2.setUser,
|
|
34
|
+
warn: () => import_browser2.warn
|
|
24
35
|
});
|
|
25
36
|
module.exports = __toCommonJS(client_exports);
|
|
26
|
-
var
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const queue = [];
|
|
30
|
-
const maxBatch = opts.maxBatchSize ?? 100;
|
|
31
|
-
let pendingTimer = null;
|
|
32
|
-
function scheduleFlush() {
|
|
33
|
-
if (pendingTimer) return;
|
|
34
|
-
pendingTimer = setTimeout(() => {
|
|
35
|
-
pendingTimer = null;
|
|
36
|
-
void flush();
|
|
37
|
-
}, opts.flushIntervalMs ?? 1500);
|
|
38
|
-
}
|
|
39
|
-
async function flush() {
|
|
40
|
-
if (queue.length === 0) return;
|
|
41
|
-
const batch = queue.splice(0, maxBatch);
|
|
42
|
-
try {
|
|
43
|
-
const body = JSON.stringify({ service: opts.service, logs: batch });
|
|
44
|
-
const ok = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && document.visibilityState === "hidden" ? navigator.sendBeacon(
|
|
45
|
-
`${endpoint}/api/v1/ingest/logs?_token=${encodeURIComponent(opts.token)}`,
|
|
46
|
-
new Blob([body], { type: "application/json" })
|
|
47
|
-
) : false;
|
|
48
|
-
if (ok) return;
|
|
49
|
-
const res = await fetch(`${endpoint}/api/v1/ingest/logs`, {
|
|
50
|
-
method: "POST",
|
|
51
|
-
keepalive: true,
|
|
52
|
-
headers: {
|
|
53
|
-
"Authorization": `Bearer ${opts.token}`,
|
|
54
|
-
"Content-Type": "application/json"
|
|
55
|
-
},
|
|
56
|
-
body
|
|
57
|
-
});
|
|
58
|
-
if (!res.ok) {
|
|
59
|
-
if (res.status >= 500) queue.unshift(...batch);
|
|
60
|
-
}
|
|
61
|
-
} catch (_e) {
|
|
62
|
-
queue.unshift(...batch);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
function enqueue(level, message, attributes) {
|
|
66
|
-
const attrs = {};
|
|
67
|
-
if (opts.attributes) {
|
|
68
|
-
for (const [k, v] of Object.entries(opts.attributes)) attrs[k] = String(v);
|
|
69
|
-
}
|
|
70
|
-
if (opts.environment) attrs["deployment.environment"] = opts.environment;
|
|
71
|
-
if (opts.release) attrs["service.version"] = opts.release;
|
|
72
|
-
if (typeof window !== "undefined") {
|
|
73
|
-
attrs["browser.url"] = window.location.href;
|
|
74
|
-
attrs["browser.userAgent"] = navigator.userAgent;
|
|
75
|
-
}
|
|
76
|
-
if (attributes) {
|
|
77
|
-
for (const [k, v] of Object.entries(attributes)) {
|
|
78
|
-
attrs[k] = typeof v === "string" ? v : JSON.stringify(v);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
queue.push({
|
|
82
|
-
level,
|
|
83
|
-
message,
|
|
84
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
85
|
-
attributes: attrs
|
|
86
|
-
});
|
|
87
|
-
if (queue.length >= maxBatch) void flush();
|
|
88
|
-
else scheduleFlush();
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
log: (e) => enqueue(e.level ?? "INFO", e.message, e.attributes),
|
|
92
|
-
info: (m, a) => enqueue("INFO", m, a),
|
|
93
|
-
warn: (m, a) => enqueue("WARN", m, a),
|
|
94
|
-
error: (m, a) => enqueue("ERROR", m, a),
|
|
95
|
-
captureException: (err, ctx) => {
|
|
96
|
-
const e = err instanceof Error ? err : new Error(typeof err === "string" ? err : JSON.stringify(err));
|
|
97
|
-
enqueue("ERROR", (ctx == null ? void 0 : ctx.message) ?? `${e.name}: ${e.message}`, {
|
|
98
|
-
"exception.type": e.name,
|
|
99
|
-
"exception.message": e.message,
|
|
100
|
-
"exception.stacktrace": e.stack ?? "",
|
|
101
|
-
...(ctx == null ? void 0 : ctx.tags) ?? {}
|
|
102
|
-
});
|
|
103
|
-
},
|
|
104
|
-
flush
|
|
105
|
-
};
|
|
106
|
-
}
|
|
37
|
+
var import_browser = require("@iaportafolio/browser");
|
|
38
|
+
var import_browser2 = require("@iaportafolio/browser");
|
|
39
|
+
var import_react = require("@iaportafolio/browser/react");
|
|
107
40
|
function initFaroClient(opts) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
},
|
|
112
|
-
info() {
|
|
113
|
-
},
|
|
114
|
-
warn() {
|
|
115
|
-
},
|
|
116
|
-
error() {
|
|
117
|
-
},
|
|
118
|
-
captureException() {
|
|
119
|
-
},
|
|
120
|
-
flush: async () => void 0
|
|
121
|
-
};
|
|
41
|
+
let release = opts.release;
|
|
42
|
+
if (!release && typeof process !== "undefined" && process.env) {
|
|
43
|
+
release = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_VERSION || void 0;
|
|
122
44
|
}
|
|
123
|
-
|
|
124
|
-
window.addEventListener("error", (ev) => {
|
|
125
|
-
client == null ? void 0 : client.captureException(ev.error ?? ev.message, { tags: { origin: "window.error" } });
|
|
126
|
-
});
|
|
127
|
-
window.addEventListener("unhandledrejection", (ev) => {
|
|
128
|
-
client == null ? void 0 : client.captureException(ev.reason, { tags: { origin: "unhandledrejection" } });
|
|
129
|
-
});
|
|
130
|
-
document.addEventListener("visibilitychange", () => {
|
|
131
|
-
if (document.visibilityState === "hidden") void (client == null ? void 0 : client.flush());
|
|
132
|
-
});
|
|
133
|
-
window.addEventListener("pagehide", () => void (client == null ? void 0 : client.flush()));
|
|
134
|
-
return client;
|
|
135
|
-
}
|
|
136
|
-
function faroClient() {
|
|
137
|
-
if (!client) throw new Error("Hay que llamar a initFaroClient() antes de usarlo");
|
|
138
|
-
return client;
|
|
45
|
+
return (0, import_browser.init)({ ...opts, release });
|
|
139
46
|
}
|
|
140
47
|
// Annotate the CommonJS export names for ESM import in node:
|
|
141
48
|
0 && (module.exports = {
|
|
142
|
-
|
|
143
|
-
|
|
49
|
+
FaroErrorBoundary,
|
|
50
|
+
addBreadcrumb,
|
|
51
|
+
captureException,
|
|
52
|
+
close,
|
|
53
|
+
error,
|
|
54
|
+
flush,
|
|
55
|
+
getClient,
|
|
56
|
+
info,
|
|
57
|
+
initFaroClient,
|
|
58
|
+
log,
|
|
59
|
+
setUser,
|
|
60
|
+
warn
|
|
144
61
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { FaroBrowserOptions, FaroBrowser } from '@iaportafolio/browser';
|
|
2
|
+
export { Breadcrumb, FaroBrowserOptions, LogEntry, Severity, UserContext, WireEvent, addBreadcrumb, captureException, close, error, flush, getClient, info, log, setUser, warn } from '@iaportafolio/browser';
|
|
3
|
+
export { FaroErrorBoundary, FaroErrorBoundaryProps } from '@iaportafolio/browser/react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Faro para Next.js — lado cliente (corre en el navegador).
|
|
7
|
+
*
|
|
8
|
+
* Es un wrapper fino sobre @iaportafolio/browser. El core (captura de
|
|
9
|
+
* window.error, Web Vitals, breadcrumbs, batching, sendBeacon en pagehide,
|
|
10
|
+
* ErrorBoundary React) vive en el paquete browser. Aquí sólo añadimos:
|
|
11
|
+
* - auto-detección de la release desde env vars típicas de Vercel/Next
|
|
12
|
+
* - re-exports para que sea ergonómico (`import {...} from '@iaportafolio/nextjs/client'`)
|
|
13
|
+
*
|
|
14
|
+
* Uso típico (App Router):
|
|
15
|
+
*
|
|
16
|
+
* // app/faro-client.tsx
|
|
17
|
+
* 'use client';
|
|
18
|
+
* import { useEffect } from 'react';
|
|
19
|
+
* import { usePathname, useSearchParams } from 'next/navigation';
|
|
20
|
+
* import { initFaroClient, addBreadcrumb } from '@iaportafolio/nextjs/client';
|
|
21
|
+
*
|
|
22
|
+
* export function FaroClient() {
|
|
23
|
+
* const pathname = usePathname();
|
|
24
|
+
* const search = useSearchParams();
|
|
25
|
+
*
|
|
26
|
+
* useEffect(() => {
|
|
27
|
+
* initFaroClient({
|
|
28
|
+
* endpoint: process.env.NEXT_PUBLIC_FARO_ENDPOINT!,
|
|
29
|
+
* token: process.env.NEXT_PUBLIC_FARO_TOKEN!,
|
|
30
|
+
* service: 'mi-next-app-web',
|
|
31
|
+
* });
|
|
32
|
+
* }, []);
|
|
33
|
+
*
|
|
34
|
+
* // (opcional) breadcrumb explícito en cada route change con el pathname limpio.
|
|
35
|
+
* // El SDK ya captura pushState, esto sólo es más legible en el dashboard.
|
|
36
|
+
* useEffect(() => {
|
|
37
|
+
* addBreadcrumb({ category: 'navigation', message: pathname, data: { pathname } });
|
|
38
|
+
* }, [pathname, search]);
|
|
39
|
+
*
|
|
40
|
+
* return null;
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* // app/layout.tsx
|
|
44
|
+
* import { FaroClient } from './faro-client';
|
|
45
|
+
* <body><FaroClient />{children}</body>
|
|
46
|
+
*
|
|
47
|
+
* Y opcionalmente envuelve secciones críticas con ErrorBoundary:
|
|
48
|
+
*
|
|
49
|
+
* import { FaroErrorBoundary } from '@iaportafolio/nextjs/client';
|
|
50
|
+
* <FaroErrorBoundary fallback={...}><Checkout /></FaroErrorBoundary>
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Inicializa el SDK browser. Seguro de llamar en SSR — si `typeof window === 'undefined'`
|
|
55
|
+
* el SDK subyacente no hace nada. Llámalo desde `useEffect` en un componente 'use client'.
|
|
56
|
+
*/
|
|
57
|
+
declare function initFaroClient(opts: FaroBrowserOptions): FaroBrowser;
|
|
58
|
+
|
|
59
|
+
export { initFaroClient };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { FaroBrowserOptions, FaroBrowser } from '@iaportafolio/browser';
|
|
2
|
+
export { Breadcrumb, FaroBrowserOptions, LogEntry, Severity, UserContext, WireEvent, addBreadcrumb, captureException, close, error, flush, getClient, info, log, setUser, warn } from '@iaportafolio/browser';
|
|
3
|
+
export { FaroErrorBoundary, FaroErrorBoundaryProps } from '@iaportafolio/browser/react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Faro para Next.js — lado cliente (corre en el navegador).
|
|
7
|
+
*
|
|
8
|
+
* Es un wrapper fino sobre @iaportafolio/browser. El core (captura de
|
|
9
|
+
* window.error, Web Vitals, breadcrumbs, batching, sendBeacon en pagehide,
|
|
10
|
+
* ErrorBoundary React) vive en el paquete browser. Aquí sólo añadimos:
|
|
11
|
+
* - auto-detección de la release desde env vars típicas de Vercel/Next
|
|
12
|
+
* - re-exports para que sea ergonómico (`import {...} from '@iaportafolio/nextjs/client'`)
|
|
13
|
+
*
|
|
14
|
+
* Uso típico (App Router):
|
|
15
|
+
*
|
|
16
|
+
* // app/faro-client.tsx
|
|
17
|
+
* 'use client';
|
|
18
|
+
* import { useEffect } from 'react';
|
|
19
|
+
* import { usePathname, useSearchParams } from 'next/navigation';
|
|
20
|
+
* import { initFaroClient, addBreadcrumb } from '@iaportafolio/nextjs/client';
|
|
21
|
+
*
|
|
22
|
+
* export function FaroClient() {
|
|
23
|
+
* const pathname = usePathname();
|
|
24
|
+
* const search = useSearchParams();
|
|
25
|
+
*
|
|
26
|
+
* useEffect(() => {
|
|
27
|
+
* initFaroClient({
|
|
28
|
+
* endpoint: process.env.NEXT_PUBLIC_FARO_ENDPOINT!,
|
|
29
|
+
* token: process.env.NEXT_PUBLIC_FARO_TOKEN!,
|
|
30
|
+
* service: 'mi-next-app-web',
|
|
31
|
+
* });
|
|
32
|
+
* }, []);
|
|
33
|
+
*
|
|
34
|
+
* // (opcional) breadcrumb explícito en cada route change con el pathname limpio.
|
|
35
|
+
* // El SDK ya captura pushState, esto sólo es más legible en el dashboard.
|
|
36
|
+
* useEffect(() => {
|
|
37
|
+
* addBreadcrumb({ category: 'navigation', message: pathname, data: { pathname } });
|
|
38
|
+
* }, [pathname, search]);
|
|
39
|
+
*
|
|
40
|
+
* return null;
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* // app/layout.tsx
|
|
44
|
+
* import { FaroClient } from './faro-client';
|
|
45
|
+
* <body><FaroClient />{children}</body>
|
|
46
|
+
*
|
|
47
|
+
* Y opcionalmente envuelve secciones críticas con ErrorBoundary:
|
|
48
|
+
*
|
|
49
|
+
* import { FaroErrorBoundary } from '@iaportafolio/nextjs/client';
|
|
50
|
+
* <FaroErrorBoundary fallback={...}><Checkout /></FaroErrorBoundary>
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Inicializa el SDK browser. Seguro de llamar en SSR — si `typeof window === 'undefined'`
|
|
55
|
+
* el SDK subyacente no hace nada. Llámalo desde `useEffect` en un componente 'use client'.
|
|
56
|
+
*/
|
|
57
|
+
declare function initFaroClient(opts: FaroBrowserOptions): FaroBrowser;
|
|
58
|
+
|
|
59
|
+
export { initFaroClient };
|
package/dist/client.js
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
FaroErrorBoundary,
|
|
3
|
+
addBreadcrumb,
|
|
4
|
+
captureException,
|
|
5
|
+
close,
|
|
6
|
+
error,
|
|
7
|
+
flush,
|
|
8
|
+
getClient,
|
|
9
|
+
info,
|
|
10
|
+
initFaroClient,
|
|
11
|
+
log,
|
|
12
|
+
setUser,
|
|
13
|
+
warn
|
|
14
|
+
} from "./chunk-YWAGEOOH.js";
|
|
5
15
|
export {
|
|
6
|
-
|
|
7
|
-
|
|
16
|
+
FaroErrorBoundary,
|
|
17
|
+
addBreadcrumb,
|
|
18
|
+
captureException,
|
|
19
|
+
close,
|
|
20
|
+
error,
|
|
21
|
+
flush,
|
|
22
|
+
getClient,
|
|
23
|
+
info,
|
|
24
|
+
initFaroClient,
|
|
25
|
+
log,
|
|
26
|
+
setUser,
|
|
27
|
+
warn
|
|
8
28
|
};
|
package/dist/index.cjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -29,11 +30,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
30
|
// src/index.ts
|
|
30
31
|
var index_exports = {};
|
|
31
32
|
__export(index_exports, {
|
|
33
|
+
FaroErrorBoundary: () => import_react.FaroErrorBoundary,
|
|
34
|
+
addBreadcrumb: () => import_browser2.addBreadcrumb,
|
|
35
|
+
captureException: () => import_browser2.captureException,
|
|
32
36
|
captureRequestError: () => captureRequestError,
|
|
37
|
+
close: () => import_browser2.close,
|
|
38
|
+
error: () => import_browser2.error,
|
|
33
39
|
faro: () => faro,
|
|
34
|
-
|
|
40
|
+
flush: () => import_browser2.flush,
|
|
41
|
+
getClient: () => import_browser2.getClient,
|
|
42
|
+
info: () => import_browser2.info,
|
|
35
43
|
initFaroClient: () => initFaroClient,
|
|
36
|
-
|
|
44
|
+
log: () => import_browser2.log,
|
|
45
|
+
registerFaro: () => registerFaro,
|
|
46
|
+
setUser: () => import_browser2.setUser,
|
|
47
|
+
warn: () => import_browser2.warn
|
|
37
48
|
});
|
|
38
49
|
module.exports = __toCommonJS(index_exports);
|
|
39
50
|
|
|
@@ -60,125 +71,31 @@ function captureRequestError(err, request) {
|
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
// src/client.ts
|
|
63
|
-
var
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const queue = [];
|
|
67
|
-
const maxBatch = opts.maxBatchSize ?? 100;
|
|
68
|
-
let pendingTimer = null;
|
|
69
|
-
function scheduleFlush() {
|
|
70
|
-
if (pendingTimer) return;
|
|
71
|
-
pendingTimer = setTimeout(() => {
|
|
72
|
-
pendingTimer = null;
|
|
73
|
-
void flush();
|
|
74
|
-
}, opts.flushIntervalMs ?? 1500);
|
|
75
|
-
}
|
|
76
|
-
async function flush() {
|
|
77
|
-
if (queue.length === 0) return;
|
|
78
|
-
const batch = queue.splice(0, maxBatch);
|
|
79
|
-
try {
|
|
80
|
-
const body = JSON.stringify({ service: opts.service, logs: batch });
|
|
81
|
-
const ok = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && document.visibilityState === "hidden" ? navigator.sendBeacon(
|
|
82
|
-
`${endpoint}/api/v1/ingest/logs?_token=${encodeURIComponent(opts.token)}`,
|
|
83
|
-
new Blob([body], { type: "application/json" })
|
|
84
|
-
) : false;
|
|
85
|
-
if (ok) return;
|
|
86
|
-
const res = await fetch(`${endpoint}/api/v1/ingest/logs`, {
|
|
87
|
-
method: "POST",
|
|
88
|
-
keepalive: true,
|
|
89
|
-
headers: {
|
|
90
|
-
"Authorization": `Bearer ${opts.token}`,
|
|
91
|
-
"Content-Type": "application/json"
|
|
92
|
-
},
|
|
93
|
-
body
|
|
94
|
-
});
|
|
95
|
-
if (!res.ok) {
|
|
96
|
-
if (res.status >= 500) queue.unshift(...batch);
|
|
97
|
-
}
|
|
98
|
-
} catch (_e) {
|
|
99
|
-
queue.unshift(...batch);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
function enqueue(level, message, attributes) {
|
|
103
|
-
const attrs = {};
|
|
104
|
-
if (opts.attributes) {
|
|
105
|
-
for (const [k, v] of Object.entries(opts.attributes)) attrs[k] = String(v);
|
|
106
|
-
}
|
|
107
|
-
if (opts.environment) attrs["deployment.environment"] = opts.environment;
|
|
108
|
-
if (opts.release) attrs["service.version"] = opts.release;
|
|
109
|
-
if (typeof window !== "undefined") {
|
|
110
|
-
attrs["browser.url"] = window.location.href;
|
|
111
|
-
attrs["browser.userAgent"] = navigator.userAgent;
|
|
112
|
-
}
|
|
113
|
-
if (attributes) {
|
|
114
|
-
for (const [k, v] of Object.entries(attributes)) {
|
|
115
|
-
attrs[k] = typeof v === "string" ? v : JSON.stringify(v);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
queue.push({
|
|
119
|
-
level,
|
|
120
|
-
message,
|
|
121
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
122
|
-
attributes: attrs
|
|
123
|
-
});
|
|
124
|
-
if (queue.length >= maxBatch) void flush();
|
|
125
|
-
else scheduleFlush();
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
log: (e) => enqueue(e.level ?? "INFO", e.message, e.attributes),
|
|
129
|
-
info: (m, a) => enqueue("INFO", m, a),
|
|
130
|
-
warn: (m, a) => enqueue("WARN", m, a),
|
|
131
|
-
error: (m, a) => enqueue("ERROR", m, a),
|
|
132
|
-
captureException: (err, ctx) => {
|
|
133
|
-
const e = err instanceof Error ? err : new Error(typeof err === "string" ? err : JSON.stringify(err));
|
|
134
|
-
enqueue("ERROR", (ctx == null ? void 0 : ctx.message) ?? `${e.name}: ${e.message}`, {
|
|
135
|
-
"exception.type": e.name,
|
|
136
|
-
"exception.message": e.message,
|
|
137
|
-
"exception.stacktrace": e.stack ?? "",
|
|
138
|
-
...(ctx == null ? void 0 : ctx.tags) ?? {}
|
|
139
|
-
});
|
|
140
|
-
},
|
|
141
|
-
flush
|
|
142
|
-
};
|
|
143
|
-
}
|
|
74
|
+
var import_browser = require("@iaportafolio/browser");
|
|
75
|
+
var import_browser2 = require("@iaportafolio/browser");
|
|
76
|
+
var import_react = require("@iaportafolio/browser/react");
|
|
144
77
|
function initFaroClient(opts) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
},
|
|
149
|
-
info() {
|
|
150
|
-
},
|
|
151
|
-
warn() {
|
|
152
|
-
},
|
|
153
|
-
error() {
|
|
154
|
-
},
|
|
155
|
-
captureException() {
|
|
156
|
-
},
|
|
157
|
-
flush: async () => void 0
|
|
158
|
-
};
|
|
78
|
+
let release = opts.release;
|
|
79
|
+
if (!release && typeof process !== "undefined" && process.env) {
|
|
80
|
+
release = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_VERSION || void 0;
|
|
159
81
|
}
|
|
160
|
-
|
|
161
|
-
window.addEventListener("error", (ev) => {
|
|
162
|
-
client == null ? void 0 : client.captureException(ev.error ?? ev.message, { tags: { origin: "window.error" } });
|
|
163
|
-
});
|
|
164
|
-
window.addEventListener("unhandledrejection", (ev) => {
|
|
165
|
-
client == null ? void 0 : client.captureException(ev.reason, { tags: { origin: "unhandledrejection" } });
|
|
166
|
-
});
|
|
167
|
-
document.addEventListener("visibilitychange", () => {
|
|
168
|
-
if (document.visibilityState === "hidden") void (client == null ? void 0 : client.flush());
|
|
169
|
-
});
|
|
170
|
-
window.addEventListener("pagehide", () => void (client == null ? void 0 : client.flush()));
|
|
171
|
-
return client;
|
|
172
|
-
}
|
|
173
|
-
function faroClient() {
|
|
174
|
-
if (!client) throw new Error("Hay que llamar a initFaroClient() antes de usarlo");
|
|
175
|
-
return client;
|
|
82
|
+
return (0, import_browser.init)({ ...opts, release });
|
|
176
83
|
}
|
|
177
84
|
// Annotate the CommonJS export names for ESM import in node:
|
|
178
85
|
0 && (module.exports = {
|
|
86
|
+
FaroErrorBoundary,
|
|
87
|
+
addBreadcrumb,
|
|
88
|
+
captureException,
|
|
179
89
|
captureRequestError,
|
|
90
|
+
close,
|
|
91
|
+
error,
|
|
180
92
|
faro,
|
|
181
|
-
|
|
93
|
+
flush,
|
|
94
|
+
getClient,
|
|
95
|
+
info,
|
|
182
96
|
initFaroClient,
|
|
183
|
-
|
|
97
|
+
log,
|
|
98
|
+
registerFaro,
|
|
99
|
+
setUser,
|
|
100
|
+
warn
|
|
184
101
|
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ServerOptions, captureRequestError, registerFaro } from './server.cjs';
|
|
2
|
+
export { initFaroClient } from './client.cjs';
|
|
3
|
+
export { Breadcrumb, FaroBrowserOptions, LogEntry, Severity, UserContext, WireEvent, addBreadcrumb, captureException, close, error, flush, getClient, info, log, setUser, warn } from '@iaportafolio/browser';
|
|
4
|
+
export { FaroErrorBoundary, FaroErrorBoundaryProps } from '@iaportafolio/browser/react';
|
|
5
|
+
import * as faro from '@iaportafolio/node';
|
|
6
|
+
export { faro };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ServerOptions, captureRequestError, registerFaro } from './server.js';
|
|
2
|
+
export { initFaroClient } from './client.js';
|
|
3
|
+
export { Breadcrumb, FaroBrowserOptions, LogEntry, Severity, UserContext, WireEvent, addBreadcrumb, captureException, close, error, flush, getClient, info, log, setUser, warn } from '@iaportafolio/browser';
|
|
4
|
+
export { FaroErrorBoundary, FaroErrorBoundaryProps } from '@iaportafolio/browser/react';
|
|
5
|
+
import * as faro from '@iaportafolio/node';
|
|
6
|
+
export { faro };
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
FaroErrorBoundary,
|
|
3
|
+
addBreadcrumb,
|
|
4
|
+
captureException,
|
|
5
|
+
close,
|
|
6
|
+
error,
|
|
7
|
+
flush,
|
|
8
|
+
getClient,
|
|
9
|
+
info,
|
|
10
|
+
initFaroClient,
|
|
11
|
+
log,
|
|
12
|
+
setUser,
|
|
13
|
+
warn
|
|
14
|
+
} from "./chunk-YWAGEOOH.js";
|
|
5
15
|
import {
|
|
6
16
|
captureRequestError,
|
|
7
17
|
faro,
|
|
8
18
|
registerFaro
|
|
9
19
|
} from "./chunk-LFDO56A5.js";
|
|
10
20
|
export {
|
|
21
|
+
FaroErrorBoundary,
|
|
22
|
+
addBreadcrumb,
|
|
23
|
+
captureException,
|
|
11
24
|
captureRequestError,
|
|
25
|
+
close,
|
|
26
|
+
error,
|
|
12
27
|
faro,
|
|
13
|
-
|
|
28
|
+
flush,
|
|
29
|
+
getClient,
|
|
30
|
+
info,
|
|
14
31
|
initFaroClient,
|
|
15
|
-
|
|
32
|
+
log,
|
|
33
|
+
registerFaro,
|
|
34
|
+
setUser,
|
|
35
|
+
warn
|
|
16
36
|
};
|
package/dist/server.cjs
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as faro from '@iaportafolio/node';
|
|
2
|
+
export { faro };
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Faro para Next.js — lado servidor (instrumentation.ts).
|
|
6
|
+
*
|
|
7
|
+
* Uso (Next.js 13.4+ App Router):
|
|
8
|
+
*
|
|
9
|
+
* // next.config.mjs
|
|
10
|
+
* export default { experimental: { instrumentationHook: true } }; // Next 14- only
|
|
11
|
+
*
|
|
12
|
+
* // instrumentation.ts
|
|
13
|
+
* export async function register() {
|
|
14
|
+
* const { registerFaro } = await import('@iaportafolio/nextjs/server');
|
|
15
|
+
* registerFaro({
|
|
16
|
+
* endpoint: process.env.FARO_ENDPOINT!,
|
|
17
|
+
* token: process.env.FARO_TOKEN!,
|
|
18
|
+
* service: 'mi-next-app',
|
|
19
|
+
* environment: process.env.NODE_ENV,
|
|
20
|
+
* release: process.env.VERCEL_GIT_COMMIT_SHA,
|
|
21
|
+
* });
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* export async function onRequestError(err: unknown, request: { path: string; method: string }) {
|
|
25
|
+
* const { captureRequestError } = await import('@iaportafolio/nextjs/server');
|
|
26
|
+
* captureRequestError(err, request);
|
|
27
|
+
* }
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
type ServerOptions = faro.FaroOptions;
|
|
31
|
+
declare function registerFaro(opts: ServerOptions): void;
|
|
32
|
+
/**
|
|
33
|
+
* Engánchalo al hook de instrumentación `onRequestError` de Next.js (Next 15+).
|
|
34
|
+
* Reporta el error con el contexto de la request adjunto como tags.
|
|
35
|
+
*/
|
|
36
|
+
declare function captureRequestError(err: unknown, request: {
|
|
37
|
+
path?: string;
|
|
38
|
+
method?: string;
|
|
39
|
+
routerKind?: string;
|
|
40
|
+
}): void;
|
|
41
|
+
|
|
42
|
+
export { type ServerOptions, captureRequestError, registerFaro };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as faro from '@iaportafolio/node';
|
|
2
|
+
export { faro };
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Faro para Next.js — lado servidor (instrumentation.ts).
|
|
6
|
+
*
|
|
7
|
+
* Uso (Next.js 13.4+ App Router):
|
|
8
|
+
*
|
|
9
|
+
* // next.config.mjs
|
|
10
|
+
* export default { experimental: { instrumentationHook: true } }; // Next 14- only
|
|
11
|
+
*
|
|
12
|
+
* // instrumentation.ts
|
|
13
|
+
* export async function register() {
|
|
14
|
+
* const { registerFaro } = await import('@iaportafolio/nextjs/server');
|
|
15
|
+
* registerFaro({
|
|
16
|
+
* endpoint: process.env.FARO_ENDPOINT!,
|
|
17
|
+
* token: process.env.FARO_TOKEN!,
|
|
18
|
+
* service: 'mi-next-app',
|
|
19
|
+
* environment: process.env.NODE_ENV,
|
|
20
|
+
* release: process.env.VERCEL_GIT_COMMIT_SHA,
|
|
21
|
+
* });
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* export async function onRequestError(err: unknown, request: { path: string; method: string }) {
|
|
25
|
+
* const { captureRequestError } = await import('@iaportafolio/nextjs/server');
|
|
26
|
+
* captureRequestError(err, request);
|
|
27
|
+
* }
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
type ServerOptions = faro.FaroOptions;
|
|
31
|
+
declare function registerFaro(opts: ServerOptions): void;
|
|
32
|
+
/**
|
|
33
|
+
* Engánchalo al hook de instrumentación `onRequestError` de Next.js (Next 15+).
|
|
34
|
+
* Reporta el error con el contexto de la request adjunto como tags.
|
|
35
|
+
*/
|
|
36
|
+
declare function captureRequestError(err: unknown, request: {
|
|
37
|
+
path?: string;
|
|
38
|
+
method?: string;
|
|
39
|
+
routerKind?: string;
|
|
40
|
+
}): void;
|
|
41
|
+
|
|
42
|
+
export { type ServerOptions, captureRequestError, registerFaro };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iaportafolio/nextjs",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Faro SDK for Next.js (App Router + Pages Router)",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Faro SDK for Next.js (App Router + Pages Router): RUM, Web Vitals, error capture, ErrorBoundary",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.cjs",
|
|
@@ -36,27 +36,25 @@
|
|
|
36
36
|
},
|
|
37
37
|
"homepage": "https://github.com/IA-Portafolio/faro/tree/main/sdks/nextjs#readme",
|
|
38
38
|
"bugs": "https://github.com/IA-Portafolio/faro/issues",
|
|
39
|
-
"keywords": [
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"observability",
|
|
43
|
-
"logging",
|
|
44
|
-
"telemetry"
|
|
45
|
-
],
|
|
46
|
-
"publishConfig": {
|
|
47
|
-
"access": "public",
|
|
48
|
-
"provenance": true
|
|
49
|
-
},
|
|
50
|
-
"scripts": {
|
|
51
|
-
"build": "tsup src/index.ts src/client.ts src/server.ts --format cjs,esm --dts --clean"
|
|
52
|
-
},
|
|
39
|
+
"keywords": ["faro", "nextjs", "observability", "logging", "telemetry"],
|
|
40
|
+
"publishConfig": { "access": "public" },
|
|
41
|
+
"scripts": { "build": "tsup src/index.ts src/client.ts src/server.ts --format cjs,esm --dts --clean --external react --external next --external @iaportafolio/node --external @iaportafolio/browser" },
|
|
53
42
|
"peerDependencies": {
|
|
54
43
|
"@iaportafolio/node": "^0.1.0",
|
|
55
|
-
"
|
|
44
|
+
"@iaportafolio/browser": "^0.1.0",
|
|
45
|
+
"next": ">=13",
|
|
46
|
+
"react": ">=16.8.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"react": { "optional": true }
|
|
56
50
|
},
|
|
57
51
|
"devDependencies": {
|
|
58
52
|
"tsup": "^8.0.0",
|
|
59
53
|
"typescript": "^5.4.0",
|
|
60
|
-
"@types/node": "^20.0.0"
|
|
54
|
+
"@types/node": "^20.0.0",
|
|
55
|
+
"@types/react": "^18.0.0",
|
|
56
|
+
"react": "^18.0.0",
|
|
57
|
+
"@iaportafolio/node": "^0.1.0",
|
|
58
|
+
"@iaportafolio/browser": "^0.1.0"
|
|
61
59
|
}
|
|
62
60
|
}
|
package/src/client.ts
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Faro para Next.js — lado cliente (corre en el navegador).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Es un wrapper fino sobre @iaportafolio/browser. El core (captura de
|
|
5
|
+
* window.error, Web Vitals, breadcrumbs, batching, sendBeacon en pagehide,
|
|
6
|
+
* ErrorBoundary React) vive en el paquete browser. Aquí sólo añadimos:
|
|
7
|
+
* - auto-detección de la release desde env vars típicas de Vercel/Next
|
|
8
|
+
* - re-exports para que sea ergonómico (`import {...} from '@iaportafolio/nextjs/client'`)
|
|
9
|
+
*
|
|
10
|
+
* Uso típico (App Router):
|
|
5
11
|
*
|
|
6
12
|
* // app/faro-client.tsx
|
|
7
13
|
* 'use client';
|
|
8
14
|
* import { useEffect } from 'react';
|
|
9
|
-
* import {
|
|
15
|
+
* import { usePathname, useSearchParams } from 'next/navigation';
|
|
16
|
+
* import { initFaroClient, addBreadcrumb } from '@iaportafolio/nextjs/client';
|
|
10
17
|
*
|
|
11
18
|
* export function FaroClient() {
|
|
19
|
+
* const pathname = usePathname();
|
|
20
|
+
* const search = useSearchParams();
|
|
21
|
+
*
|
|
12
22
|
* useEffect(() => {
|
|
13
23
|
* initFaroClient({
|
|
14
24
|
* endpoint: process.env.NEXT_PUBLIC_FARO_ENDPOINT!,
|
|
@@ -16,143 +26,70 @@
|
|
|
16
26
|
* service: 'mi-next-app-web',
|
|
17
27
|
* });
|
|
18
28
|
* }, []);
|
|
29
|
+
*
|
|
30
|
+
* // (opcional) breadcrumb explícito en cada route change con el pathname limpio.
|
|
31
|
+
* // El SDK ya captura pushState, esto sólo es más legible en el dashboard.
|
|
32
|
+
* useEffect(() => {
|
|
33
|
+
* addBreadcrumb({ category: 'navigation', message: pathname, data: { pathname } });
|
|
34
|
+
* }, [pathname, search]);
|
|
35
|
+
*
|
|
19
36
|
* return null;
|
|
20
37
|
* }
|
|
21
38
|
*
|
|
22
|
-
* //
|
|
39
|
+
* // app/layout.tsx
|
|
40
|
+
* import { FaroClient } from './faro-client';
|
|
41
|
+
* <body><FaroClient />{children}</body>
|
|
42
|
+
*
|
|
43
|
+
* Y opcionalmente envuelve secciones críticas con ErrorBoundary:
|
|
44
|
+
*
|
|
45
|
+
* import { FaroErrorBoundary } from '@iaportafolio/nextjs/client';
|
|
46
|
+
* <FaroErrorBoundary fallback={...}><Checkout /></FaroErrorBoundary>
|
|
23
47
|
*/
|
|
24
48
|
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
log(entry: { level?: string; message: string; attributes?: Record<string, unknown> }): void;
|
|
31
|
-
info(msg: string, attrs?: Record<string, unknown>): void;
|
|
32
|
-
warn(msg: string, attrs?: Record<string, unknown>): void;
|
|
33
|
-
error(msg: string, attrs?: Record<string, unknown>): void;
|
|
34
|
-
captureException(err: unknown, ctx?: { tags?: Record<string, string>; message?: string }): void;
|
|
35
|
-
flush(): Promise<void>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function newClient(opts: FaroOptions): BrowserClient {
|
|
39
|
-
const endpoint = opts.endpoint.replace(/\/$/, '');
|
|
40
|
-
const queue: unknown[] = [];
|
|
41
|
-
const maxBatch = opts.maxBatchSize ?? 100;
|
|
42
|
-
let pendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
49
|
+
import {
|
|
50
|
+
init as initBrowser,
|
|
51
|
+
type FaroBrowser,
|
|
52
|
+
type FaroBrowserOptions,
|
|
53
|
+
} from '@iaportafolio/browser';
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
async function flush(): Promise<void> {
|
|
53
|
-
if (queue.length === 0) return;
|
|
54
|
-
const batch = queue.splice(0, maxBatch);
|
|
55
|
-
try {
|
|
56
|
-
// sendBeacon maneja `pagehide` mejor que fetch.
|
|
57
|
-
const body = JSON.stringify({ service: opts.service, logs: batch });
|
|
58
|
-
const ok =
|
|
59
|
-
typeof navigator !== 'undefined' &&
|
|
60
|
-
typeof navigator.sendBeacon === 'function' &&
|
|
61
|
-
document.visibilityState === 'hidden'
|
|
62
|
-
? navigator.sendBeacon(
|
|
63
|
-
`${endpoint}/api/v1/ingest/logs?_token=${encodeURIComponent(opts.token)}`,
|
|
64
|
-
new Blob([body], { type: 'application/json' }),
|
|
65
|
-
)
|
|
66
|
-
: false;
|
|
67
|
-
if (ok) return;
|
|
68
|
-
const res = await fetch(`${endpoint}/api/v1/ingest/logs`, {
|
|
69
|
-
method: 'POST',
|
|
70
|
-
keepalive: true,
|
|
71
|
-
headers: {
|
|
72
|
-
'Authorization': `Bearer ${opts.token}`,
|
|
73
|
-
'Content-Type': 'application/json',
|
|
74
|
-
},
|
|
75
|
-
body,
|
|
76
|
-
});
|
|
77
|
-
if (!res.ok) {
|
|
78
|
-
// Re-encola en best-effort si no es un 4xx (ahí la request es irrecuperable).
|
|
79
|
-
if (res.status >= 500) queue.unshift(...batch);
|
|
80
|
-
}
|
|
81
|
-
} catch (_e) {
|
|
82
|
-
queue.unshift(...batch);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
55
|
+
export type {
|
|
56
|
+
FaroBrowserOptions,
|
|
57
|
+
UserContext,
|
|
58
|
+
Breadcrumb,
|
|
59
|
+
LogEntry,
|
|
60
|
+
Severity,
|
|
61
|
+
WireEvent,
|
|
62
|
+
} from '@iaportafolio/browser';
|
|
85
63
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
for (const [k, v] of Object.entries(attributes)) {
|
|
99
|
-
attrs[k] = typeof v === 'string' ? v : JSON.stringify(v);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
queue.push({
|
|
103
|
-
level,
|
|
104
|
-
message,
|
|
105
|
-
timestamp: new Date().toISOString(),
|
|
106
|
-
attributes: attrs,
|
|
107
|
-
});
|
|
108
|
-
if (queue.length >= maxBatch) void flush();
|
|
109
|
-
else scheduleFlush();
|
|
110
|
-
}
|
|
64
|
+
export {
|
|
65
|
+
log,
|
|
66
|
+
info,
|
|
67
|
+
warn,
|
|
68
|
+
error,
|
|
69
|
+
captureException,
|
|
70
|
+
setUser,
|
|
71
|
+
addBreadcrumb,
|
|
72
|
+
flush,
|
|
73
|
+
close,
|
|
74
|
+
getClient,
|
|
75
|
+
} from '@iaportafolio/browser';
|
|
111
76
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
info: (m, a) => enqueue('INFO', m, a),
|
|
115
|
-
warn: (m, a) => enqueue('WARN', m, a),
|
|
116
|
-
error: (m, a) => enqueue('ERROR', m, a),
|
|
117
|
-
captureException: (err, ctx) => {
|
|
118
|
-
const e = err instanceof Error ? err : new Error(typeof err === 'string' ? err : JSON.stringify(err));
|
|
119
|
-
enqueue('ERROR', ctx?.message ?? `${e.name}: ${e.message}`, {
|
|
120
|
-
'exception.type': e.name,
|
|
121
|
-
'exception.message': e.message,
|
|
122
|
-
'exception.stacktrace': e.stack ?? '',
|
|
123
|
-
...(ctx?.tags ?? {}),
|
|
124
|
-
});
|
|
125
|
-
},
|
|
126
|
-
flush,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
77
|
+
export { FaroErrorBoundary } from '@iaportafolio/browser/react';
|
|
78
|
+
export type { FaroErrorBoundaryProps } from '@iaportafolio/browser/react';
|
|
129
79
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Inicializa el SDK browser. Seguro de llamar en SSR — si `typeof window === 'undefined'`
|
|
82
|
+
* el SDK subyacente no hace nada. Llámalo desde `useEffect` en un componente 'use client'.
|
|
83
|
+
*/
|
|
84
|
+
export function initFaroClient(opts: FaroBrowserOptions): FaroBrowser {
|
|
85
|
+
// Auto-detect release desde env vars típicas de Vercel/Next si no se pasó explícita.
|
|
86
|
+
let release = opts.release;
|
|
87
|
+
if (!release && typeof process !== 'undefined' && process.env) {
|
|
88
|
+
release =
|
|
89
|
+
process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA ||
|
|
90
|
+
process.env.NEXT_PUBLIC_GIT_COMMIT_SHA ||
|
|
91
|
+
process.env.NEXT_PUBLIC_VERSION ||
|
|
92
|
+
undefined;
|
|
136
93
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// Captura errores no manejados en el navegador.
|
|
140
|
-
window.addEventListener('error', (ev) => {
|
|
141
|
-
client?.captureException(ev.error ?? ev.message, { tags: { origin: 'window.error' } });
|
|
142
|
-
});
|
|
143
|
-
window.addEventListener('unhandledrejection', (ev) => {
|
|
144
|
-
client?.captureException(ev.reason, { tags: { origin: 'unhandledrejection' } });
|
|
145
|
-
});
|
|
146
|
-
// Hace flush cuando la pestaña se oculta.
|
|
147
|
-
document.addEventListener('visibilitychange', () => {
|
|
148
|
-
if (document.visibilityState === 'hidden') void client?.flush();
|
|
149
|
-
});
|
|
150
|
-
window.addEventListener('pagehide', () => void client?.flush());
|
|
151
|
-
|
|
152
|
-
return client;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function faroClient(): BrowserClient {
|
|
156
|
-
if (!client) throw new Error('Hay que llamar a initFaroClient() antes de usarlo');
|
|
157
|
-
return client;
|
|
94
|
+
return initBrowser({ ...opts, release });
|
|
158
95
|
}
|
package/dist/chunk-TYH3TMKC.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
// src/client.ts
|
|
2
|
-
var client = null;
|
|
3
|
-
function newClient(opts) {
|
|
4
|
-
const endpoint = opts.endpoint.replace(/\/$/, "");
|
|
5
|
-
const queue = [];
|
|
6
|
-
const maxBatch = opts.maxBatchSize ?? 100;
|
|
7
|
-
let pendingTimer = null;
|
|
8
|
-
function scheduleFlush() {
|
|
9
|
-
if (pendingTimer) return;
|
|
10
|
-
pendingTimer = setTimeout(() => {
|
|
11
|
-
pendingTimer = null;
|
|
12
|
-
void flush();
|
|
13
|
-
}, opts.flushIntervalMs ?? 1500);
|
|
14
|
-
}
|
|
15
|
-
async function flush() {
|
|
16
|
-
if (queue.length === 0) return;
|
|
17
|
-
const batch = queue.splice(0, maxBatch);
|
|
18
|
-
try {
|
|
19
|
-
const body = JSON.stringify({ service: opts.service, logs: batch });
|
|
20
|
-
const ok = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && document.visibilityState === "hidden" ? navigator.sendBeacon(
|
|
21
|
-
`${endpoint}/api/v1/ingest/logs?_token=${encodeURIComponent(opts.token)}`,
|
|
22
|
-
new Blob([body], { type: "application/json" })
|
|
23
|
-
) : false;
|
|
24
|
-
if (ok) return;
|
|
25
|
-
const res = await fetch(`${endpoint}/api/v1/ingest/logs`, {
|
|
26
|
-
method: "POST",
|
|
27
|
-
keepalive: true,
|
|
28
|
-
headers: {
|
|
29
|
-
"Authorization": `Bearer ${opts.token}`,
|
|
30
|
-
"Content-Type": "application/json"
|
|
31
|
-
},
|
|
32
|
-
body
|
|
33
|
-
});
|
|
34
|
-
if (!res.ok) {
|
|
35
|
-
if (res.status >= 500) queue.unshift(...batch);
|
|
36
|
-
}
|
|
37
|
-
} catch (_e) {
|
|
38
|
-
queue.unshift(...batch);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function enqueue(level, message, attributes) {
|
|
42
|
-
const attrs = {};
|
|
43
|
-
if (opts.attributes) {
|
|
44
|
-
for (const [k, v] of Object.entries(opts.attributes)) attrs[k] = String(v);
|
|
45
|
-
}
|
|
46
|
-
if (opts.environment) attrs["deployment.environment"] = opts.environment;
|
|
47
|
-
if (opts.release) attrs["service.version"] = opts.release;
|
|
48
|
-
if (typeof window !== "undefined") {
|
|
49
|
-
attrs["browser.url"] = window.location.href;
|
|
50
|
-
attrs["browser.userAgent"] = navigator.userAgent;
|
|
51
|
-
}
|
|
52
|
-
if (attributes) {
|
|
53
|
-
for (const [k, v] of Object.entries(attributes)) {
|
|
54
|
-
attrs[k] = typeof v === "string" ? v : JSON.stringify(v);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
queue.push({
|
|
58
|
-
level,
|
|
59
|
-
message,
|
|
60
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
61
|
-
attributes: attrs
|
|
62
|
-
});
|
|
63
|
-
if (queue.length >= maxBatch) void flush();
|
|
64
|
-
else scheduleFlush();
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
log: (e) => enqueue(e.level ?? "INFO", e.message, e.attributes),
|
|
68
|
-
info: (m, a) => enqueue("INFO", m, a),
|
|
69
|
-
warn: (m, a) => enqueue("WARN", m, a),
|
|
70
|
-
error: (m, a) => enqueue("ERROR", m, a),
|
|
71
|
-
captureException: (err, ctx) => {
|
|
72
|
-
const e = err instanceof Error ? err : new Error(typeof err === "string" ? err : JSON.stringify(err));
|
|
73
|
-
enqueue("ERROR", (ctx == null ? void 0 : ctx.message) ?? `${e.name}: ${e.message}`, {
|
|
74
|
-
"exception.type": e.name,
|
|
75
|
-
"exception.message": e.message,
|
|
76
|
-
"exception.stacktrace": e.stack ?? "",
|
|
77
|
-
...(ctx == null ? void 0 : ctx.tags) ?? {}
|
|
78
|
-
});
|
|
79
|
-
},
|
|
80
|
-
flush
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
function initFaroClient(opts) {
|
|
84
|
-
if (typeof window === "undefined") {
|
|
85
|
-
return {
|
|
86
|
-
log() {
|
|
87
|
-
},
|
|
88
|
-
info() {
|
|
89
|
-
},
|
|
90
|
-
warn() {
|
|
91
|
-
},
|
|
92
|
-
error() {
|
|
93
|
-
},
|
|
94
|
-
captureException() {
|
|
95
|
-
},
|
|
96
|
-
flush: async () => void 0
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
client = newClient(opts);
|
|
100
|
-
window.addEventListener("error", (ev) => {
|
|
101
|
-
client == null ? void 0 : client.captureException(ev.error ?? ev.message, { tags: { origin: "window.error" } });
|
|
102
|
-
});
|
|
103
|
-
window.addEventListener("unhandledrejection", (ev) => {
|
|
104
|
-
client == null ? void 0 : client.captureException(ev.reason, { tags: { origin: "unhandledrejection" } });
|
|
105
|
-
});
|
|
106
|
-
document.addEventListener("visibilitychange", () => {
|
|
107
|
-
if (document.visibilityState === "hidden") void (client == null ? void 0 : client.flush());
|
|
108
|
-
});
|
|
109
|
-
window.addEventListener("pagehide", () => void (client == null ? void 0 : client.flush()));
|
|
110
|
-
return client;
|
|
111
|
-
}
|
|
112
|
-
function faroClient() {
|
|
113
|
-
if (!client) throw new Error("Hay que llamar a initFaroClient() antes de usarlo");
|
|
114
|
-
return client;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export {
|
|
118
|
-
initFaroClient,
|
|
119
|
-
faroClient
|
|
120
|
-
};
|