@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 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,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
- faroClient: () => faroClient,
23
- initFaroClient: () => initFaroClient
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 client = null;
27
- function newClient(opts) {
28
- const endpoint = opts.endpoint.replace(/\/$/, "");
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
- if (typeof window === "undefined") {
109
- return {
110
- log() {
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
- client = newClient(opts);
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
- faroClient,
143
- initFaroClient
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 };
@@ -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
- faroClient,
3
- initFaroClient
4
- } from "./chunk-TYH3TMKC.js";
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
- faroClient,
7
- initFaroClient
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
- faroClient: () => faroClient,
40
+ flush: () => import_browser2.flush,
41
+ getClient: () => import_browser2.getClient,
42
+ info: () => import_browser2.info,
35
43
  initFaroClient: () => initFaroClient,
36
- registerFaro: () => registerFaro
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 client = null;
64
- function newClient(opts) {
65
- const endpoint = opts.endpoint.replace(/\/$/, "");
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
- if (typeof window === "undefined") {
146
- return {
147
- log() {
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
- client = newClient(opts);
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
- faroClient,
93
+ flush,
94
+ getClient,
95
+ info,
182
96
  initFaroClient,
183
- registerFaro
97
+ log,
98
+ registerFaro,
99
+ setUser,
100
+ warn
184
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 CHANGED
@@ -1,16 +1,36 @@
1
1
  import {
2
- faroClient,
3
- initFaroClient
4
- } from "./chunk-TYH3TMKC.js";
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
- faroClient,
28
+ flush,
29
+ getClient,
30
+ info,
14
31
  initFaroClient,
15
- registerFaro
32
+ log,
33
+ registerFaro,
34
+ setUser,
35
+ warn
16
36
  };
package/dist/server.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;
@@ -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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iaportafolio/nextjs",
3
- "version": "0.1.1",
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
- "faro",
41
- "nextjs",
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
- "next": ">=13"
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
- * 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
  }
@@ -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
- };