@iaportafolio/nextjs 0.1.0 → 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 CHANGED
@@ -1,9 +1,12 @@
1
1
  # @iaportafolio/nextjs
2
2
 
3
- SDK para Next.js (App Router y Pages Router). Tiene dos mitades: **server** (Node runtime, captura errores de Route Handlers / Server Actions / SSR) y **client** (browser, captura `window.onerror` + `unhandledrejection` + Web Vitals manuales).
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 (browser)
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 { initFaroClient } from '@iaportafolio/nextjs/client';
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
- El cliente:
80
- - Registra `window.onerror` y `unhandledrejection`.
81
- - Hace `flush` automático al `visibilitychange=hidden` y `pagehide` (usa `navigator.sendBeacon` cuando está disponible).
82
- - Adjunta `browser.url` y `browser.userAgent` a cada evento.
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.** queda expuesto en el bundle — es deliberado, igual que en Sentry: el token solo permite ingerir, no leer datos del dashboard. |
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,27 @@
1
+ // src/server.ts
2
+ import * as faro from "@iaportafolio/node";
3
+ var installed = false;
4
+ function registerFaro(opts) {
5
+ if (installed) return;
6
+ if (process.env.NEXT_RUNTIME && process.env.NEXT_RUNTIME !== "nodejs") {
7
+ return;
8
+ }
9
+ faro.init(opts);
10
+ installed = true;
11
+ }
12
+ function captureRequestError(err, request) {
13
+ if (!installed) return;
14
+ faro.captureException(err, {
15
+ tags: {
16
+ "http.path": request.path ?? "",
17
+ "http.method": request.method ?? "",
18
+ "next.router": request.routerKind ?? ""
19
+ }
20
+ });
21
+ }
22
+
23
+ export {
24
+ faro,
25
+ registerFaro,
26
+ captureRequestError
27
+ };
@@ -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
+ };
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/client.ts
21
+ var client_exports = {};
22
+ __export(client_exports, {
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
35
+ });
36
+ module.exports = __toCommonJS(client_exports);
37
+ var import_browser = require("@iaportafolio/browser");
38
+ var import_browser2 = require("@iaportafolio/browser");
39
+ var import_react = require("@iaportafolio/browser/react");
40
+ function initFaroClient(opts) {
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;
44
+ }
45
+ return (0, import_browser.init)({ ...opts, release });
46
+ }
47
+ // Annotate the CommonJS export names for ESM import in node:
48
+ 0 && (module.exports = {
49
+ FaroErrorBoundary,
50
+ addBreadcrumb,
51
+ captureException,
52
+ close,
53
+ error,
54
+ flush,
55
+ getClient,
56
+ info,
57
+ initFaroClient,
58
+ log,
59
+ setUser,
60
+ warn
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 };
@@ -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 ADDED
@@ -0,0 +1,28 @@
1
+ import {
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";
15
+ export {
16
+ FaroErrorBoundary,
17
+ addBreadcrumb,
18
+ captureException,
19
+ close,
20
+ error,
21
+ flush,
22
+ getClient,
23
+ info,
24
+ initFaroClient,
25
+ log,
26
+ setUser,
27
+ warn
28
+ };
package/dist/index.cjs ADDED
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ FaroErrorBoundary: () => import_react.FaroErrorBoundary,
34
+ addBreadcrumb: () => import_browser2.addBreadcrumb,
35
+ captureException: () => import_browser2.captureException,
36
+ captureRequestError: () => captureRequestError,
37
+ close: () => import_browser2.close,
38
+ error: () => import_browser2.error,
39
+ faro: () => faro,
40
+ flush: () => import_browser2.flush,
41
+ getClient: () => import_browser2.getClient,
42
+ info: () => import_browser2.info,
43
+ initFaroClient: () => initFaroClient,
44
+ log: () => import_browser2.log,
45
+ registerFaro: () => registerFaro,
46
+ setUser: () => import_browser2.setUser,
47
+ warn: () => import_browser2.warn
48
+ });
49
+ module.exports = __toCommonJS(index_exports);
50
+
51
+ // src/server.ts
52
+ var faro = __toESM(require("@iaportafolio/node"), 1);
53
+ var installed = false;
54
+ function registerFaro(opts) {
55
+ if (installed) return;
56
+ if (process.env.NEXT_RUNTIME && process.env.NEXT_RUNTIME !== "nodejs") {
57
+ return;
58
+ }
59
+ faro.init(opts);
60
+ installed = true;
61
+ }
62
+ function captureRequestError(err, request) {
63
+ if (!installed) return;
64
+ faro.captureException(err, {
65
+ tags: {
66
+ "http.path": request.path ?? "",
67
+ "http.method": request.method ?? "",
68
+ "next.router": request.routerKind ?? ""
69
+ }
70
+ });
71
+ }
72
+
73
+ // src/client.ts
74
+ var import_browser = require("@iaportafolio/browser");
75
+ var import_browser2 = require("@iaportafolio/browser");
76
+ var import_react = require("@iaportafolio/browser/react");
77
+ function initFaroClient(opts) {
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;
81
+ }
82
+ return (0, import_browser.init)({ ...opts, release });
83
+ }
84
+ // Annotate the CommonJS export names for ESM import in node:
85
+ 0 && (module.exports = {
86
+ FaroErrorBoundary,
87
+ addBreadcrumb,
88
+ captureException,
89
+ captureRequestError,
90
+ close,
91
+ error,
92
+ faro,
93
+ flush,
94
+ getClient,
95
+ info,
96
+ initFaroClient,
97
+ log,
98
+ registerFaro,
99
+ setUser,
100
+ warn
101
+ });
@@ -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 };
@@ -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 ADDED
@@ -0,0 +1,36 @@
1
+ import {
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";
15
+ import {
16
+ captureRequestError,
17
+ faro,
18
+ registerFaro
19
+ } from "./chunk-LFDO56A5.js";
20
+ export {
21
+ FaroErrorBoundary,
22
+ addBreadcrumb,
23
+ captureException,
24
+ captureRequestError,
25
+ close,
26
+ error,
27
+ faro,
28
+ flush,
29
+ getClient,
30
+ info,
31
+ initFaroClient,
32
+ log,
33
+ registerFaro,
34
+ setUser,
35
+ warn
36
+ };
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/server.ts
31
+ var server_exports = {};
32
+ __export(server_exports, {
33
+ captureRequestError: () => captureRequestError,
34
+ faro: () => faro,
35
+ registerFaro: () => registerFaro
36
+ });
37
+ module.exports = __toCommonJS(server_exports);
38
+ var faro = __toESM(require("@iaportafolio/node"), 1);
39
+ var installed = false;
40
+ function registerFaro(opts) {
41
+ if (installed) return;
42
+ if (process.env.NEXT_RUNTIME && process.env.NEXT_RUNTIME !== "nodejs") {
43
+ return;
44
+ }
45
+ faro.init(opts);
46
+ installed = true;
47
+ }
48
+ function captureRequestError(err, request) {
49
+ if (!installed) return;
50
+ faro.captureException(err, {
51
+ tags: {
52
+ "http.path": request.path ?? "",
53
+ "http.method": request.method ?? "",
54
+ "next.router": request.routerKind ?? ""
55
+ }
56
+ });
57
+ }
58
+ // Annotate the CommonJS export names for ESM import in node:
59
+ 0 && (module.exports = {
60
+ captureRequestError,
61
+ faro,
62
+ registerFaro
63
+ });
@@ -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 };
@@ -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.js ADDED
@@ -0,0 +1,10 @@
1
+ import {
2
+ captureRequestError,
3
+ faro,
4
+ registerFaro
5
+ } from "./chunk-LFDO56A5.js";
6
+ export {
7
+ captureRequestError,
8
+ faro,
9
+ registerFaro
10
+ };
package/package.json CHANGED
@@ -1,24 +1,60 @@
1
1
  {
2
2
  "name": "@iaportafolio/nextjs",
3
- "version": "0.1.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",
8
8
  "module": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
10
10
  "exports": {
11
- ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" },
12
- "./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js", "require": "./dist/client.cjs" },
13
- "./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js", "require": "./dist/server.cjs" }
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ },
16
+ "./client": {
17
+ "types": "./dist/client.d.ts",
18
+ "import": "./dist/client.js",
19
+ "require": "./dist/client.cjs"
20
+ },
21
+ "./server": {
22
+ "types": "./dist/server.d.ts",
23
+ "import": "./dist/server.js",
24
+ "require": "./dist/server.cjs"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "src",
30
+ "README.md"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/IA-Portafolio/faro.git",
35
+ "directory": "sdks/nextjs"
14
36
  },
15
- "files": ["dist", "src", "README.md"],
16
- "repository": { "type": "git", "url": "git+https://github.com/IA-Portafolio/faro.git", "directory": "sdks/nextjs" },
17
37
  "homepage": "https://github.com/IA-Portafolio/faro/tree/main/sdks/nextjs#readme",
18
38
  "bugs": "https://github.com/IA-Portafolio/faro/issues",
19
39
  "keywords": ["faro", "nextjs", "observability", "logging", "telemetry"],
20
40
  "publishConfig": { "access": "public" },
21
- "scripts": { "build": "tsup src/index.ts src/client.ts src/server.ts --format cjs,esm --dts --clean" },
22
- "peerDependencies": { "@iaportafolio/node": "^0.1.0", "next": ">=13" },
23
- "devDependencies": { "tsup": "^8.0.0", "typescript": "^5.4.0", "@types/node": "^20.0.0" }
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" },
42
+ "peerDependencies": {
43
+ "@iaportafolio/node": "^0.1.0",
44
+ "@iaportafolio/browser": "^0.1.0",
45
+ "next": ">=13",
46
+ "react": ">=16.8.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "react": { "optional": true }
50
+ },
51
+ "devDependencies": {
52
+ "tsup": "^8.0.0",
53
+ "typescript": "^5.4.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"
59
+ }
24
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
- * Uso:
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 { initFaroClient } from '@iaportafolio/nextjs/client';
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
- * // y renderiza <FaroClient /> dentro de app/layout.tsx
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 type { FaroOptions } from '@iaportafolio/node';
26
-
27
- let client: ReturnType<typeof newClient> | null = null;
28
-
29
- interface BrowserClient {
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
- function scheduleFlush(): void {
45
- if (pendingTimer) return;
46
- pendingTimer = setTimeout(() => {
47
- pendingTimer = null;
48
- void flush();
49
- }, opts.flushIntervalMs ?? 1500);
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
- function enqueue(level: string, message: string, attributes?: Record<string, unknown>): void {
87
- const attrs: Record<string, string> = {};
88
- if (opts.attributes) {
89
- for (const [k, v] of Object.entries(opts.attributes)) attrs[k] = String(v);
90
- }
91
- if (opts.environment) attrs['deployment.environment'] = opts.environment;
92
- if (opts.release) attrs['service.version'] = opts.release;
93
- if (typeof window !== 'undefined') {
94
- attrs['browser.url'] = window.location.href;
95
- attrs['browser.userAgent'] = navigator.userAgent;
96
- }
97
- if (attributes) {
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
- return {
113
- log: (e) => enqueue(e.level ?? 'INFO', e.message, e.attributes),
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
- export function initFaroClient(opts: FaroOptions): BrowserClient {
131
- if (typeof window === 'undefined') {
132
- // Render en servidor devolvemos un no-op para que el mismo import funcione.
133
- return {
134
- log() {}, info() {}, warn() {}, error() {}, captureException() {}, flush: async () => undefined,
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
- client = newClient(opts);
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
  }