@iaportafolio/nextjs 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts CHANGED
@@ -1,14 +1,26 @@
1
1
  /**
2
2
  * Faro para Next.js — lado cliente (corre en el navegador).
3
3
  *
4
- * Uso:
4
+ * Punto de entrada público para el RUM en Next.js:
5
+ * - captura de window.error / unhandledrejection
6
+ * - Web Vitals (LCP/CLS/INP/FCP/TTFB)
7
+ * - breadcrumbs de clicks y navegaciones (history.pushState/popstate)
8
+ * - sendBeacon en pagehide / visibilitychange=hidden (no se pierden eventos)
9
+ * - ErrorBoundary React (`<FaroErrorBoundary>`)
10
+ * - auto-detección de release desde env vars típicas de Vercel/Next
11
+ *
12
+ * Uso típico (App Router):
5
13
  *
6
14
  * // app/faro-client.tsx
7
15
  * 'use client';
8
16
  * import { useEffect } from 'react';
9
- * import { initFaroClient } from '@iaportafolio/nextjs/client';
17
+ * import { usePathname, useSearchParams } from 'next/navigation';
18
+ * import { initFaroClient, addBreadcrumb } from '@iaportafolio/nextjs/client';
10
19
  *
11
20
  * export function FaroClient() {
21
+ * const pathname = usePathname();
22
+ * const search = useSearchParams();
23
+ *
12
24
  * useEffect(() => {
13
25
  * initFaroClient({
14
26
  * endpoint: process.env.NEXT_PUBLIC_FARO_ENDPOINT!,
@@ -16,143 +28,63 @@
16
28
  * service: 'mi-next-app-web',
17
29
  * });
18
30
  * }, []);
31
+ *
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>
23
42
  */
24
43
 
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;
43
-
44
- function scheduleFlush(): void {
45
- if (pendingTimer) return;
46
- pendingTimer = setTimeout(() => {
47
- pendingTimer = null;
48
- void flush();
49
- }, opts.flushIntervalMs ?? 1500);
50
- }
44
+ import {
45
+ init as initBrowser,
46
+ type FaroBrowser,
47
+ type FaroBrowserOptions,
48
+ } from './browser-core';
51
49
 
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
- }
50
+ export type {
51
+ FaroBrowserOptions,
52
+ UserContext,
53
+ Breadcrumb,
54
+ LogEntry,
55
+ Severity,
56
+ WireEvent,
57
+ FaroBrowser,
58
+ } from './browser-core';
85
59
 
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
- }
60
+ export {
61
+ log,
62
+ info,
63
+ warn,
64
+ error,
65
+ captureException,
66
+ setUser,
67
+ addBreadcrumb,
68
+ flush,
69
+ close,
70
+ getClient,
71
+ } from './browser-core';
111
72
 
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
- }
73
+ export { FaroErrorBoundary } from './browser-react';
74
+ export type { FaroErrorBoundaryProps } from './browser-react';
129
75
 
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
- };
76
+ /**
77
+ * Inicializa el RUM en el navegador. Seguro de llamar en SSR — si `typeof window === 'undefined'`
78
+ * el core no hace nada. Llámalo desde `useEffect` en un componente 'use client'.
79
+ */
80
+ export function initFaroClient(opts: FaroBrowserOptions): FaroBrowser {
81
+ let release = opts.release;
82
+ if (!release && typeof process !== 'undefined' && process.env) {
83
+ release =
84
+ process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA ||
85
+ process.env.NEXT_PUBLIC_GIT_COMMIT_SHA ||
86
+ process.env.NEXT_PUBLIC_VERSION ||
87
+ undefined;
136
88
  }
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;
89
+ return initBrowser({ ...opts, release });
158
90
  }
@@ -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
- };