@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.
@@ -1,92 +1,162 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
 
3
- interface MonitorEvent {
4
- type: 'heartbeat' | 'request' | 'vital';
5
- data: Record<string, unknown>;
6
- timestamp: string;
3
+ interface HeartbeatData {
4
+ status: 'online' | 'offline';
5
+ latencyMs: number;
7
6
  }
8
- interface TransportConfig {
9
- endpoint: string;
10
- apiKey: string;
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
- declare class Collector {
14
- private buffer;
15
- private timer;
16
- private config;
17
- private flushInterval;
18
- constructor(config: TransportConfig, flushInterval?: number);
19
- start(): void;
20
- stop(): void;
21
- push(event: Omit<MonitorEvent, 'timestamp'>): void;
22
- private flush;
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 (ex: https://app.nuvio.com/api/monitor/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: 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
- name: 'LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB';
61
- value: number;
62
- rating: 'good' | 'needs-improvement' | 'poor';
63
- page?: string;
64
- deviceType?: string;
65
- browser?: string;
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
- * function middleware(request: NextRequest) {
83
- * // sua lógica de middleware
84
- * return NextResponse.next()
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 };
@@ -1,92 +1,162 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
 
3
- interface MonitorEvent {
4
- type: 'heartbeat' | 'request' | 'vital';
5
- data: Record<string, unknown>;
6
- timestamp: string;
3
+ interface HeartbeatData {
4
+ status: 'online' | 'offline';
5
+ latencyMs: number;
7
6
  }
8
- interface TransportConfig {
9
- endpoint: string;
10
- apiKey: string;
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
- declare class Collector {
14
- private buffer;
15
- private timer;
16
- private config;
17
- private flushInterval;
18
- constructor(config: TransportConfig, flushInterval?: number);
19
- start(): void;
20
- stop(): void;
21
- push(event: Omit<MonitorEvent, 'timestamp'>): void;
22
- private flush;
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 (ex: https://app.nuvio.com/api/monitor/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: 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
- name: 'LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB';
61
- value: number;
62
- rating: 'good' | 'needs-improvement' | 'poor';
63
- page?: string;
64
- deviceType?: string;
65
- browser?: string;
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
- * function middleware(request: NextRequest) {
83
- * // sua lógica de middleware
84
- * return NextResponse.next()
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 };
@@ -1,26 +1,45 @@
1
1
  // src/next/middleware.ts
2
- function withMonitor(monitor, middleware) {
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
- const method = request.method;
11
- const route = new URL(request.url).pathname;
12
- const statusCode = response instanceof Response ? response.status : 200;
13
- const userAgent = request.headers.get("user-agent") ?? void 0;
14
- const region = request.headers.get("x-vercel-ip-country") ?? void 0;
15
- if (route.startsWith("/_next/") || route.startsWith("/favicon") || route.endsWith(".ico") || route.endsWith(".png") || route.endsWith(".jpg") || route.endsWith(".svg") || route.endsWith(".css") || route.endsWith(".js")) {
16
- return response;
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
- statusCode,
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;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/next/middleware.ts"],"names":[],"mappings":";AA0BO,SAAS,WAAA,CAAY,SAAwB,UAAA,EAA6B;AAC/E,EAAA,OAAO,OAAO,OAAA,KAAuE;AACnF,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,MAAM,SAAS,OAAA,CAAQ,MAAA;AACvB,IAAA,MAAM,KAAA,GAAQ,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA,CAAE,QAAA;AACnC,IAAA,MAAM,UAAA,GAAa,QAAA,YAAoB,QAAA,GAAW,QAAA,CAAS,MAAA,GAAS,GAAA;AACpE,IAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,YAAY,CAAA,IAAK,MAAA;AACvD,IAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,qBAAqB,CAAA,IAAK,MAAA;AAG7D,IAAA,IACE,KAAA,CAAM,UAAA,CAAW,SAAS,CAAA,IAC1B,KAAA,CAAM,UAAA,CAAW,UAAU,CAAA,IAC3B,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,IACrB,MAAM,QAAA,CAAS,MAAM,CAAA,IACrB,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,IACrB,KAAA,CAAM,SAAS,MAAM,CAAA,IACrB,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,IACrB,KAAA,CAAM,QAAA,CAAS,KAAK,CAAA,EACpB;AACA,MAAA,OAAO,QAAA;AAAA,IACT;AAEA,IAAA,OAAA,CAAQ,YAAA,CAAa;AAAA,MACnB,MAAA;AAAA,MACA,KAAA;AAAA,MACA,UAAA;AAAA,MACA,cAAA;AAAA,MACA,SAAA;AAAA,MACA;AAAA,KACD,CAAA;AAED,IAAA,OAAO,QAAA;AAAA,EACT,CAAA;AACF","file":"index.js","sourcesContent":["// withMonitor — wrapper de middleware Next.js que captura métricas HTTP\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/**\n * Envolve o middleware do Next.js para capturar métricas de request.\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { withMonitor } from '@victor-studio/monitor/next'\n * import { monitor } from '@/lib/monitor'\n *\n * function middleware(request: NextRequest) {\n * // sua lógica de middleware\n * return NextResponse.next()\n * }\n *\n * export default withMonitor(monitor, middleware)\n * ```\n */\nexport function withMonitor(monitor: MonitorClient, middleware?: NextMiddleware) {\n return async (request: NextRequest): Promise<NextResponse | Response | undefined> => {\n const start = performance.now()\n\n // Executar middleware original\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 // Extrair dados do request\n const method = request.method\n const route = new URL(request.url).pathname\n const statusCode = response instanceof Response ? response.status : 200\n const userAgent = request.headers.get('user-agent') ?? undefined\n const region = request.headers.get('x-vercel-ip-country') ?? undefined\n\n // Ignorar requests de assets estáticos\n if (\n route.startsWith('/_next/') ||\n route.startsWith('/favicon') ||\n route.endsWith('.ico') ||\n route.endsWith('.png') ||\n route.endsWith('.jpg') ||\n route.endsWith('.svg') ||\n route.endsWith('.css') ||\n route.endsWith('.js')\n ) {\n return response\n }\n\n monitor.trackRequest({\n method,\n route,\n statusCode,\n responseTimeMs,\n userAgent,\n region,\n })\n\n return response\n }\n}\n"]}
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"]}
@@ -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
- const page = typeof window !== "undefined" ? window.location.pathname : void 0;
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
  }
@@ -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;AAC1C,EAAA,MAAM,KAAK,SAAA,CAAU,SAAA;AACrB,EAAA,IAAI,iBAAA,CAAkB,IAAA,CAAK,EAAE,CAAA,EAAG,OAAO,QAAA;AACvC,EAAA,IAAI,cAAA,CAAe,IAAA,CAAK,EAAE,CAAA,EAAG,OAAO,QAAA;AACpC,EAAA,OAAO,SAAA;AACT;AAEA,SAAS,aAAA,GAAwB;AAC/B,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,SAAA;AAC1C,EAAA,MAAM,KAAK,SAAA,CAAU,SAAA;AACrB,EAAA,IAAI,EAAA,CAAG,QAAA,CAAS,SAAS,CAAA,EAAG,OAAO,SAAA;AACnC,EAAA,IAAI,EAAA,CAAG,QAAA,CAAS,MAAM,CAAA,EAAG,OAAO,MAAA;AAChC,EAAA,IAAI,EAAA,CAAG,QAAA,CAAS,QAAQ,CAAA,EAAG,OAAO,QAAA;AAClC,EAAA,IAAI,EAAA,CAAG,QAAA,CAAS,QAAQ,CAAA,EAAG,OAAO,QAAA;AAClC,EAAA,OAAO,OAAA;AACT;AAwBO,SAAS,aAAA,CAAc,EAAE,OAAA,EAAQ,EAAuB;AAC7D,EAAAA,eAAA,CAAU,MAAM;AAEd,IAAA,OAAO,YAAY,CAAA,CAAE,IAAA,CAAK,CAAC,EAAE,OAAO,KAAA,EAAO,KAAA,EAAO,KAAA,EAAO,MAAA,EAAO,KAAM;AACpE,MAAA,MAAM,OAAO,OAAO,MAAA,KAAW,WAAA,GAAc,MAAA,CAAO,SAAS,QAAA,GAAW,MAAA;AACxE,MAAA,MAAM,aAAa,gBAAA,EAAiB;AACpC,MAAA,MAAM,UAAU,aAAA,EAAc;AAE9B,MAAA,MAAM,MAAA,GAAS,CACb,IAAA,EACA,KAAA,EACA,MAAA,KACG;AACH,QAAA,OAAA,CAAQ,UAAA,CAAW,EAAE,IAAA,EAAM,KAAA,EAAO,QAAQ,IAAA,EAAM,UAAA,EAAY,SAAS,CAAA;AAAA,MACvE,CAAA;AAEA,MAAA,KAAA,CAAM,CAAC,WAAW,MAAA,CAAO,KAAA,EAAO,OAAO,KAAA,EAAO,MAAA,CAAO,MAAM,CAAC,CAAA;AAC5D,MAAA,KAAA,CAAM,CAAC,WAAW,MAAA,CAAO,KAAA,EAAO,OAAO,KAAA,EAAO,MAAA,CAAO,MAAM,CAAC,CAAA;AAC5D,MAAA,KAAA,CAAM,CAAC,WAAW,MAAA,CAAO,KAAA,EAAO,OAAO,KAAA,EAAO,MAAA,CAAO,MAAM,CAAC,CAAA;AAC5D,MAAA,KAAA,CAAM,CAAC,WAAW,MAAA,CAAO,KAAA,EAAO,OAAO,KAAA,EAAO,MAAA,CAAO,MAAM,CAAC,CAAA;AAC5D,MAAA,MAAA,CAAO,CAAC,WAAW,MAAA,CAAO,MAAA,EAAQ,OAAO,KAAA,EAAO,MAAA,CAAO,MAAM,CAAC,CAAA;AAAA,IAChE,CAAC,CAAA;AAAA,EACH,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,OAAO,IAAA;AACT","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 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 const ua = navigator.userAgent\n if (ua.includes('Firefox')) return 'Firefox'\n if (ua.includes('Edg/')) return 'Edge'\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 * // app/layout.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 // Import dinâmico pra não aumentar o bundle se não usar\n import('web-vitals').then(({ onLCP, onINP, onCLS, onFCP, onTTFB }) => {\n const page = typeof window !== 'undefined' ? window.location.pathname : undefined\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 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 }, [monitor])\n\n return null\n}\n"]}
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;AAG1C,EAAA,MAAM,SAAU,SAAA,CAAmE,aAAA;AACnF,EAAA,IAAI,MAAA,EAAQ;AACV,IAAA,OAAO,MAAA,CAAO,SAAS,QAAA,GAAW,SAAA;AAAA,EACpC;AAGA,EAAA,MAAM,KAAK,SAAA,CAAU,SAAA;AACrB,EAAA,IAAI,iBAAA,CAAkB,IAAA,CAAK,EAAE,CAAA,EAAG,OAAO,QAAA;AACvC,EAAA,IAAI,cAAA,CAAe,IAAA,CAAK,EAAE,CAAA,EAAG,OAAO,QAAA;AACpC,EAAA,OAAO,SAAA;AACT;AAEA,SAAS,aAAA,GAAwB;AAC/B,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,SAAA;AAG1C,EAAA,MAAM,SAAU,SAAA,CACb,aAAA;AACH,EAAA,IAAI,MAAA,EAAQ,QAAQ,MAAA,EAAQ;AAC1B,IAAA,MAAM,SAAA,GAAY,OAAO,MAAA,CAAO,IAAA;AAAA,MAC9B,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,KAAA,CAAM,UAAA,CAAW,KAAK,CAAA,IAAK,CAAC,CAAA,CAAE,KAAA,CAAM,UAAA,CAAW,UAAU;AAAA,KACrE;AACA,IAAA,IAAI,SAAA,SAAkB,SAAA,CAAU,KAAA;AAAA,EAClC;AAGA,EAAA,MAAM,KAAK,SAAA,CAAU,SAAA;AACrB,EAAA,IAAI,EAAA,CAAG,QAAA,CAAS,SAAS,CAAA,EAAG,OAAO,SAAA;AACnC,EAAA,IAAI,EAAA,CAAG,QAAA,CAAS,MAAM,CAAA,EAAG,OAAO,MAAA;AAChC,EAAA,IAAI,EAAA,CAAG,SAAS,MAAM,CAAA,IAAK,GAAG,QAAA,CAAS,OAAO,GAAG,OAAO,OAAA;AACxD,EAAA,IAAI,EAAA,CAAG,QAAA,CAAS,gBAAgB,CAAA,EAAG,OAAO,iBAAA;AAC1C,EAAA,IAAI,EAAA,CAAG,QAAA,CAAS,OAAO,CAAA,EAAG,OAAO,OAAA;AACjC,EAAA,IAAI,EAAA,CAAG,QAAA,CAAS,QAAQ,CAAA,EAAG,OAAO,QAAA;AAClC,EAAA,IAAI,EAAA,CAAG,QAAA,CAAS,QAAQ,CAAA,EAAG,OAAO,QAAA;AAClC,EAAA,OAAO,OAAA;AACT;AAuBO,SAAS,aAAA,CAAc,EAAE,OAAA,EAAQ,EAAuB;AAC7D,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,SAAA,GAAY,KAAA;AAGhB,IAAA,OAAO,YAAY,CAAA,CAAE,IAAA,CAAK,CAAC,EAAE,OAAO,KAAA,EAAO,KAAA,EAAO,KAAA,EAAO,MAAA,EAAO,KAAM;AACpE,MAAA,IAAI,SAAA,EAAW;AAEf,MAAA,MAAM,aAAa,gBAAA,EAAiB;AACpC,MAAA,MAAM,UAAU,aAAA,EAAc;AAE9B,MAAA,MAAM,MAAA,GAAS,CACb,IAAA,EACA,KAAA,EACA,MAAA,KACG;AAEH,QAAA,MAAM,OAAO,OAAO,MAAA,KAAW,WAAA,GAAc,MAAA,CAAO,SAAS,QAAA,GAAW,MAAA;AACxE,QAAA,OAAA,CAAQ,UAAA,CAAW,EAAE,IAAA,EAAM,KAAA,EAAO,QAAQ,IAAA,EAAM,UAAA,EAAY,SAAS,CAAA;AAAA,MACvE,CAAA;AAEA,MAAA,KAAA,CAAM,CAAC,WAAW,MAAA,CAAO,KAAA,EAAO,OAAO,KAAA,EAAO,MAAA,CAAO,MAAM,CAAC,CAAA;AAC5D,MAAA,KAAA,CAAM,CAAC,WAAW,MAAA,CAAO,KAAA,EAAO,OAAO,KAAA,EAAO,MAAA,CAAO,MAAM,CAAC,CAAA;AAC5D,MAAA,KAAA,CAAM,CAAC,WAAW,MAAA,CAAO,KAAA,EAAO,OAAO,KAAA,EAAO,MAAA,CAAO,MAAM,CAAC,CAAA;AAC5D,MAAA,KAAA,CAAM,CAAC,WAAW,MAAA,CAAO,KAAA,EAAO,OAAO,KAAA,EAAO,MAAA,CAAO,MAAM,CAAC,CAAA;AAC5D,MAAA,MAAA,CAAO,CAAC,WAAW,MAAA,CAAO,MAAA,EAAQ,OAAO,KAAA,EAAO,MAAA,CAAO,MAAM,CAAC,CAAA;AAAA,IAChE,CAAC,CAAA;AAED,IAAA,OAAO,MAAM;AACX,MAAA,SAAA,GAAY,IAAA;AAAA,IACd,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,OAAO,IAAA;AACT","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"]}