@monzori/browser 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,43 @@
1
- # Temporary Holding Version
1
+ # Monzori Browser SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Captura erros globais, promises rejeitadas, reporte manual e console error/warn. Demais níveis de console são opt-in. O adaptador React exporta MonzoriErrorBoundary; React é peer opcional e fica fora do núcleo.
4
+
5
+ Instale com npm install @monzori/browser. Versão inicial para piloto privado.
6
+
7
+ O endpoint hospedado depende da implantação do serviço. Configure um endpoint explícito para testes locais.
8
+
9
+ ```ts
10
+ import Monzori from '@monzori/browser';
11
+
12
+ const monzori = Monzori.start({
13
+ apiKey: 'IDENTIFICADOR_PUBLICO_GERADO_NO_PAINEL',
14
+ release: 'web-2026.11.10',
15
+ console: { log: false, info: false, debug: false }
16
+ });
17
+
18
+ monzori.captureException(new Error('Evento explícito de teste'));
19
+ ```
20
+
21
+ O endereço padrão é `https://ingest.monzori.com.br/v1/web/events`. `apiKey` é o identificador público restrito à ingestão da fonte, não uma senha de administração. A inicialização devolve o client usado no Error Boundary e no reporte manual. Há também o export nomeado `start`.
22
+
23
+ `endpoint` é uma opção avançada: para testes locais, informar `http://localhost:3333/v1/web/events`; destinos fora de loopback exigem HTTPS. Um endpoint explícito inválido desabilita o envio, sem fallback silencioso. O endereço padrão não implica que a infraestrutura do piloto já esteja publicada.
24
+
25
+ Compatibilidade: `initMonzori({publicKey, release, endpoint})` continua funcionando como alias legado, usando o mesmo client; preferir `Monzori.start({apiKey, release})` nas novas integrações. Escolher `apiKey` ou `publicKey`, sem combinar os dois. A primeira inicialização fica ativa até `close()`, sem duplicar listeners em chamadas subsequentes.
26
+
27
+ ```tsx
28
+ import { MonzoriErrorBoundary } from '@monzori/browser/react';
29
+
30
+ <MonzoriErrorBoundary client={monzori} fallback={<p>Não foi possível exibir este conteúdo.</p>}>
31
+ <App />
32
+ </MonzoriErrorBoundary>
33
+ ```
34
+
35
+ Cadastre a origem HTTPS exata no painel; HTTP é aceito apenas em loopback local. O identificador público só admite eventos naquele vínculo, sem permitir leitura ou upload de mapas. A URL de ingresso deve aceitar CORS para a origem cadastrada; a instalação local usa http://localhost:3333/v1/web/events. Release precisa coincidir com o upload privado dos source maps. Identificadores de release devem ser únicos por versão; não reutilizá-los para substituir mapas de um mesmo artefato.
36
+
37
+ Buffer somente em memória, sem cookies/storage/DOM/formulários/identificação de visitantes automáticos. Limites: 100 eventos/256 KiB, flush a cada 5 segundos ou 20 eventos, até 50 eventos e 32 KiB por pedido. O SDK reserva espaço para contexto do servidor e descarta um evento serializado acima de 6 KiB; o teto canônico na API é 8 KiB. Reenvio com o mesmo ID, backoff de 1–60 segundos com jitter, até 24 horas de idade; fechar a página pode perder eventos não admitidos. No máximo 60 entradas de console por minuto/página. O SDK não registra seus próprios diagnósticos no console monitorado.
38
+
39
+ URL, mensagens, stacks e atributos são sanitizados antes do envio; antes do commit há uma segunda sanitização no servidor. beforeSend permite remover eventos ou ajustar conteúdo. Nenhuma heurística remove todo segredo ou dado pessoal de texto livre: evite emiti-los. Contexto usa apenas propriedades próprias de objetos simples, sem avaliar getters; a stack de um Error real é lida explicitamente para suportar engines com stack lazy.
40
+
41
+ monzori.stats() mostra pendências, bytes, descartes, reenvios e confirmações HTTP locais. Confirmado não significa já pesquisável. monzori.flush() tenta enviar os pendentes, respeitando backoff. monzori.close() encerra captura/timers e restaura os métodos de console se eles ainda forem os wrappers instalados pelo SDK.
42
+
43
+ Source maps v3 planos Vite/Rollup e Webpack são enviados de forma privada pelo painel/API autenticada, fora do SDK. Não publicar .map no bucket/CDN público. O backend só consulta mapas cadastrados para organização/aplicação/release/URL do artefato exatos; nunca busca sourceMappingURL na Internet. Sem mapa, o painel preserva a stack recebida.
@@ -0,0 +1,52 @@
1
+ export type Kind = 'js_error' | 'promise_rejection' | 'react_error' | 'console' | 'manual';
2
+ export type Severity = 'error' | 'warn' | 'info' | 'debug';
3
+ type BaseOptions = {
4
+ endpoint?: string;
5
+ release: string;
6
+ console?: false | {
7
+ log?: boolean;
8
+ info?: boolean;
9
+ debug?: boolean;
10
+ };
11
+ beforeSend?: (event: WebEvent) => WebEvent | null;
12
+ };
13
+ export type Options = BaseOptions & ({
14
+ apiKey: string;
15
+ publicKey?: never;
16
+ } | {
17
+ publicKey: string;
18
+ apiKey?: never;
19
+ });
20
+ export declare const DEFAULT_ENDPOINT = "https://ingest.monzori.com.br/v1/web/events";
21
+ export type WebEvent = {
22
+ event_id: string;
23
+ occurred_at: string;
24
+ kind: Kind;
25
+ severity: Severity;
26
+ message: string;
27
+ stack: string;
28
+ url: string;
29
+ release: string;
30
+ attributes: unknown;
31
+ };
32
+ export type Client = {
33
+ captureException: (error: unknown, context?: unknown, kind?: Kind) => void;
34
+ captureMessage: (message: string, severity?: Severity, context?: unknown) => void;
35
+ flush: () => Promise<void>;
36
+ close: () => void;
37
+ stats: () => {
38
+ pending: number;
39
+ bytes: number;
40
+ dropped: number;
41
+ retries: number;
42
+ confirmed: number;
43
+ disabled: boolean;
44
+ };
45
+ };
46
+ export declare function start(options: Options): Client;
47
+ /** @deprecated Use Monzori.start(options) or the named start export. */
48
+ export declare const initMonzori: typeof start;
49
+ declare const Monzori: {
50
+ start: typeof start;
51
+ };
52
+ export default Monzori;
package/dist/index.js ADDED
@@ -0,0 +1,219 @@
1
+ import { cleanText, cleanUrl, safeValue } from './sanitize.js';
2
+ export const DEFAULT_ENDPOINT = 'https://ingest.monzori.com.br/v1/web/events';
3
+ let active;
4
+ const encode = new TextEncoder();
5
+ export function start(options) {
6
+ if (active)
7
+ return active;
8
+ let endpoint = '', apiKey = '', disabled = false;
9
+ try {
10
+ apiKey = options.apiKey ?? options.publicKey ?? '';
11
+ const parsed = new URL(options.endpoint === undefined ? DEFAULT_ENDPOINT : options.endpoint);
12
+ if (parsed.protocol !== 'https:' && !(['localhost', '127.0.0.1', '[::1]'].includes(parsed.hostname) && parsed.protocol === 'http:'))
13
+ throw Error();
14
+ if (parsed.username || parsed.password || parsed.search || parsed.hash)
15
+ throw Error();
16
+ endpoint = parsed.href;
17
+ if ((options.apiKey !== undefined && options.publicKey !== undefined) || !/^[A-Za-z0-9_-]{43}$/.test(apiKey) || !/^[A-Za-z0-9_.\/-]{1,128}$/.test(options.release))
18
+ throw Error();
19
+ }
20
+ catch {
21
+ disabled = true;
22
+ }
23
+ if (typeof window === 'undefined' || typeof crypto === 'undefined' || typeof fetch === 'undefined' || typeof AbortController === 'undefined')
24
+ disabled = true;
25
+ let queue = [], bytes = 0, dropped = 0, retries = 0, confirmed = 0, closed = false, attempt = 0, nextAttempt = 0;
26
+ let pending;
27
+ let controller;
28
+ const protectedIds = new Set();
29
+ const restorers = [];
30
+ const expired = () => { const keep = queue.filter(entry => { if (Date.now() - Date.parse(entry.event.occurred_at) > 86400000) {
31
+ bytes -= entry.size;
32
+ dropped++;
33
+ return false;
34
+ } return true; }); queue = keep; };
35
+ function capture(message, stack, kind, severity, context) {
36
+ if (disabled || closed)
37
+ return;
38
+ try {
39
+ let event = { event_id: crypto.randomUUID(), occurred_at: new Date().toISOString(), kind, severity, message: cleanText(message, 1200) || 'Evento sem mensagem', stack: cleanText(stack, 4000), url: cleanUrl(window.location.href), release: options.release, attributes: safeValue(context ?? {}) };
40
+ if (options.beforeSend) {
41
+ const candidate = options.beforeSend(event);
42
+ if (!candidate) {
43
+ dropped++;
44
+ return;
45
+ }
46
+ event = { ...candidate, event_id: event.event_id, occurred_at: event.occurred_at, kind, severity, release: options.release, message: cleanText(String(candidate.message), 1200), stack: cleanText(String(candidate.stack), 4000), url: cleanUrl(String(candidate.url)), attributes: safeValue(candidate.attributes) };
47
+ }
48
+ // Reserve room for the server's tenant/application context inside the 8 KiB canonical ceiling.
49
+ const size = encode.encode(JSON.stringify(event)).length;
50
+ if (size > 6144) {
51
+ dropped++;
52
+ return;
53
+ }
54
+ expired();
55
+ while (queue.length >= 100 || bytes + size > 256 * 1024) {
56
+ const index = queue.findIndex(e => !protectedIds.has(e.event.event_id));
57
+ if (index < 0) {
58
+ dropped++;
59
+ return;
60
+ }
61
+ bytes -= queue[index].size;
62
+ queue.splice(index, 1);
63
+ dropped++;
64
+ }
65
+ queue.push({ event, size });
66
+ bytes += size;
67
+ if (queue.length >= 20)
68
+ void flush();
69
+ }
70
+ catch {
71
+ dropped++;
72
+ }
73
+ }
74
+ function exception(error, context, kind = 'manual') {
75
+ try {
76
+ const object = error && typeof error === 'object' ? error : {};
77
+ const message = Object.getOwnPropertyDescriptor(object, 'message')?.value;
78
+ let stack = Object.getOwnPropertyDescriptor(object, 'stack')?.value;
79
+ // Modern engines expose Error.stack lazily. Read that explicit Error field, never arbitrary context getters.
80
+ if (typeof stack !== 'string' && error instanceof Error) {
81
+ try {
82
+ stack = error.stack;
83
+ }
84
+ catch {
85
+ stack = '';
86
+ }
87
+ }
88
+ capture(typeof message === 'string' ? message : typeof error === 'string' ? error : 'Erro sem mensagem', typeof stack === 'string' ? stack : '', kind, 'error', context);
89
+ }
90
+ catch {
91
+ dropped++;
92
+ }
93
+ }
94
+ function flush() {
95
+ if (pending)
96
+ return pending;
97
+ if (disabled || closed || Date.now() < nextAttempt)
98
+ return Promise.resolve();
99
+ expired();
100
+ if (!queue.length)
101
+ return Promise.resolve();
102
+ const batch = [];
103
+ let size = 20;
104
+ for (const entry of queue) {
105
+ if (batch.length >= 50 || size + entry.size + 1 > 32 * 1024)
106
+ break;
107
+ batch.push(entry);
108
+ size += entry.size + 1;
109
+ protectedIds.add(entry.event.event_id);
110
+ }
111
+ if (!batch.length)
112
+ return Promise.resolve();
113
+ pending = Promise.resolve().then(async () => {
114
+ controller = new AbortController();
115
+ const timeout = setTimeout(() => controller?.abort(), 3000);
116
+ try {
117
+ const response = await fetch(endpoint, { method: 'POST', headers: { 'content-type': 'application/json', 'x-monzori-key': apiKey }, body: JSON.stringify({ events: batch.map(e => e.event) }), credentials: 'omit', referrerPolicy: 'no-referrer', keepalive: true, signal: controller.signal });
118
+ if (response.status === 202 || [400, 409, 413, 415, 422].includes(response.status)) {
119
+ const ids = new Set(batch.map(e => e.event.event_id));
120
+ queue = queue.filter(e => { if (ids.has(e.event.event_id)) {
121
+ bytes -= e.size;
122
+ return false;
123
+ } return true; });
124
+ if (response.status === 202)
125
+ confirmed += batch.length;
126
+ else
127
+ dropped += batch.length;
128
+ attempt = 0;
129
+ nextAttempt = 0;
130
+ }
131
+ else if (response.status === 401 || response.status === 403) {
132
+ disabled = true;
133
+ dropped += queue.length;
134
+ queue = [];
135
+ bytes = 0;
136
+ }
137
+ else
138
+ throw Error('retry');
139
+ }
140
+ catch {
141
+ if (!closed) {
142
+ retries++;
143
+ const delay = Math.min(60000, 1000 * 2 ** Math.min(attempt++, 6));
144
+ nextAttempt = Date.now() + Math.max(1000, delay * (.5 + Math.random() * .5));
145
+ }
146
+ }
147
+ finally {
148
+ clearTimeout(timeout);
149
+ protectedIds.clear();
150
+ pending = undefined;
151
+ controller = undefined;
152
+ }
153
+ });
154
+ return pending;
155
+ }
156
+ const client = { captureException: exception, captureMessage: (message, severity = 'info', context) => capture(message, '', 'manual', severity, context), flush,
157
+ stats: () => ({ pending: queue.length, bytes, dropped, retries, confirmed, disabled }),
158
+ close: () => { closed = true; controller?.abort(); restorers.forEach(fn => fn()); queue = []; bytes = 0; if (active === client)
159
+ active = undefined; }
160
+ };
161
+ if (!disabled) {
162
+ const onError = (event) => { if (event.error)
163
+ exception(event.error, {}, 'js_error');
164
+ else if (event.message)
165
+ capture(event.message, event.filename ? `${event.filename}:${event.lineno}:${event.colno}` : '', 'js_error', 'error'); };
166
+ const onRejection = (event) => exception(event.reason, {}, 'promise_rejection');
167
+ const onVisibility = () => { if (document.visibilityState === 'hidden')
168
+ void flush(); };
169
+ window.addEventListener('error', onError);
170
+ window.addEventListener('unhandledrejection', onRejection);
171
+ document.addEventListener('visibilitychange', onVisibility);
172
+ restorers.push(() => { window.removeEventListener('error', onError); window.removeEventListener('unhandledrejection', onRejection); document.removeEventListener('visibilitychange', onVisibility); });
173
+ if (options.console !== false) {
174
+ let minute = 0, count = 0;
175
+ const levels = ['error', 'warn', ...(options.console?.log ? ['log'] : []), ...(options.console?.info ? ['info'] : []), ...(options.console?.debug ? ['debug'] : [])];
176
+ for (const level of levels) {
177
+ const original = console[level];
178
+ const wrapped = (...args) => {
179
+ const result = Reflect.apply(original, console, args);
180
+ try {
181
+ const now = Math.floor(Date.now() / 60000);
182
+ if (now !== minute) {
183
+ minute = now;
184
+ count = 0;
185
+ }
186
+ if (count++ >= 60)
187
+ dropped++;
188
+ else {
189
+ const values = args.slice(0, 20), error = values.find(value => value instanceof Error);
190
+ let stack = '';
191
+ if (error instanceof Error) {
192
+ try {
193
+ stack = error.stack ?? '';
194
+ }
195
+ catch { /* Optional console stack. */ }
196
+ }
197
+ capture(values.map(a => typeof a === 'string' ? cleanText(a, 1000) : a instanceof Error ? String(Object.getOwnPropertyDescriptor(a, 'message')?.value ?? '[Error]') : JSON.stringify(safeValue(a))).join(' '), stack, 'console', level === 'log' ? 'info' : level);
198
+ }
199
+ }
200
+ catch {
201
+ dropped++;
202
+ }
203
+ return result;
204
+ };
205
+ console[level] = wrapped;
206
+ restorers.push(() => { if (console[level] === wrapped)
207
+ console[level] = original; });
208
+ }
209
+ }
210
+ const timer = setInterval(() => void flush(), 5000);
211
+ restorers.push(() => clearInterval(timer));
212
+ }
213
+ active = client;
214
+ return client;
215
+ }
216
+ /** @deprecated Use Monzori.start(options) or the named start export. */
217
+ export const initMonzori = start;
218
+ const Monzori = { start };
219
+ export default Monzori;
@@ -0,0 +1,18 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from 'react';
2
+ import type { Client } from './index.js';
3
+ export declare class MonzoriErrorBoundary extends Component<{
4
+ client: Client;
5
+ children: ReactNode;
6
+ fallback?: ReactNode;
7
+ }, {
8
+ failed: boolean;
9
+ }> {
10
+ state: {
11
+ failed: boolean;
12
+ };
13
+ static getDerivedStateFromError(): {
14
+ failed: boolean;
15
+ };
16
+ componentDidCatch(error: unknown, info: ErrorInfo): void;
17
+ render(): ReactNode;
18
+ }
package/dist/react.js ADDED
@@ -0,0 +1,10 @@
1
+ import { Component, createElement } from 'react';
2
+ export class MonzoriErrorBoundary extends Component {
3
+ constructor() {
4
+ super(...arguments);
5
+ this.state = { failed: false };
6
+ }
7
+ static getDerivedStateFromError() { return { failed: true }; }
8
+ componentDidCatch(error, info) { this.props.client.captureException(error, { componentStack: info.componentStack ?? '' }, 'react_error'); }
9
+ render() { return this.state.failed ? this.props.fallback ?? createElement('p', null, 'Não foi possível exibir este conteúdo.') : this.props.children; }
10
+ }
@@ -0,0 +1,6 @@
1
+ export declare function cleanUrl(value: string): string;
2
+ export declare function cleanText(text: string, max?: number): string;
3
+ export declare function safeValue(value: unknown, depth?: number, budget?: {
4
+ nodes: number;
5
+ characters: number;
6
+ }): unknown;
@@ -0,0 +1,57 @@
1
+ const sensitive = /password|passwd|secret|authorization|cookie|token|api[_-]?key|localstorage|sessionstorage/i;
2
+ export function cleanUrl(value) { try {
3
+ const u = new URL(value);
4
+ return ['http:', 'https:'].includes(u.protocol) ? `${u.origin}${u.pathname}` : '';
5
+ }
6
+ catch {
7
+ return '';
8
+ } }
9
+ export function cleanText(text, max = 4000) { return text.slice(0, 16000).replace(/https?:\/\/[^\s'"<>\)]+/g, raw => { const suffix = raw.match(/(:\d+:\d+)$/)?.[1] ?? ''; return cleanUrl(suffix ? raw.slice(0, -suffix.length) : raw) + suffix; }).replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [redacted]').replace(/\b(password|passwd|secret|token|api[_-]?key|authorization|cookie)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi, '$1=[redacted]').slice(0, max); }
10
+ export function safeValue(value, depth = 0, budget = { nodes: 128, characters: 2000 }) {
11
+ try {
12
+ if (budget.nodes-- <= 0)
13
+ return '[value limit]';
14
+ if (depth > 3)
15
+ return '[depth limit]';
16
+ if (typeof value === 'string') {
17
+ if (budget.characters <= 0)
18
+ return '[string limit]';
19
+ const text = cleanText(value.slice(0, Math.min(1000, budget.characters)), 1000);
20
+ budget.characters -= text.length;
21
+ return text;
22
+ }
23
+ if (typeof value === 'number')
24
+ return Number.isFinite(value) ? value : String(value);
25
+ if (typeof value === 'boolean' || value === null)
26
+ return value;
27
+ if (typeof value === 'function')
28
+ return '[function]';
29
+ if (typeof value !== 'object')
30
+ return cleanText(String(value), 100);
31
+ const prototype = Object.getPrototypeOf(value);
32
+ if (prototype !== Object.prototype && prototype !== null && !Array.isArray(value) && !(value instanceof Error))
33
+ return '[non-plain object]';
34
+ const keys = [];
35
+ for (const key in value) {
36
+ if (Object.prototype.hasOwnProperty.call(value, key))
37
+ keys.push(key);
38
+ if (keys.length >= 20)
39
+ break;
40
+ }
41
+ const out = {};
42
+ for (const key of keys.sort()) {
43
+ if (budget.nodes <= 0) {
44
+ out['...'] = '[value limit]';
45
+ break;
46
+ }
47
+ const item = Object.getOwnPropertyDescriptor(value, key);
48
+ if (!item)
49
+ continue;
50
+ out[key.slice(0, 80)] = sensitive.test(key) ? '[redacted]' : 'value' in item ? safeValue(item.value, depth + 1, budget) : '[accessor omitted]';
51
+ }
52
+ return out;
53
+ }
54
+ catch {
55
+ return '[unavailable]';
56
+ }
57
+ }
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
- "name": "@monzori/browser",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@monzori/browser",
3
+ "version": "0.1.1",
4
+ "description": "Browser error and console collection SDK for Monzori",
5
+ "publishConfig": {
6
+ "access": "public",
7
+ "registry": "https://registry.npmjs.org/"
8
+ },
9
+ "type": "module",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./react": {
16
+ "types": "./dist/react.d.ts",
17
+ "default": "./dist/react.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist/index.js",
22
+ "dist/index.d.ts",
23
+ "dist/react.js",
24
+ "dist/react.d.ts",
25
+ "dist/sanitize.js",
26
+ "dist/sanitize.d.ts"
27
+ ],
28
+ "sideEffects": false,
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json",
31
+ "typecheck": "tsc --noEmit -p tsconfig.json",
32
+ "test": "vitest run"
33
+ },
34
+ "peerDependencies": {
35
+ "react": ">=18 <20"
36
+ },
37
+ "peerDependenciesMeta": {
38
+ "react": {
39
+ "optional": true
40
+ }
41
+ },
42
+ "devDependencies": {
43
+ "@types/react": "^19.2.2",
44
+ "vitest": "^5.1.0"
45
+ }
46
+ }