@victor-studio/monitor 0.1.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 +277 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -39
- package/dist/index.d.ts +144 -39
- package/dist/index.js +276 -37
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +31 -12
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +115 -45
- package/dist/next/index.d.ts +115 -45
- package/dist/next/index.js +31 -12
- package/dist/next/index.js.map +1 -1
- package/dist/react/index.cjs +20 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +104 -38
- package/dist/react/index.d.ts +104 -38
- package/dist/react/index.js +20 -1
- package/dist/react/index.js.map +1 -1
- package/package.json +17 -7
package/dist/next/index.d.cts
CHANGED
|
@@ -1,92 +1,162 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
|
|
3
|
-
interface
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
timestamp: string;
|
|
3
|
+
interface HeartbeatData {
|
|
4
|
+
status: 'online' | 'offline';
|
|
5
|
+
latencyMs: number;
|
|
7
6
|
}
|
|
8
|
-
interface
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
interface RequestData {
|
|
8
|
+
method: string;
|
|
9
|
+
route: string;
|
|
10
|
+
statusCode: number;
|
|
11
|
+
responseTimeMs: number;
|
|
12
|
+
userAgent?: string;
|
|
13
|
+
region?: string;
|
|
11
14
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
interface VitalData {
|
|
16
|
+
name: 'LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB';
|
|
17
|
+
value: number;
|
|
18
|
+
rating: 'good' | 'needs-improvement' | 'poor';
|
|
19
|
+
page?: string;
|
|
20
|
+
deviceType?: string;
|
|
21
|
+
browser?: string;
|
|
22
|
+
}
|
|
23
|
+
interface ErrorData {
|
|
24
|
+
type: 'unhandled' | 'caught' | 'promise';
|
|
25
|
+
message: string;
|
|
26
|
+
stack?: string;
|
|
27
|
+
groupingKey: string;
|
|
28
|
+
route?: string;
|
|
29
|
+
method?: string;
|
|
30
|
+
statusCode?: number;
|
|
31
|
+
environment?: string;
|
|
32
|
+
commitHash?: string;
|
|
33
|
+
extra?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
interface DeployData {
|
|
36
|
+
commitHash: string;
|
|
37
|
+
branch?: string;
|
|
38
|
+
author?: string;
|
|
39
|
+
status: 'started' | 'succeeded' | 'failed';
|
|
40
|
+
buildDurationMs?: number;
|
|
41
|
+
url?: string;
|
|
42
|
+
environment?: string;
|
|
43
|
+
provider?: string;
|
|
44
|
+
}
|
|
45
|
+
interface AdapterData {
|
|
46
|
+
adapter: string;
|
|
47
|
+
operation: string;
|
|
48
|
+
durationMs: number;
|
|
49
|
+
success: boolean;
|
|
50
|
+
error?: string;
|
|
51
|
+
meta?: Record<string, string>;
|
|
23
52
|
}
|
|
53
|
+
type MonitorEvent = {
|
|
54
|
+
type: 'heartbeat';
|
|
55
|
+
data: HeartbeatData;
|
|
56
|
+
timestamp: string;
|
|
57
|
+
} | {
|
|
58
|
+
type: 'request';
|
|
59
|
+
data: RequestData;
|
|
60
|
+
timestamp: string;
|
|
61
|
+
} | {
|
|
62
|
+
type: 'vital';
|
|
63
|
+
data: VitalData;
|
|
64
|
+
timestamp: string;
|
|
65
|
+
} | {
|
|
66
|
+
type: 'error';
|
|
67
|
+
data: ErrorData;
|
|
68
|
+
timestamp: string;
|
|
69
|
+
} | {
|
|
70
|
+
type: 'deploy';
|
|
71
|
+
data: DeployData;
|
|
72
|
+
timestamp: string;
|
|
73
|
+
} | {
|
|
74
|
+
type: 'adapter';
|
|
75
|
+
data: AdapterData;
|
|
76
|
+
timestamp: string;
|
|
77
|
+
};
|
|
78
|
+
/** Hook para filtrar/modificar eventos antes do envio */
|
|
79
|
+
type BeforeSendHook = (event: MonitorEvent) => MonitorEvent | null;
|
|
24
80
|
|
|
25
81
|
interface MonitorConfig {
|
|
26
82
|
/** ID do projeto no Nuvio */
|
|
27
83
|
projectId: string;
|
|
28
84
|
/** API key gerada no Nuvio */
|
|
29
85
|
apiKey: string;
|
|
30
|
-
/** URL do endpoint de ingest
|
|
86
|
+
/** URL do endpoint de ingest */
|
|
31
87
|
endpoint: string;
|
|
32
88
|
/** Intervalo do heartbeat em ms (default: 60000) */
|
|
33
89
|
heartbeatInterval?: number;
|
|
34
90
|
/** Intervalo do flush do buffer em ms (default: 30000) */
|
|
35
91
|
flushInterval?: number;
|
|
92
|
+
/** Timeout do fetch em ms (default: 10000) */
|
|
93
|
+
timeout?: number;
|
|
94
|
+
/** Max tentativas de retry (default: 3) */
|
|
95
|
+
maxRetries?: number;
|
|
36
96
|
/** Desabilitar em desenvolvimento (default: true) */
|
|
37
97
|
disableInDev?: boolean;
|
|
98
|
+
/** Taxa de amostragem 0.0-1.0 (default: 1.0). Heartbeats nunca são amostrados */
|
|
99
|
+
sampleRate?: number;
|
|
100
|
+
/** Hook chamado antes de enfileirar cada evento. Retorna null para dropar */
|
|
101
|
+
beforeSend?: BeforeSendHook;
|
|
102
|
+
/** Habilitar logs de debug no console (default: false) */
|
|
103
|
+
debug?: boolean;
|
|
104
|
+
/** Capturar erros globais automaticamente — window.onerror (default: false) */
|
|
105
|
+
captureErrors?: boolean;
|
|
106
|
+
/** Capturar unhandled promise rejections automaticamente (default: false) */
|
|
107
|
+
captureUnhandledRejections?: boolean;
|
|
38
108
|
}
|
|
39
109
|
declare class MonitorClient {
|
|
40
110
|
readonly config: MonitorConfig;
|
|
41
|
-
readonly collector
|
|
111
|
+
private readonly collector;
|
|
112
|
+
private readonly logger;
|
|
42
113
|
private heartbeatTimer;
|
|
114
|
+
private globalHandlersCleanup;
|
|
43
115
|
private active;
|
|
44
116
|
constructor(config: MonitorConfig);
|
|
45
117
|
/** Inicia o monitoring (heartbeat + collector) */
|
|
46
118
|
start(): void;
|
|
47
119
|
/** Para o monitoring */
|
|
48
120
|
stop(): void;
|
|
121
|
+
/** Força um flush imediato do buffer */
|
|
122
|
+
flush(): void;
|
|
123
|
+
/** Verifica se o monitoring está ativo */
|
|
124
|
+
get isActive(): boolean;
|
|
49
125
|
/** Registra um evento de request HTTP (usado pelo middleware) */
|
|
50
|
-
trackRequest(data:
|
|
51
|
-
method: string;
|
|
52
|
-
route: string;
|
|
53
|
-
statusCode: number;
|
|
54
|
-
responseTimeMs: number;
|
|
55
|
-
userAgent?: string;
|
|
56
|
-
region?: string;
|
|
57
|
-
}): void;
|
|
126
|
+
trackRequest(data: RequestData): void;
|
|
58
127
|
/** Registra um Web Vital (usado pelo MonitorScript) */
|
|
59
|
-
trackVital(data:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}): void;
|
|
128
|
+
trackVital(data: VitalData): void;
|
|
129
|
+
/** Registra um evento de adapter (DB, cache, AI, queue, email) */
|
|
130
|
+
trackAdapter(data: AdapterData): void;
|
|
131
|
+
/** Registra um erro capturado */
|
|
132
|
+
captureError(data: ErrorData): void;
|
|
133
|
+
/** Registra um evento de deploy */
|
|
134
|
+
trackDeploy(data: DeployData): void;
|
|
67
135
|
private startHeartbeat;
|
|
68
136
|
private sendHeartbeat;
|
|
69
|
-
private isDev;
|
|
70
137
|
}
|
|
71
138
|
|
|
72
139
|
type NextMiddleware = (request: NextRequest) => NextResponse | Response | Promise<NextResponse | Response> | undefined | Promise<undefined>;
|
|
140
|
+
/** Opções do middleware de monitoring */
|
|
141
|
+
interface WithMonitorOptions {
|
|
142
|
+
/** Regex patterns para rotas a excluir (além dos defaults) */
|
|
143
|
+
excludeRoutes?: RegExp[];
|
|
144
|
+
/** Incluir assets estáticos no tracking (default: false) */
|
|
145
|
+
includeStaticAssets?: boolean;
|
|
146
|
+
}
|
|
73
147
|
/**
|
|
74
148
|
* Envolve o middleware do Next.js para capturar métricas de request.
|
|
75
149
|
*
|
|
76
150
|
* @example
|
|
77
151
|
* ```ts
|
|
78
|
-
* // middleware.ts
|
|
79
152
|
* import { withMonitor } from '@victor-studio/monitor/next'
|
|
80
153
|
* import { monitor } from '@/lib/monitor'
|
|
81
154
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* }
|
|
86
|
-
*
|
|
87
|
-
* export default withMonitor(monitor, middleware)
|
|
155
|
+
* export default withMonitor(monitor)
|
|
156
|
+
* // ou com middleware existente:
|
|
157
|
+
* export default withMonitor(monitor, myMiddleware, { excludeRoutes: [/^\/api\/health/] })
|
|
88
158
|
* ```
|
|
89
159
|
*/
|
|
90
|
-
declare function withMonitor(monitor: MonitorClient, middleware?: NextMiddleware): (request: NextRequest) => Promise<NextResponse | Response | undefined>;
|
|
160
|
+
declare function withMonitor(monitor: MonitorClient, middleware?: NextMiddleware, options?: WithMonitorOptions): (request: NextRequest) => Promise<NextResponse | Response | undefined>;
|
|
91
161
|
|
|
92
162
|
export { withMonitor };
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,92 +1,162 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
|
|
3
|
-
interface
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
timestamp: string;
|
|
3
|
+
interface HeartbeatData {
|
|
4
|
+
status: 'online' | 'offline';
|
|
5
|
+
latencyMs: number;
|
|
7
6
|
}
|
|
8
|
-
interface
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
interface RequestData {
|
|
8
|
+
method: string;
|
|
9
|
+
route: string;
|
|
10
|
+
statusCode: number;
|
|
11
|
+
responseTimeMs: number;
|
|
12
|
+
userAgent?: string;
|
|
13
|
+
region?: string;
|
|
11
14
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
interface VitalData {
|
|
16
|
+
name: 'LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB';
|
|
17
|
+
value: number;
|
|
18
|
+
rating: 'good' | 'needs-improvement' | 'poor';
|
|
19
|
+
page?: string;
|
|
20
|
+
deviceType?: string;
|
|
21
|
+
browser?: string;
|
|
22
|
+
}
|
|
23
|
+
interface ErrorData {
|
|
24
|
+
type: 'unhandled' | 'caught' | 'promise';
|
|
25
|
+
message: string;
|
|
26
|
+
stack?: string;
|
|
27
|
+
groupingKey: string;
|
|
28
|
+
route?: string;
|
|
29
|
+
method?: string;
|
|
30
|
+
statusCode?: number;
|
|
31
|
+
environment?: string;
|
|
32
|
+
commitHash?: string;
|
|
33
|
+
extra?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
interface DeployData {
|
|
36
|
+
commitHash: string;
|
|
37
|
+
branch?: string;
|
|
38
|
+
author?: string;
|
|
39
|
+
status: 'started' | 'succeeded' | 'failed';
|
|
40
|
+
buildDurationMs?: number;
|
|
41
|
+
url?: string;
|
|
42
|
+
environment?: string;
|
|
43
|
+
provider?: string;
|
|
44
|
+
}
|
|
45
|
+
interface AdapterData {
|
|
46
|
+
adapter: string;
|
|
47
|
+
operation: string;
|
|
48
|
+
durationMs: number;
|
|
49
|
+
success: boolean;
|
|
50
|
+
error?: string;
|
|
51
|
+
meta?: Record<string, string>;
|
|
23
52
|
}
|
|
53
|
+
type MonitorEvent = {
|
|
54
|
+
type: 'heartbeat';
|
|
55
|
+
data: HeartbeatData;
|
|
56
|
+
timestamp: string;
|
|
57
|
+
} | {
|
|
58
|
+
type: 'request';
|
|
59
|
+
data: RequestData;
|
|
60
|
+
timestamp: string;
|
|
61
|
+
} | {
|
|
62
|
+
type: 'vital';
|
|
63
|
+
data: VitalData;
|
|
64
|
+
timestamp: string;
|
|
65
|
+
} | {
|
|
66
|
+
type: 'error';
|
|
67
|
+
data: ErrorData;
|
|
68
|
+
timestamp: string;
|
|
69
|
+
} | {
|
|
70
|
+
type: 'deploy';
|
|
71
|
+
data: DeployData;
|
|
72
|
+
timestamp: string;
|
|
73
|
+
} | {
|
|
74
|
+
type: 'adapter';
|
|
75
|
+
data: AdapterData;
|
|
76
|
+
timestamp: string;
|
|
77
|
+
};
|
|
78
|
+
/** Hook para filtrar/modificar eventos antes do envio */
|
|
79
|
+
type BeforeSendHook = (event: MonitorEvent) => MonitorEvent | null;
|
|
24
80
|
|
|
25
81
|
interface MonitorConfig {
|
|
26
82
|
/** ID do projeto no Nuvio */
|
|
27
83
|
projectId: string;
|
|
28
84
|
/** API key gerada no Nuvio */
|
|
29
85
|
apiKey: string;
|
|
30
|
-
/** URL do endpoint de ingest
|
|
86
|
+
/** URL do endpoint de ingest */
|
|
31
87
|
endpoint: string;
|
|
32
88
|
/** Intervalo do heartbeat em ms (default: 60000) */
|
|
33
89
|
heartbeatInterval?: number;
|
|
34
90
|
/** Intervalo do flush do buffer em ms (default: 30000) */
|
|
35
91
|
flushInterval?: number;
|
|
92
|
+
/** Timeout do fetch em ms (default: 10000) */
|
|
93
|
+
timeout?: number;
|
|
94
|
+
/** Max tentativas de retry (default: 3) */
|
|
95
|
+
maxRetries?: number;
|
|
36
96
|
/** Desabilitar em desenvolvimento (default: true) */
|
|
37
97
|
disableInDev?: boolean;
|
|
98
|
+
/** Taxa de amostragem 0.0-1.0 (default: 1.0). Heartbeats nunca são amostrados */
|
|
99
|
+
sampleRate?: number;
|
|
100
|
+
/** Hook chamado antes de enfileirar cada evento. Retorna null para dropar */
|
|
101
|
+
beforeSend?: BeforeSendHook;
|
|
102
|
+
/** Habilitar logs de debug no console (default: false) */
|
|
103
|
+
debug?: boolean;
|
|
104
|
+
/** Capturar erros globais automaticamente — window.onerror (default: false) */
|
|
105
|
+
captureErrors?: boolean;
|
|
106
|
+
/** Capturar unhandled promise rejections automaticamente (default: false) */
|
|
107
|
+
captureUnhandledRejections?: boolean;
|
|
38
108
|
}
|
|
39
109
|
declare class MonitorClient {
|
|
40
110
|
readonly config: MonitorConfig;
|
|
41
|
-
readonly collector
|
|
111
|
+
private readonly collector;
|
|
112
|
+
private readonly logger;
|
|
42
113
|
private heartbeatTimer;
|
|
114
|
+
private globalHandlersCleanup;
|
|
43
115
|
private active;
|
|
44
116
|
constructor(config: MonitorConfig);
|
|
45
117
|
/** Inicia o monitoring (heartbeat + collector) */
|
|
46
118
|
start(): void;
|
|
47
119
|
/** Para o monitoring */
|
|
48
120
|
stop(): void;
|
|
121
|
+
/** Força um flush imediato do buffer */
|
|
122
|
+
flush(): void;
|
|
123
|
+
/** Verifica se o monitoring está ativo */
|
|
124
|
+
get isActive(): boolean;
|
|
49
125
|
/** Registra um evento de request HTTP (usado pelo middleware) */
|
|
50
|
-
trackRequest(data:
|
|
51
|
-
method: string;
|
|
52
|
-
route: string;
|
|
53
|
-
statusCode: number;
|
|
54
|
-
responseTimeMs: number;
|
|
55
|
-
userAgent?: string;
|
|
56
|
-
region?: string;
|
|
57
|
-
}): void;
|
|
126
|
+
trackRequest(data: RequestData): void;
|
|
58
127
|
/** Registra um Web Vital (usado pelo MonitorScript) */
|
|
59
|
-
trackVital(data:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}): void;
|
|
128
|
+
trackVital(data: VitalData): void;
|
|
129
|
+
/** Registra um evento de adapter (DB, cache, AI, queue, email) */
|
|
130
|
+
trackAdapter(data: AdapterData): void;
|
|
131
|
+
/** Registra um erro capturado */
|
|
132
|
+
captureError(data: ErrorData): void;
|
|
133
|
+
/** Registra um evento de deploy */
|
|
134
|
+
trackDeploy(data: DeployData): void;
|
|
67
135
|
private startHeartbeat;
|
|
68
136
|
private sendHeartbeat;
|
|
69
|
-
private isDev;
|
|
70
137
|
}
|
|
71
138
|
|
|
72
139
|
type NextMiddleware = (request: NextRequest) => NextResponse | Response | Promise<NextResponse | Response> | undefined | Promise<undefined>;
|
|
140
|
+
/** Opções do middleware de monitoring */
|
|
141
|
+
interface WithMonitorOptions {
|
|
142
|
+
/** Regex patterns para rotas a excluir (além dos defaults) */
|
|
143
|
+
excludeRoutes?: RegExp[];
|
|
144
|
+
/** Incluir assets estáticos no tracking (default: false) */
|
|
145
|
+
includeStaticAssets?: boolean;
|
|
146
|
+
}
|
|
73
147
|
/**
|
|
74
148
|
* Envolve o middleware do Next.js para capturar métricas de request.
|
|
75
149
|
*
|
|
76
150
|
* @example
|
|
77
151
|
* ```ts
|
|
78
|
-
* // middleware.ts
|
|
79
152
|
* import { withMonitor } from '@victor-studio/monitor/next'
|
|
80
153
|
* import { monitor } from '@/lib/monitor'
|
|
81
154
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* }
|
|
86
|
-
*
|
|
87
|
-
* export default withMonitor(monitor, middleware)
|
|
155
|
+
* export default withMonitor(monitor)
|
|
156
|
+
* // ou com middleware existente:
|
|
157
|
+
* export default withMonitor(monitor, myMiddleware, { excludeRoutes: [/^\/api\/health/] })
|
|
88
158
|
* ```
|
|
89
159
|
*/
|
|
90
|
-
declare function withMonitor(monitor: MonitorClient, middleware?: NextMiddleware): (request: NextRequest) => Promise<NextResponse | Response | undefined>;
|
|
160
|
+
declare function withMonitor(monitor: MonitorClient, middleware?: NextMiddleware, options?: WithMonitorOptions): (request: NextRequest) => Promise<NextResponse | Response | undefined>;
|
|
91
161
|
|
|
92
162
|
export { withMonitor };
|
package/dist/next/index.js
CHANGED
|
@@ -1,26 +1,45 @@
|
|
|
1
1
|
// src/next/middleware.ts
|
|
2
|
-
|
|
2
|
+
var STATIC_ASSET_PATTERN = /^\/(_next\/|favicon|.*\.(ico|png|jpg|jpeg|gif|svg|css|woff2?|ttf|eot)$)/;
|
|
3
|
+
var REGION_HEADERS = [
|
|
4
|
+
"x-vercel-ip-country",
|
|
5
|
+
// Vercel
|
|
6
|
+
"x-railway-region",
|
|
7
|
+
// Railway
|
|
8
|
+
"cf-ipcountry",
|
|
9
|
+
// Cloudflare
|
|
10
|
+
"x-country-code"
|
|
11
|
+
// Generic CDN
|
|
12
|
+
];
|
|
13
|
+
function withMonitor(monitor, middleware, options) {
|
|
3
14
|
return async (request) => {
|
|
15
|
+
const { pathname } = new URL(request.url);
|
|
16
|
+
if (!options?.includeStaticAssets && STATIC_ASSET_PATTERN.test(pathname)) {
|
|
17
|
+
return middleware ? middleware(request) : void 0;
|
|
18
|
+
}
|
|
19
|
+
if (options?.excludeRoutes?.some((pattern) => pattern.test(pathname))) {
|
|
20
|
+
return middleware ? middleware(request) : void 0;
|
|
21
|
+
}
|
|
4
22
|
const start = performance.now();
|
|
5
23
|
let response;
|
|
6
24
|
if (middleware) {
|
|
7
25
|
response = await middleware(request);
|
|
8
26
|
}
|
|
9
27
|
const responseTimeMs = Math.round(performance.now() - start);
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
28
|
+
let region;
|
|
29
|
+
for (const header of REGION_HEADERS) {
|
|
30
|
+
const value = request.headers.get(header);
|
|
31
|
+
if (value) {
|
|
32
|
+
region = value;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
17
35
|
}
|
|
18
36
|
monitor.trackRequest({
|
|
19
|
-
method,
|
|
20
|
-
route,
|
|
21
|
-
|
|
37
|
+
method: request.method,
|
|
38
|
+
route: pathname,
|
|
39
|
+
// Não assumir 200 quando middleware retorna undefined (pass-through)
|
|
40
|
+
statusCode: response instanceof Response ? response.status : 0,
|
|
22
41
|
responseTimeMs,
|
|
23
|
-
userAgent,
|
|
42
|
+
userAgent: request.headers.get("user-agent") ?? void 0,
|
|
24
43
|
region
|
|
25
44
|
});
|
|
26
45
|
return response;
|
package/dist/next/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/next/middleware.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/next/middleware.ts"],"names":[],"mappings":";AAkBA,IAAM,oBAAA,GACJ,yEAAA;AAGF,IAAM,cAAA,GAAiB;AAAA,EACrB,qBAAA;AAAA;AAAA,EACA,kBAAA;AAAA;AAAA,EACA,cAAA;AAAA;AAAA,EACA;AAAA;AACF,CAAA;AAeO,SAAS,WAAA,CACd,OAAA,EACA,UAAA,EACA,OAAA,EACA;AACA,EAAA,OAAO,OAAO,OAAA,KAAuE;AACnF,IAAA,MAAM,EAAE,QAAA,EAAS,GAAI,IAAI,GAAA,CAAI,QAAQ,GAAG,CAAA;AAGxC,IAAA,IAAI,CAAC,OAAA,EAAS,mBAAA,IAAuB,oBAAA,CAAqB,IAAA,CAAK,QAAQ,CAAA,EAAG;AACxE,MAAA,OAAO,UAAA,GAAa,UAAA,CAAW,OAAO,CAAA,GAAI,MAAA;AAAA,IAC5C;AAGA,IAAA,IAAI,OAAA,EAAS,eAAe,IAAA,CAAK,CAAC,YAAY,OAAA,CAAQ,IAAA,CAAK,QAAQ,CAAC,CAAA,EAAG;AACrE,MAAA,OAAO,UAAA,GAAa,UAAA,CAAW,OAAO,CAAA,GAAI,MAAA;AAAA,IAC5C;AAEA,IAAA,MAAM,KAAA,GAAQ,YAAY,GAAA,EAAI;AAG9B,IAAA,IAAI,QAAA;AACJ,IAAA,IAAI,UAAA,EAAY;AACd,MAAA,QAAA,GAAW,MAAM,WAAW,OAAO,CAAA;AAAA,IACrC;AAEA,IAAA,MAAM,iBAAiB,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,GAAA,KAAQ,KAAK,CAAA;AAG3D,IAAA,IAAI,MAAA;AACJ,IAAA,KAAA,MAAW,UAAU,cAAA,EAAgB;AACnC,MAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,MAAM,CAAA;AACxC,MAAA,IAAI,KAAA,EAAO;AACT,QAAA,MAAA,GAAS,KAAA;AACT,QAAA;AAAA,MACF;AAAA,IACF;AAEA,IAAA,OAAA,CAAQ,YAAA,CAAa;AAAA,MACnB,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,KAAA,EAAO,QAAA;AAAA;AAAA,MAEP,UAAA,EAAY,QAAA,YAAoB,QAAA,GAAW,QAAA,CAAS,MAAA,GAAS,CAAA;AAAA,MAC7D,cAAA;AAAA,MACA,SAAA,EAAW,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,YAAY,CAAA,IAAK,MAAA;AAAA,MAChD;AAAA,KACD,CAAA;AAED,IAAA,OAAO,QAAA;AAAA,EACT,CAAA;AACF","file":"index.js","sourcesContent":["// withMonitor — wrapper de middleware Next.js para tracking de requests\n\nimport type { MonitorClient } from '../core/client'\nimport type { NextRequest, NextResponse } from 'next/server'\n\ntype NextMiddleware = (\n request: NextRequest,\n) => NextResponse | Response | Promise<NextResponse | Response> | undefined | Promise<undefined>\n\n/** Opções do middleware de monitoring */\nexport interface WithMonitorOptions {\n /** Regex patterns para rotas a excluir (além dos defaults) */\n excludeRoutes?: RegExp[]\n /** Incluir assets estáticos no tracking (default: false) */\n includeStaticAssets?: boolean\n}\n\n// Padrão default para assets estáticos\nconst STATIC_ASSET_PATTERN =\n /^\\/(_next\\/|favicon|.*\\.(ico|png|jpg|jpeg|gif|svg|css|woff2?|ttf|eot)$)/\n\n// Headers de região por provider (testados em ordem)\nconst REGION_HEADERS = [\n 'x-vercel-ip-country', // Vercel\n 'x-railway-region', // Railway\n 'cf-ipcountry', // Cloudflare\n 'x-country-code', // Generic CDN\n] as const\n\n/**\n * Envolve o middleware do Next.js para capturar métricas de request.\n *\n * @example\n * ```ts\n * import { withMonitor } from '@victor-studio/monitor/next'\n * import { monitor } from '@/lib/monitor'\n *\n * export default withMonitor(monitor)\n * // ou com middleware existente:\n * export default withMonitor(monitor, myMiddleware, { excludeRoutes: [/^\\/api\\/health/] })\n * ```\n */\nexport function withMonitor(\n monitor: MonitorClient,\n middleware?: NextMiddleware,\n options?: WithMonitorOptions,\n) {\n return async (request: NextRequest): Promise<NextResponse | Response | undefined> => {\n const { pathname } = new URL(request.url)\n\n // Filtrar assets estáticos (a menos que explicitamente incluídos)\n if (!options?.includeStaticAssets && STATIC_ASSET_PATTERN.test(pathname)) {\n return middleware ? middleware(request) : undefined\n }\n\n // Filtrar rotas customizadas\n if (options?.excludeRoutes?.some((pattern) => pattern.test(pathname))) {\n return middleware ? middleware(request) : undefined\n }\n\n const start = performance.now()\n\n // Executar middleware original (se existir)\n let response: NextResponse | Response | undefined\n if (middleware) {\n response = await middleware(request)\n }\n\n const responseTimeMs = Math.round(performance.now() - start)\n\n // Detectar região — chain de headers por provider\n let region: string | undefined\n for (const header of REGION_HEADERS) {\n const value = request.headers.get(header)\n if (value) {\n region = value\n break\n }\n }\n\n monitor.trackRequest({\n method: request.method,\n route: pathname,\n // Não assumir 200 quando middleware retorna undefined (pass-through)\n statusCode: response instanceof Response ? response.status : 0,\n responseTimeMs,\n userAgent: request.headers.get('user-agent') ?? undefined,\n region,\n })\n\n return response\n }\n}\n"]}
|
package/dist/react/index.cjs
CHANGED
|
@@ -5,6 +5,10 @@ var react = require('react');
|
|
|
5
5
|
// src/react/monitor-script.tsx
|
|
6
6
|
function detectDeviceType() {
|
|
7
7
|
if (typeof window === "undefined") return "unknown";
|
|
8
|
+
const uaData = navigator.userAgentData;
|
|
9
|
+
if (uaData) {
|
|
10
|
+
return uaData.mobile ? "mobile" : "desktop";
|
|
11
|
+
}
|
|
8
12
|
const ua = navigator.userAgent;
|
|
9
13
|
if (/Mobile|Android/i.test(ua)) return "mobile";
|
|
10
14
|
if (/Tablet|iPad/i.test(ua)) return "tablet";
|
|
@@ -12,20 +16,32 @@ function detectDeviceType() {
|
|
|
12
16
|
}
|
|
13
17
|
function detectBrowser() {
|
|
14
18
|
if (typeof window === "undefined") return "unknown";
|
|
19
|
+
const uaData = navigator.userAgentData;
|
|
20
|
+
if (uaData?.brands?.length) {
|
|
21
|
+
const dominated = uaData.brands.find(
|
|
22
|
+
(b) => !b.brand.startsWith("Not") && !b.brand.startsWith("Chromium")
|
|
23
|
+
);
|
|
24
|
+
if (dominated) return dominated.brand;
|
|
25
|
+
}
|
|
15
26
|
const ua = navigator.userAgent;
|
|
16
27
|
if (ua.includes("Firefox")) return "Firefox";
|
|
17
28
|
if (ua.includes("Edg/")) return "Edge";
|
|
29
|
+
if (ua.includes("OPR/") || ua.includes("Opera")) return "Opera";
|
|
30
|
+
if (ua.includes("SamsungBrowser")) return "Samsung Browser";
|
|
31
|
+
if (ua.includes("Brave")) return "Brave";
|
|
18
32
|
if (ua.includes("Chrome")) return "Chrome";
|
|
19
33
|
if (ua.includes("Safari")) return "Safari";
|
|
20
34
|
return "other";
|
|
21
35
|
}
|
|
22
36
|
function MonitorScript({ monitor }) {
|
|
23
37
|
react.useEffect(() => {
|
|
38
|
+
let cancelled = false;
|
|
24
39
|
import('web-vitals').then(({ onLCP, onINP, onCLS, onFCP, onTTFB }) => {
|
|
25
|
-
|
|
40
|
+
if (cancelled) return;
|
|
26
41
|
const deviceType = detectDeviceType();
|
|
27
42
|
const browser = detectBrowser();
|
|
28
43
|
const report = (name, value, rating) => {
|
|
44
|
+
const page = typeof window !== "undefined" ? window.location.pathname : void 0;
|
|
29
45
|
monitor.trackVital({ name, value, rating, page, deviceType, browser });
|
|
30
46
|
};
|
|
31
47
|
onLCP((metric) => report("LCP", metric.value, metric.rating));
|
|
@@ -34,6 +50,9 @@ function MonitorScript({ monitor }) {
|
|
|
34
50
|
onFCP((metric) => report("FCP", metric.value, metric.rating));
|
|
35
51
|
onTTFB((metric) => report("TTFB", metric.value, metric.rating));
|
|
36
52
|
});
|
|
53
|
+
return () => {
|
|
54
|
+
cancelled = true;
|
|
55
|
+
};
|
|
37
56
|
}, [monitor]);
|
|
38
57
|
return null;
|
|
39
58
|
}
|
package/dist/react/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/monitor-script.tsx"],"names":["useEffect"],"mappings":";;;;;AAWA,SAAS,gBAAA,GAA2B;AAClC,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,SAAA;
|
|
1
|
+
{"version":3,"sources":["../../src/react/monitor-script.tsx"],"names":["useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["// MonitorScript — componente React que coleta Web Vitals\n\n'use client'\n\nimport { useEffect } from 'react'\nimport type { MonitorClient } from '../core/client'\n\ninterface MonitorScriptProps {\n monitor: MonitorClient\n}\n\nfunction detectDeviceType(): string {\n if (typeof window === 'undefined') return 'unknown'\n\n // Preferir User-Agent Client Hints (API moderna)\n const uaData = (navigator as Navigator & { userAgentData?: { mobile?: boolean } }).userAgentData\n if (uaData) {\n return uaData.mobile ? 'mobile' : 'desktop'\n }\n\n // Fallback para UA string\n const ua = navigator.userAgent\n if (/Mobile|Android/i.test(ua)) return 'mobile'\n if (/Tablet|iPad/i.test(ua)) return 'tablet'\n return 'desktop'\n}\n\nfunction detectBrowser(): string {\n if (typeof window === 'undefined') return 'unknown'\n\n // Preferir User-Agent Client Hints\n const uaData = (navigator as Navigator & { userAgentData?: { brands?: { brand: string }[] } })\n .userAgentData\n if (uaData?.brands?.length) {\n const dominated = uaData.brands.find(\n (b) => !b.brand.startsWith('Not') && !b.brand.startsWith('Chromium'),\n )\n if (dominated) return dominated.brand\n }\n\n // Fallback para UA string (ordem importa — Chrome inclui Safari no UA)\n const ua = navigator.userAgent\n if (ua.includes('Firefox')) return 'Firefox'\n if (ua.includes('Edg/')) return 'Edge'\n if (ua.includes('OPR/') || ua.includes('Opera')) return 'Opera'\n if (ua.includes('SamsungBrowser')) return 'Samsung Browser'\n if (ua.includes('Brave')) return 'Brave'\n if (ua.includes('Chrome')) return 'Chrome'\n if (ua.includes('Safari')) return 'Safari'\n return 'other'\n}\n\n/**\n * Componente React que coleta Web Vitals automaticamente.\n * Coloque no root layout da aplicação.\n *\n * @example\n * ```tsx\n * import { MonitorScript } from '@victor-studio/monitor/react'\n * import { monitor } from '@/lib/monitor'\n *\n * export default function RootLayout({ children }) {\n * return (\n * <html>\n * <body>\n * {children}\n * <MonitorScript monitor={monitor} />\n * </body>\n * </html>\n * )\n * }\n * ```\n */\nexport function MonitorScript({ monitor }: MonitorScriptProps) {\n useEffect(() => {\n let cancelled = false\n\n // Import dinâmico pra não aumentar o bundle\n import('web-vitals').then(({ onLCP, onINP, onCLS, onFCP, onTTFB }) => {\n if (cancelled) return\n\n const deviceType = detectDeviceType()\n const browser = detectBrowser()\n\n const report = (\n name: 'LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB',\n value: number,\n rating: 'good' | 'needs-improvement' | 'poor',\n ) => {\n // Captura page no momento do report, não no mount (fix SPA navigation)\n const page = typeof window !== 'undefined' ? window.location.pathname : undefined\n monitor.trackVital({ name, value, rating, page, deviceType, browser })\n }\n\n onLCP((metric) => report('LCP', metric.value, metric.rating))\n onINP((metric) => report('INP', metric.value, metric.rating))\n onCLS((metric) => report('CLS', metric.value, metric.rating))\n onFCP((metric) => report('FCP', metric.value, metric.rating))\n onTTFB((metric) => report('TTFB', metric.value, metric.rating))\n })\n\n return () => {\n cancelled = true\n }\n }, [monitor])\n\n return null\n}\n"]}
|