@iaportafolio/nextjs 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -8
- package/dist/chunk-I3FDJF4L.js +369 -0
- package/dist/client.cjs +365 -93
- package/dist/client.d.cts +191 -0
- package/dist/client.d.ts +191 -0
- package/dist/client.js +25 -5
- package/dist/index.cjs +354 -94
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +25 -5
- package/dist/server.cjs +1 -0
- package/dist/server.d.cts +42 -0
- package/dist/server.d.ts +42 -0
- package/package.json +61 -62
- package/src/browser-core.ts +394 -0
- package/src/browser-react.tsx +53 -0
- package/src/client.ts +63 -131
- package/dist/chunk-TYH3TMKC.js +0 -120
package/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
|
-
*
|
|
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 {
|
|
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
|
-
* //
|
|
39
|
+
* // app/layout.tsx
|
|
40
|
+
* import { FaroClient } from './faro-client';
|
|
41
|
+
* <body><FaroClient />{children}</body>
|
|
23
42
|
*/
|
|
24
43
|
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
log(entry: { level?: string; message: string; attributes?: Record<string, unknown> }): void;
|
|
31
|
-
info(msg: string, attrs?: Record<string, unknown>): void;
|
|
32
|
-
warn(msg: string, attrs?: Record<string, unknown>): void;
|
|
33
|
-
error(msg: string, attrs?: Record<string, unknown>): void;
|
|
34
|
-
captureException(err: unknown, ctx?: { tags?: Record<string, string>; message?: string }): void;
|
|
35
|
-
flush(): Promise<void>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function newClient(opts: FaroOptions): BrowserClient {
|
|
39
|
-
const endpoint = opts.endpoint.replace(/\/$/, '');
|
|
40
|
-
const queue: unknown[] = [];
|
|
41
|
-
const maxBatch = opts.maxBatchSize ?? 100;
|
|
42
|
-
let pendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
for (const [k, v] of Object.entries(attributes)) {
|
|
99
|
-
attrs[k] = typeof v === 'string' ? v : JSON.stringify(v);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
queue.push({
|
|
103
|
-
level,
|
|
104
|
-
message,
|
|
105
|
-
timestamp: new Date().toISOString(),
|
|
106
|
-
attributes: attrs,
|
|
107
|
-
});
|
|
108
|
-
if (queue.length >= maxBatch) void flush();
|
|
109
|
-
else scheduleFlush();
|
|
110
|
-
}
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
info: (m, a) => enqueue('INFO', m, a),
|
|
115
|
-
warn: (m, a) => enqueue('WARN', m, a),
|
|
116
|
-
error: (m, a) => enqueue('ERROR', m, a),
|
|
117
|
-
captureException: (err, ctx) => {
|
|
118
|
-
const e = err instanceof Error ? err : new Error(typeof err === 'string' ? err : JSON.stringify(err));
|
|
119
|
-
enqueue('ERROR', ctx?.message ?? `${e.name}: ${e.message}`, {
|
|
120
|
-
'exception.type': e.name,
|
|
121
|
-
'exception.message': e.message,
|
|
122
|
-
'exception.stacktrace': e.stack ?? '',
|
|
123
|
-
...(ctx?.tags ?? {}),
|
|
124
|
-
});
|
|
125
|
-
},
|
|
126
|
-
flush,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
73
|
+
export { FaroErrorBoundary } from './browser-react';
|
|
74
|
+
export type { FaroErrorBoundaryProps } from './browser-react';
|
|
129
75
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/chunk-TYH3TMKC.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
// src/client.ts
|
|
2
|
-
var client = null;
|
|
3
|
-
function newClient(opts) {
|
|
4
|
-
const endpoint = opts.endpoint.replace(/\/$/, "");
|
|
5
|
-
const queue = [];
|
|
6
|
-
const maxBatch = opts.maxBatchSize ?? 100;
|
|
7
|
-
let pendingTimer = null;
|
|
8
|
-
function scheduleFlush() {
|
|
9
|
-
if (pendingTimer) return;
|
|
10
|
-
pendingTimer = setTimeout(() => {
|
|
11
|
-
pendingTimer = null;
|
|
12
|
-
void flush();
|
|
13
|
-
}, opts.flushIntervalMs ?? 1500);
|
|
14
|
-
}
|
|
15
|
-
async function flush() {
|
|
16
|
-
if (queue.length === 0) return;
|
|
17
|
-
const batch = queue.splice(0, maxBatch);
|
|
18
|
-
try {
|
|
19
|
-
const body = JSON.stringify({ service: opts.service, logs: batch });
|
|
20
|
-
const ok = typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && document.visibilityState === "hidden" ? navigator.sendBeacon(
|
|
21
|
-
`${endpoint}/api/v1/ingest/logs?_token=${encodeURIComponent(opts.token)}`,
|
|
22
|
-
new Blob([body], { type: "application/json" })
|
|
23
|
-
) : false;
|
|
24
|
-
if (ok) return;
|
|
25
|
-
const res = await fetch(`${endpoint}/api/v1/ingest/logs`, {
|
|
26
|
-
method: "POST",
|
|
27
|
-
keepalive: true,
|
|
28
|
-
headers: {
|
|
29
|
-
"Authorization": `Bearer ${opts.token}`,
|
|
30
|
-
"Content-Type": "application/json"
|
|
31
|
-
},
|
|
32
|
-
body
|
|
33
|
-
});
|
|
34
|
-
if (!res.ok) {
|
|
35
|
-
if (res.status >= 500) queue.unshift(...batch);
|
|
36
|
-
}
|
|
37
|
-
} catch (_e) {
|
|
38
|
-
queue.unshift(...batch);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function enqueue(level, message, attributes) {
|
|
42
|
-
const attrs = {};
|
|
43
|
-
if (opts.attributes) {
|
|
44
|
-
for (const [k, v] of Object.entries(opts.attributes)) attrs[k] = String(v);
|
|
45
|
-
}
|
|
46
|
-
if (opts.environment) attrs["deployment.environment"] = opts.environment;
|
|
47
|
-
if (opts.release) attrs["service.version"] = opts.release;
|
|
48
|
-
if (typeof window !== "undefined") {
|
|
49
|
-
attrs["browser.url"] = window.location.href;
|
|
50
|
-
attrs["browser.userAgent"] = navigator.userAgent;
|
|
51
|
-
}
|
|
52
|
-
if (attributes) {
|
|
53
|
-
for (const [k, v] of Object.entries(attributes)) {
|
|
54
|
-
attrs[k] = typeof v === "string" ? v : JSON.stringify(v);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
queue.push({
|
|
58
|
-
level,
|
|
59
|
-
message,
|
|
60
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
61
|
-
attributes: attrs
|
|
62
|
-
});
|
|
63
|
-
if (queue.length >= maxBatch) void flush();
|
|
64
|
-
else scheduleFlush();
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
log: (e) => enqueue(e.level ?? "INFO", e.message, e.attributes),
|
|
68
|
-
info: (m, a) => enqueue("INFO", m, a),
|
|
69
|
-
warn: (m, a) => enqueue("WARN", m, a),
|
|
70
|
-
error: (m, a) => enqueue("ERROR", m, a),
|
|
71
|
-
captureException: (err, ctx) => {
|
|
72
|
-
const e = err instanceof Error ? err : new Error(typeof err === "string" ? err : JSON.stringify(err));
|
|
73
|
-
enqueue("ERROR", (ctx == null ? void 0 : ctx.message) ?? `${e.name}: ${e.message}`, {
|
|
74
|
-
"exception.type": e.name,
|
|
75
|
-
"exception.message": e.message,
|
|
76
|
-
"exception.stacktrace": e.stack ?? "",
|
|
77
|
-
...(ctx == null ? void 0 : ctx.tags) ?? {}
|
|
78
|
-
});
|
|
79
|
-
},
|
|
80
|
-
flush
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
function initFaroClient(opts) {
|
|
84
|
-
if (typeof window === "undefined") {
|
|
85
|
-
return {
|
|
86
|
-
log() {
|
|
87
|
-
},
|
|
88
|
-
info() {
|
|
89
|
-
},
|
|
90
|
-
warn() {
|
|
91
|
-
},
|
|
92
|
-
error() {
|
|
93
|
-
},
|
|
94
|
-
captureException() {
|
|
95
|
-
},
|
|
96
|
-
flush: async () => void 0
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
client = newClient(opts);
|
|
100
|
-
window.addEventListener("error", (ev) => {
|
|
101
|
-
client == null ? void 0 : client.captureException(ev.error ?? ev.message, { tags: { origin: "window.error" } });
|
|
102
|
-
});
|
|
103
|
-
window.addEventListener("unhandledrejection", (ev) => {
|
|
104
|
-
client == null ? void 0 : client.captureException(ev.reason, { tags: { origin: "unhandledrejection" } });
|
|
105
|
-
});
|
|
106
|
-
document.addEventListener("visibilitychange", () => {
|
|
107
|
-
if (document.visibilityState === "hidden") void (client == null ? void 0 : client.flush());
|
|
108
|
-
});
|
|
109
|
-
window.addEventListener("pagehide", () => void (client == null ? void 0 : client.flush()));
|
|
110
|
-
return client;
|
|
111
|
-
}
|
|
112
|
-
function faroClient() {
|
|
113
|
-
if (!client) throw new Error("Hay que llamar a initFaroClient() antes de usarlo");
|
|
114
|
-
return client;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export {
|
|
118
|
-
initFaroClient,
|
|
119
|
-
faroClient
|
|
120
|
-
};
|