@victor-studio/monitor 0.2.0 → 0.4.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/dist/errors/index.cjs +93 -0
- package/dist/errors/index.cjs.map +1 -0
- package/dist/errors/index.d.cts +183 -0
- package/dist/errors/index.d.ts +183 -0
- package/dist/errors/index.js +88 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.cjs +96 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -1
- package/dist/index.d.ts +41 -1
- package/dist/index.js +95 -1
- package/dist/index.js.map +1 -1
- package/dist/next/index.d.cts +5 -0
- package/dist/next/index.d.ts +5 -0
- package/dist/react/index.d.cts +5 -0
- package/dist/react/index.d.ts +5 -0
- package/package.json +6 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/errors/normalize.ts
|
|
4
|
+
function generateGroupingKey(error) {
|
|
5
|
+
const name = error.name || "Error";
|
|
6
|
+
const message = stripDynamicValues(error.message);
|
|
7
|
+
const frames = extractTopFrames(error.stack, 3);
|
|
8
|
+
const raw = `${name}:${message}:${frames.join("|")}`;
|
|
9
|
+
return simpleHash(raw);
|
|
10
|
+
}
|
|
11
|
+
function normalizeStack(stack) {
|
|
12
|
+
if (!stack) return void 0;
|
|
13
|
+
return stack.split("\n").map((line) => {
|
|
14
|
+
let normalized = line.replace(/\(?\/?(?:[\w.-]+\/)+/g, "");
|
|
15
|
+
normalized = normalized.replace(/\?[^):\s]+/g, "");
|
|
16
|
+
return normalized;
|
|
17
|
+
}).join("\n");
|
|
18
|
+
}
|
|
19
|
+
function extractTopFrames(stack, count) {
|
|
20
|
+
if (!stack) return [];
|
|
21
|
+
return stack.split("\n").filter((line) => line.includes("at ") || line.includes("@")).slice(0, count).map((line) => line.trim());
|
|
22
|
+
}
|
|
23
|
+
function stripDynamicValues(message) {
|
|
24
|
+
return message.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<uuid>").replace(/\b\d{4,}\b/g, "<n>").replace(/["'][^"']{32,}["']/g, "<token>");
|
|
25
|
+
}
|
|
26
|
+
function simpleHash(str) {
|
|
27
|
+
let hash = 0;
|
|
28
|
+
for (let i = 0; i < str.length; i++) {
|
|
29
|
+
const char = str.charCodeAt(i);
|
|
30
|
+
hash = (hash << 5) - hash + char | 0;
|
|
31
|
+
}
|
|
32
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/errors/handler.ts
|
|
36
|
+
function captureError(monitor, error, context) {
|
|
37
|
+
const normalized = toError(error);
|
|
38
|
+
const data = {
|
|
39
|
+
type: "caught",
|
|
40
|
+
message: normalized.message,
|
|
41
|
+
stack: normalizeStack(normalized.stack),
|
|
42
|
+
groupingKey: generateGroupingKey(normalized),
|
|
43
|
+
route: context?.route,
|
|
44
|
+
method: context?.method,
|
|
45
|
+
statusCode: context?.statusCode,
|
|
46
|
+
extra: context?.extra
|
|
47
|
+
};
|
|
48
|
+
monitor.captureError(data);
|
|
49
|
+
}
|
|
50
|
+
function setupGlobalHandlers(monitor) {
|
|
51
|
+
if (typeof window === "undefined") return () => {
|
|
52
|
+
};
|
|
53
|
+
const onError = (event) => {
|
|
54
|
+
const error = event.error ?? new Error(event.message);
|
|
55
|
+
const normalized = toError(error);
|
|
56
|
+
monitor.captureError({
|
|
57
|
+
type: "unhandled",
|
|
58
|
+
message: normalized.message,
|
|
59
|
+
stack: normalizeStack(normalized.stack),
|
|
60
|
+
groupingKey: generateGroupingKey(normalized),
|
|
61
|
+
route: window.location.pathname
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const onRejection = (event) => {
|
|
65
|
+
const error = event.reason ?? new Error("Unhandled promise rejection");
|
|
66
|
+
const normalized = toError(error);
|
|
67
|
+
monitor.captureError({
|
|
68
|
+
type: "promise",
|
|
69
|
+
message: normalized.message,
|
|
70
|
+
stack: normalizeStack(normalized.stack),
|
|
71
|
+
groupingKey: generateGroupingKey(normalized),
|
|
72
|
+
route: window.location.pathname
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
window.addEventListener("error", onError);
|
|
76
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
77
|
+
return () => {
|
|
78
|
+
window.removeEventListener("error", onError);
|
|
79
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function toError(value) {
|
|
83
|
+
if (value instanceof Error) return value;
|
|
84
|
+
if (typeof value === "string") return new Error(value);
|
|
85
|
+
return new Error(String(value));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
exports.captureError = captureError;
|
|
89
|
+
exports.generateGroupingKey = generateGroupingKey;
|
|
90
|
+
exports.normalizeStack = normalizeStack;
|
|
91
|
+
exports.setupGlobalHandlers = setupGlobalHandlers;
|
|
92
|
+
//# sourceMappingURL=index.cjs.map
|
|
93
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/errors/normalize.ts","../../src/errors/handler.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// Normalização de erros — stack cleanup e grouping key\n\n/**\n * Gera uma chave de agrupamento a partir do erro.\n * Erros com o mesmo groupingKey são agrupados no dashboard.\n * Usa: nome do erro + mensagem + 3 primeiros frames do stack.\n */\nexport function generateGroupingKey(error: Error): string {\n const name = error.name || 'Error'\n const message = stripDynamicValues(error.message)\n const frames = extractTopFrames(error.stack, 3)\n\n const raw = `${name}:${message}:${frames.join('|')}`\n return simpleHash(raw)\n}\n\n/**\n * Normaliza stack trace — remove caminhos absolutos e queries dinâmicas.\n */\nexport function normalizeStack(stack: string | undefined): string | undefined {\n if (!stack) return undefined\n\n return stack\n .split('\\n')\n .map((line) => {\n // Remover caminhos absolutos do filesystem\n let normalized = line.replace(/\\(?\\/?(?:[\\w.-]+\\/)+/g, '')\n // Remover query strings e hashes de URLs\n normalized = normalized.replace(/\\?[^):\\s]+/g, '')\n return normalized\n })\n .join('\\n')\n}\n\n// ─── Helpers internos ───────────────────────────────\n\n/** Extrai as N primeiras linhas significativas do stack */\nfunction extractTopFrames(stack: string | undefined, count: number): string[] {\n if (!stack) return []\n\n return stack\n .split('\\n')\n .filter((line) => line.includes('at ') || line.includes('@'))\n .slice(0, count)\n .map((line) => line.trim())\n}\n\n/** Remove valores dinâmicos da mensagem (UUIDs, números, tokens) */\nfunction stripDynamicValues(message: string): string {\n return message\n .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<uuid>')\n .replace(/\\b\\d{4,}\\b/g, '<n>')\n .replace(/[\"'][^\"']{32,}[\"']/g, '<token>')\n}\n\n/** Hash simples para gerar groupingKey determinístico */\nfunction simpleHash(str: string): string {\n let hash = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hash = ((hash << 5) - hash + char) | 0\n }\n // Converter para hex positivo\n return (hash >>> 0).toString(16).padStart(8, '0')\n}\n","// Error handler — captura de erros manual e automática\n\nimport type { MonitorClient } from '../core/client'\nimport type { ErrorData } from '../types'\nimport { generateGroupingKey, normalizeStack } from './normalize'\n\ninterface CaptureContext {\n route?: string\n method?: string\n statusCode?: number\n extra?: Record<string, string>\n}\n\n/**\n * Captura um erro e envia pro monitor.\n *\n * @example\n * ```ts\n * try {\n * await riskyOperation()\n * } catch (err) {\n * captureError(monitor, err, { route: '/api/users' })\n * }\n * ```\n */\nexport function captureError(\n monitor: MonitorClient,\n error: unknown,\n context?: CaptureContext,\n): void {\n const normalized = toError(error)\n const data: ErrorData = {\n type: 'caught',\n message: normalized.message,\n stack: normalizeStack(normalized.stack),\n groupingKey: generateGroupingKey(normalized),\n route: context?.route,\n method: context?.method,\n statusCode: context?.statusCode,\n extra: context?.extra,\n }\n\n monitor.captureError(data)\n}\n\n/**\n * Registra handlers globais para erros não capturados.\n * Só funciona no browser — no servidor, NÃO interceptamos uncaughtException\n * (perigoso para um SDK de monitoring).\n *\n * @returns Função de cleanup que remove os handlers\n *\n * @example\n * ```ts\n * const cleanup = setupGlobalHandlers(monitor)\n * // Quando quiser parar:\n * cleanup()\n * ```\n */\nexport function setupGlobalHandlers(monitor: MonitorClient): () => void {\n if (typeof window === 'undefined') return () => {}\n\n const onError = (event: ErrorEvent) => {\n const error = event.error ?? new Error(event.message)\n const normalized = toError(error)\n\n monitor.captureError({\n type: 'unhandled',\n message: normalized.message,\n stack: normalizeStack(normalized.stack),\n groupingKey: generateGroupingKey(normalized),\n route: window.location.pathname,\n })\n }\n\n const onRejection = (event: PromiseRejectionEvent) => {\n const error = event.reason ?? new Error('Unhandled promise rejection')\n const normalized = toError(error)\n\n monitor.captureError({\n type: 'promise',\n message: normalized.message,\n stack: normalizeStack(normalized.stack),\n groupingKey: generateGroupingKey(normalized),\n route: window.location.pathname,\n })\n }\n\n window.addEventListener('error', onError)\n window.addEventListener('unhandledrejection', onRejection)\n\n return () => {\n window.removeEventListener('error', onError)\n window.removeEventListener('unhandledrejection', onRejection)\n }\n}\n\n/** Converte qualquer valor em Error */\nfunction toError(value: unknown): Error {\n if (value instanceof Error) return value\n if (typeof value === 'string') return new Error(value)\n return new Error(String(value))\n}\n"]}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
interface HeartbeatData {
|
|
2
|
+
status: 'online' | 'offline';
|
|
3
|
+
latencyMs: number;
|
|
4
|
+
}
|
|
5
|
+
interface RequestData {
|
|
6
|
+
method: string;
|
|
7
|
+
route: string;
|
|
8
|
+
statusCode: number;
|
|
9
|
+
responseTimeMs: number;
|
|
10
|
+
userAgent?: string;
|
|
11
|
+
region?: string;
|
|
12
|
+
}
|
|
13
|
+
interface VitalData {
|
|
14
|
+
name: 'LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB';
|
|
15
|
+
value: number;
|
|
16
|
+
rating: 'good' | 'needs-improvement' | 'poor';
|
|
17
|
+
page?: string;
|
|
18
|
+
deviceType?: string;
|
|
19
|
+
browser?: string;
|
|
20
|
+
}
|
|
21
|
+
interface ErrorData {
|
|
22
|
+
type: 'unhandled' | 'caught' | 'promise';
|
|
23
|
+
message: string;
|
|
24
|
+
stack?: string;
|
|
25
|
+
groupingKey: string;
|
|
26
|
+
route?: string;
|
|
27
|
+
method?: string;
|
|
28
|
+
statusCode?: number;
|
|
29
|
+
environment?: string;
|
|
30
|
+
commitHash?: string;
|
|
31
|
+
extra?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
interface DeployData {
|
|
34
|
+
commitHash: string;
|
|
35
|
+
branch?: string;
|
|
36
|
+
author?: string;
|
|
37
|
+
status: 'started' | 'succeeded' | 'failed';
|
|
38
|
+
buildDurationMs?: number;
|
|
39
|
+
url?: string;
|
|
40
|
+
environment?: string;
|
|
41
|
+
provider?: string;
|
|
42
|
+
}
|
|
43
|
+
interface AdapterData {
|
|
44
|
+
adapter: string;
|
|
45
|
+
operation: string;
|
|
46
|
+
durationMs: number;
|
|
47
|
+
success: boolean;
|
|
48
|
+
error?: string;
|
|
49
|
+
meta?: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
type MonitorEvent = {
|
|
52
|
+
type: 'heartbeat';
|
|
53
|
+
data: HeartbeatData;
|
|
54
|
+
timestamp: string;
|
|
55
|
+
} | {
|
|
56
|
+
type: 'request';
|
|
57
|
+
data: RequestData;
|
|
58
|
+
timestamp: string;
|
|
59
|
+
} | {
|
|
60
|
+
type: 'vital';
|
|
61
|
+
data: VitalData;
|
|
62
|
+
timestamp: string;
|
|
63
|
+
} | {
|
|
64
|
+
type: 'error';
|
|
65
|
+
data: ErrorData;
|
|
66
|
+
timestamp: string;
|
|
67
|
+
} | {
|
|
68
|
+
type: 'deploy';
|
|
69
|
+
data: DeployData;
|
|
70
|
+
timestamp: string;
|
|
71
|
+
} | {
|
|
72
|
+
type: 'adapter';
|
|
73
|
+
data: AdapterData;
|
|
74
|
+
timestamp: string;
|
|
75
|
+
};
|
|
76
|
+
/** Hook para filtrar/modificar eventos antes do envio */
|
|
77
|
+
type BeforeSendHook = (event: MonitorEvent) => MonitorEvent | null;
|
|
78
|
+
|
|
79
|
+
interface MonitorConfig {
|
|
80
|
+
/** ID do projeto no Nuvio */
|
|
81
|
+
projectId: string;
|
|
82
|
+
/** API key gerada no Nuvio */
|
|
83
|
+
apiKey: string;
|
|
84
|
+
/** URL do endpoint de ingest */
|
|
85
|
+
endpoint: string;
|
|
86
|
+
/** Intervalo do heartbeat em ms (default: 60000) */
|
|
87
|
+
heartbeatInterval?: number;
|
|
88
|
+
/** Intervalo do flush do buffer em ms (default: 30000) */
|
|
89
|
+
flushInterval?: number;
|
|
90
|
+
/** Timeout do fetch em ms (default: 10000) */
|
|
91
|
+
timeout?: number;
|
|
92
|
+
/** Max tentativas de retry (default: 3) */
|
|
93
|
+
maxRetries?: number;
|
|
94
|
+
/** Desabilitar em desenvolvimento (default: true) */
|
|
95
|
+
disableInDev?: boolean;
|
|
96
|
+
/** Taxa de amostragem 0.0-1.0 (default: 1.0). Heartbeats nunca são amostrados */
|
|
97
|
+
sampleRate?: number;
|
|
98
|
+
/** Hook chamado antes de enfileirar cada evento. Retorna null para dropar */
|
|
99
|
+
beforeSend?: BeforeSendHook;
|
|
100
|
+
/** Habilitar logs de debug no console (default: false) */
|
|
101
|
+
debug?: boolean;
|
|
102
|
+
/** Capturar erros globais automaticamente — window.onerror (default: false) */
|
|
103
|
+
captureErrors?: boolean;
|
|
104
|
+
/** Capturar unhandled promise rejections automaticamente (default: false) */
|
|
105
|
+
captureUnhandledRejections?: boolean;
|
|
106
|
+
}
|
|
107
|
+
declare class MonitorClient {
|
|
108
|
+
readonly config: MonitorConfig;
|
|
109
|
+
private readonly collector;
|
|
110
|
+
private readonly logger;
|
|
111
|
+
private heartbeatTimer;
|
|
112
|
+
private globalHandlersCleanup;
|
|
113
|
+
private active;
|
|
114
|
+
constructor(config: MonitorConfig);
|
|
115
|
+
/** Inicia o monitoring (heartbeat + collector) */
|
|
116
|
+
start(): void;
|
|
117
|
+
/** Para o monitoring */
|
|
118
|
+
stop(): void;
|
|
119
|
+
/** Força um flush imediato do buffer */
|
|
120
|
+
flush(): void;
|
|
121
|
+
/** Verifica se o monitoring está ativo */
|
|
122
|
+
get isActive(): boolean;
|
|
123
|
+
/** Registra um evento de request HTTP (usado pelo middleware) */
|
|
124
|
+
trackRequest(data: RequestData): void;
|
|
125
|
+
/** Registra um Web Vital (usado pelo MonitorScript) */
|
|
126
|
+
trackVital(data: VitalData): void;
|
|
127
|
+
/** Registra um evento de adapter (DB, cache, AI, queue, email) */
|
|
128
|
+
trackAdapter(data: AdapterData): void;
|
|
129
|
+
/** Registra um erro capturado */
|
|
130
|
+
captureError(data: ErrorData): void;
|
|
131
|
+
/** Registra um evento de deploy */
|
|
132
|
+
trackDeploy(data: DeployData): void;
|
|
133
|
+
private startHeartbeat;
|
|
134
|
+
private sendHeartbeat;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface CaptureContext {
|
|
138
|
+
route?: string;
|
|
139
|
+
method?: string;
|
|
140
|
+
statusCode?: number;
|
|
141
|
+
extra?: Record<string, string>;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Captura um erro e envia pro monitor.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* try {
|
|
149
|
+
* await riskyOperation()
|
|
150
|
+
* } catch (err) {
|
|
151
|
+
* captureError(monitor, err, { route: '/api/users' })
|
|
152
|
+
* }
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
declare function captureError(monitor: MonitorClient, error: unknown, context?: CaptureContext): void;
|
|
156
|
+
/**
|
|
157
|
+
* Registra handlers globais para erros não capturados.
|
|
158
|
+
* Só funciona no browser — no servidor, NÃO interceptamos uncaughtException
|
|
159
|
+
* (perigoso para um SDK de monitoring).
|
|
160
|
+
*
|
|
161
|
+
* @returns Função de cleanup que remove os handlers
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* const cleanup = setupGlobalHandlers(monitor)
|
|
166
|
+
* // Quando quiser parar:
|
|
167
|
+
* cleanup()
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
declare function setupGlobalHandlers(monitor: MonitorClient): () => void;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Gera uma chave de agrupamento a partir do erro.
|
|
174
|
+
* Erros com o mesmo groupingKey são agrupados no dashboard.
|
|
175
|
+
* Usa: nome do erro + mensagem + 3 primeiros frames do stack.
|
|
176
|
+
*/
|
|
177
|
+
declare function generateGroupingKey(error: Error): string;
|
|
178
|
+
/**
|
|
179
|
+
* Normaliza stack trace — remove caminhos absolutos e queries dinâmicas.
|
|
180
|
+
*/
|
|
181
|
+
declare function normalizeStack(stack: string | undefined): string | undefined;
|
|
182
|
+
|
|
183
|
+
export { captureError, generateGroupingKey, normalizeStack, setupGlobalHandlers };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
interface HeartbeatData {
|
|
2
|
+
status: 'online' | 'offline';
|
|
3
|
+
latencyMs: number;
|
|
4
|
+
}
|
|
5
|
+
interface RequestData {
|
|
6
|
+
method: string;
|
|
7
|
+
route: string;
|
|
8
|
+
statusCode: number;
|
|
9
|
+
responseTimeMs: number;
|
|
10
|
+
userAgent?: string;
|
|
11
|
+
region?: string;
|
|
12
|
+
}
|
|
13
|
+
interface VitalData {
|
|
14
|
+
name: 'LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB';
|
|
15
|
+
value: number;
|
|
16
|
+
rating: 'good' | 'needs-improvement' | 'poor';
|
|
17
|
+
page?: string;
|
|
18
|
+
deviceType?: string;
|
|
19
|
+
browser?: string;
|
|
20
|
+
}
|
|
21
|
+
interface ErrorData {
|
|
22
|
+
type: 'unhandled' | 'caught' | 'promise';
|
|
23
|
+
message: string;
|
|
24
|
+
stack?: string;
|
|
25
|
+
groupingKey: string;
|
|
26
|
+
route?: string;
|
|
27
|
+
method?: string;
|
|
28
|
+
statusCode?: number;
|
|
29
|
+
environment?: string;
|
|
30
|
+
commitHash?: string;
|
|
31
|
+
extra?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
interface DeployData {
|
|
34
|
+
commitHash: string;
|
|
35
|
+
branch?: string;
|
|
36
|
+
author?: string;
|
|
37
|
+
status: 'started' | 'succeeded' | 'failed';
|
|
38
|
+
buildDurationMs?: number;
|
|
39
|
+
url?: string;
|
|
40
|
+
environment?: string;
|
|
41
|
+
provider?: string;
|
|
42
|
+
}
|
|
43
|
+
interface AdapterData {
|
|
44
|
+
adapter: string;
|
|
45
|
+
operation: string;
|
|
46
|
+
durationMs: number;
|
|
47
|
+
success: boolean;
|
|
48
|
+
error?: string;
|
|
49
|
+
meta?: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
type MonitorEvent = {
|
|
52
|
+
type: 'heartbeat';
|
|
53
|
+
data: HeartbeatData;
|
|
54
|
+
timestamp: string;
|
|
55
|
+
} | {
|
|
56
|
+
type: 'request';
|
|
57
|
+
data: RequestData;
|
|
58
|
+
timestamp: string;
|
|
59
|
+
} | {
|
|
60
|
+
type: 'vital';
|
|
61
|
+
data: VitalData;
|
|
62
|
+
timestamp: string;
|
|
63
|
+
} | {
|
|
64
|
+
type: 'error';
|
|
65
|
+
data: ErrorData;
|
|
66
|
+
timestamp: string;
|
|
67
|
+
} | {
|
|
68
|
+
type: 'deploy';
|
|
69
|
+
data: DeployData;
|
|
70
|
+
timestamp: string;
|
|
71
|
+
} | {
|
|
72
|
+
type: 'adapter';
|
|
73
|
+
data: AdapterData;
|
|
74
|
+
timestamp: string;
|
|
75
|
+
};
|
|
76
|
+
/** Hook para filtrar/modificar eventos antes do envio */
|
|
77
|
+
type BeforeSendHook = (event: MonitorEvent) => MonitorEvent | null;
|
|
78
|
+
|
|
79
|
+
interface MonitorConfig {
|
|
80
|
+
/** ID do projeto no Nuvio */
|
|
81
|
+
projectId: string;
|
|
82
|
+
/** API key gerada no Nuvio */
|
|
83
|
+
apiKey: string;
|
|
84
|
+
/** URL do endpoint de ingest */
|
|
85
|
+
endpoint: string;
|
|
86
|
+
/** Intervalo do heartbeat em ms (default: 60000) */
|
|
87
|
+
heartbeatInterval?: number;
|
|
88
|
+
/** Intervalo do flush do buffer em ms (default: 30000) */
|
|
89
|
+
flushInterval?: number;
|
|
90
|
+
/** Timeout do fetch em ms (default: 10000) */
|
|
91
|
+
timeout?: number;
|
|
92
|
+
/** Max tentativas de retry (default: 3) */
|
|
93
|
+
maxRetries?: number;
|
|
94
|
+
/** Desabilitar em desenvolvimento (default: true) */
|
|
95
|
+
disableInDev?: boolean;
|
|
96
|
+
/** Taxa de amostragem 0.0-1.0 (default: 1.0). Heartbeats nunca são amostrados */
|
|
97
|
+
sampleRate?: number;
|
|
98
|
+
/** Hook chamado antes de enfileirar cada evento. Retorna null para dropar */
|
|
99
|
+
beforeSend?: BeforeSendHook;
|
|
100
|
+
/** Habilitar logs de debug no console (default: false) */
|
|
101
|
+
debug?: boolean;
|
|
102
|
+
/** Capturar erros globais automaticamente — window.onerror (default: false) */
|
|
103
|
+
captureErrors?: boolean;
|
|
104
|
+
/** Capturar unhandled promise rejections automaticamente (default: false) */
|
|
105
|
+
captureUnhandledRejections?: boolean;
|
|
106
|
+
}
|
|
107
|
+
declare class MonitorClient {
|
|
108
|
+
readonly config: MonitorConfig;
|
|
109
|
+
private readonly collector;
|
|
110
|
+
private readonly logger;
|
|
111
|
+
private heartbeatTimer;
|
|
112
|
+
private globalHandlersCleanup;
|
|
113
|
+
private active;
|
|
114
|
+
constructor(config: MonitorConfig);
|
|
115
|
+
/** Inicia o monitoring (heartbeat + collector) */
|
|
116
|
+
start(): void;
|
|
117
|
+
/** Para o monitoring */
|
|
118
|
+
stop(): void;
|
|
119
|
+
/** Força um flush imediato do buffer */
|
|
120
|
+
flush(): void;
|
|
121
|
+
/** Verifica se o monitoring está ativo */
|
|
122
|
+
get isActive(): boolean;
|
|
123
|
+
/** Registra um evento de request HTTP (usado pelo middleware) */
|
|
124
|
+
trackRequest(data: RequestData): void;
|
|
125
|
+
/** Registra um Web Vital (usado pelo MonitorScript) */
|
|
126
|
+
trackVital(data: VitalData): void;
|
|
127
|
+
/** Registra um evento de adapter (DB, cache, AI, queue, email) */
|
|
128
|
+
trackAdapter(data: AdapterData): void;
|
|
129
|
+
/** Registra um erro capturado */
|
|
130
|
+
captureError(data: ErrorData): void;
|
|
131
|
+
/** Registra um evento de deploy */
|
|
132
|
+
trackDeploy(data: DeployData): void;
|
|
133
|
+
private startHeartbeat;
|
|
134
|
+
private sendHeartbeat;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface CaptureContext {
|
|
138
|
+
route?: string;
|
|
139
|
+
method?: string;
|
|
140
|
+
statusCode?: number;
|
|
141
|
+
extra?: Record<string, string>;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Captura um erro e envia pro monitor.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* try {
|
|
149
|
+
* await riskyOperation()
|
|
150
|
+
* } catch (err) {
|
|
151
|
+
* captureError(monitor, err, { route: '/api/users' })
|
|
152
|
+
* }
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
declare function captureError(monitor: MonitorClient, error: unknown, context?: CaptureContext): void;
|
|
156
|
+
/**
|
|
157
|
+
* Registra handlers globais para erros não capturados.
|
|
158
|
+
* Só funciona no browser — no servidor, NÃO interceptamos uncaughtException
|
|
159
|
+
* (perigoso para um SDK de monitoring).
|
|
160
|
+
*
|
|
161
|
+
* @returns Função de cleanup que remove os handlers
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* const cleanup = setupGlobalHandlers(monitor)
|
|
166
|
+
* // Quando quiser parar:
|
|
167
|
+
* cleanup()
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
declare function setupGlobalHandlers(monitor: MonitorClient): () => void;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Gera uma chave de agrupamento a partir do erro.
|
|
174
|
+
* Erros com o mesmo groupingKey são agrupados no dashboard.
|
|
175
|
+
* Usa: nome do erro + mensagem + 3 primeiros frames do stack.
|
|
176
|
+
*/
|
|
177
|
+
declare function generateGroupingKey(error: Error): string;
|
|
178
|
+
/**
|
|
179
|
+
* Normaliza stack trace — remove caminhos absolutos e queries dinâmicas.
|
|
180
|
+
*/
|
|
181
|
+
declare function normalizeStack(stack: string | undefined): string | undefined;
|
|
182
|
+
|
|
183
|
+
export { captureError, generateGroupingKey, normalizeStack, setupGlobalHandlers };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/errors/normalize.ts
|
|
2
|
+
function generateGroupingKey(error) {
|
|
3
|
+
const name = error.name || "Error";
|
|
4
|
+
const message = stripDynamicValues(error.message);
|
|
5
|
+
const frames = extractTopFrames(error.stack, 3);
|
|
6
|
+
const raw = `${name}:${message}:${frames.join("|")}`;
|
|
7
|
+
return simpleHash(raw);
|
|
8
|
+
}
|
|
9
|
+
function normalizeStack(stack) {
|
|
10
|
+
if (!stack) return void 0;
|
|
11
|
+
return stack.split("\n").map((line) => {
|
|
12
|
+
let normalized = line.replace(/\(?\/?(?:[\w.-]+\/)+/g, "");
|
|
13
|
+
normalized = normalized.replace(/\?[^):\s]+/g, "");
|
|
14
|
+
return normalized;
|
|
15
|
+
}).join("\n");
|
|
16
|
+
}
|
|
17
|
+
function extractTopFrames(stack, count) {
|
|
18
|
+
if (!stack) return [];
|
|
19
|
+
return stack.split("\n").filter((line) => line.includes("at ") || line.includes("@")).slice(0, count).map((line) => line.trim());
|
|
20
|
+
}
|
|
21
|
+
function stripDynamicValues(message) {
|
|
22
|
+
return message.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<uuid>").replace(/\b\d{4,}\b/g, "<n>").replace(/["'][^"']{32,}["']/g, "<token>");
|
|
23
|
+
}
|
|
24
|
+
function simpleHash(str) {
|
|
25
|
+
let hash = 0;
|
|
26
|
+
for (let i = 0; i < str.length; i++) {
|
|
27
|
+
const char = str.charCodeAt(i);
|
|
28
|
+
hash = (hash << 5) - hash + char | 0;
|
|
29
|
+
}
|
|
30
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/errors/handler.ts
|
|
34
|
+
function captureError(monitor, error, context) {
|
|
35
|
+
const normalized = toError(error);
|
|
36
|
+
const data = {
|
|
37
|
+
type: "caught",
|
|
38
|
+
message: normalized.message,
|
|
39
|
+
stack: normalizeStack(normalized.stack),
|
|
40
|
+
groupingKey: generateGroupingKey(normalized),
|
|
41
|
+
route: context?.route,
|
|
42
|
+
method: context?.method,
|
|
43
|
+
statusCode: context?.statusCode,
|
|
44
|
+
extra: context?.extra
|
|
45
|
+
};
|
|
46
|
+
monitor.captureError(data);
|
|
47
|
+
}
|
|
48
|
+
function setupGlobalHandlers(monitor) {
|
|
49
|
+
if (typeof window === "undefined") return () => {
|
|
50
|
+
};
|
|
51
|
+
const onError = (event) => {
|
|
52
|
+
const error = event.error ?? new Error(event.message);
|
|
53
|
+
const normalized = toError(error);
|
|
54
|
+
monitor.captureError({
|
|
55
|
+
type: "unhandled",
|
|
56
|
+
message: normalized.message,
|
|
57
|
+
stack: normalizeStack(normalized.stack),
|
|
58
|
+
groupingKey: generateGroupingKey(normalized),
|
|
59
|
+
route: window.location.pathname
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
const onRejection = (event) => {
|
|
63
|
+
const error = event.reason ?? new Error("Unhandled promise rejection");
|
|
64
|
+
const normalized = toError(error);
|
|
65
|
+
monitor.captureError({
|
|
66
|
+
type: "promise",
|
|
67
|
+
message: normalized.message,
|
|
68
|
+
stack: normalizeStack(normalized.stack),
|
|
69
|
+
groupingKey: generateGroupingKey(normalized),
|
|
70
|
+
route: window.location.pathname
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
window.addEventListener("error", onError);
|
|
74
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
75
|
+
return () => {
|
|
76
|
+
window.removeEventListener("error", onError);
|
|
77
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function toError(value) {
|
|
81
|
+
if (value instanceof Error) return value;
|
|
82
|
+
if (typeof value === "string") return new Error(value);
|
|
83
|
+
return new Error(String(value));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { captureError, generateGroupingKey, normalizeStack, setupGlobalHandlers };
|
|
87
|
+
//# sourceMappingURL=index.js.map
|
|
88
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/errors/normalize.ts","../../src/errors/handler.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// Normalização de erros — stack cleanup e grouping key\n\n/**\n * Gera uma chave de agrupamento a partir do erro.\n * Erros com o mesmo groupingKey são agrupados no dashboard.\n * Usa: nome do erro + mensagem + 3 primeiros frames do stack.\n */\nexport function generateGroupingKey(error: Error): string {\n const name = error.name || 'Error'\n const message = stripDynamicValues(error.message)\n const frames = extractTopFrames(error.stack, 3)\n\n const raw = `${name}:${message}:${frames.join('|')}`\n return simpleHash(raw)\n}\n\n/**\n * Normaliza stack trace — remove caminhos absolutos e queries dinâmicas.\n */\nexport function normalizeStack(stack: string | undefined): string | undefined {\n if (!stack) return undefined\n\n return stack\n .split('\\n')\n .map((line) => {\n // Remover caminhos absolutos do filesystem\n let normalized = line.replace(/\\(?\\/?(?:[\\w.-]+\\/)+/g, '')\n // Remover query strings e hashes de URLs\n normalized = normalized.replace(/\\?[^):\\s]+/g, '')\n return normalized\n })\n .join('\\n')\n}\n\n// ─── Helpers internos ───────────────────────────────\n\n/** Extrai as N primeiras linhas significativas do stack */\nfunction extractTopFrames(stack: string | undefined, count: number): string[] {\n if (!stack) return []\n\n return stack\n .split('\\n')\n .filter((line) => line.includes('at ') || line.includes('@'))\n .slice(0, count)\n .map((line) => line.trim())\n}\n\n/** Remove valores dinâmicos da mensagem (UUIDs, números, tokens) */\nfunction stripDynamicValues(message: string): string {\n return message\n .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<uuid>')\n .replace(/\\b\\d{4,}\\b/g, '<n>')\n .replace(/[\"'][^\"']{32,}[\"']/g, '<token>')\n}\n\n/** Hash simples para gerar groupingKey determinístico */\nfunction simpleHash(str: string): string {\n let hash = 0\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i)\n hash = ((hash << 5) - hash + char) | 0\n }\n // Converter para hex positivo\n return (hash >>> 0).toString(16).padStart(8, '0')\n}\n","// Error handler — captura de erros manual e automática\n\nimport type { MonitorClient } from '../core/client'\nimport type { ErrorData } from '../types'\nimport { generateGroupingKey, normalizeStack } from './normalize'\n\ninterface CaptureContext {\n route?: string\n method?: string\n statusCode?: number\n extra?: Record<string, string>\n}\n\n/**\n * Captura um erro e envia pro monitor.\n *\n * @example\n * ```ts\n * try {\n * await riskyOperation()\n * } catch (err) {\n * captureError(monitor, err, { route: '/api/users' })\n * }\n * ```\n */\nexport function captureError(\n monitor: MonitorClient,\n error: unknown,\n context?: CaptureContext,\n): void {\n const normalized = toError(error)\n const data: ErrorData = {\n type: 'caught',\n message: normalized.message,\n stack: normalizeStack(normalized.stack),\n groupingKey: generateGroupingKey(normalized),\n route: context?.route,\n method: context?.method,\n statusCode: context?.statusCode,\n extra: context?.extra,\n }\n\n monitor.captureError(data)\n}\n\n/**\n * Registra handlers globais para erros não capturados.\n * Só funciona no browser — no servidor, NÃO interceptamos uncaughtException\n * (perigoso para um SDK de monitoring).\n *\n * @returns Função de cleanup que remove os handlers\n *\n * @example\n * ```ts\n * const cleanup = setupGlobalHandlers(monitor)\n * // Quando quiser parar:\n * cleanup()\n * ```\n */\nexport function setupGlobalHandlers(monitor: MonitorClient): () => void {\n if (typeof window === 'undefined') return () => {}\n\n const onError = (event: ErrorEvent) => {\n const error = event.error ?? new Error(event.message)\n const normalized = toError(error)\n\n monitor.captureError({\n type: 'unhandled',\n message: normalized.message,\n stack: normalizeStack(normalized.stack),\n groupingKey: generateGroupingKey(normalized),\n route: window.location.pathname,\n })\n }\n\n const onRejection = (event: PromiseRejectionEvent) => {\n const error = event.reason ?? new Error('Unhandled promise rejection')\n const normalized = toError(error)\n\n monitor.captureError({\n type: 'promise',\n message: normalized.message,\n stack: normalizeStack(normalized.stack),\n groupingKey: generateGroupingKey(normalized),\n route: window.location.pathname,\n })\n }\n\n window.addEventListener('error', onError)\n window.addEventListener('unhandledrejection', onRejection)\n\n return () => {\n window.removeEventListener('error', onError)\n window.removeEventListener('unhandledrejection', onRejection)\n }\n}\n\n/** Converte qualquer valor em Error */\nfunction toError(value: unknown): Error {\n if (value instanceof Error) return value\n if (typeof value === 'string') return new Error(value)\n return new Error(String(value))\n}\n"]}
|