@bussolabs/closeyourit-js
v0.6.1
Published
Browser SDK for CloseYourIt — error and log monitoring for the web (self-hosted, Sentry-compatible wire format)
Readme
closeyourit-js
SDK JavaScript di CloseYourIt per il monitoring di errori, log e
performance — isomorfo: browser + Node 20.16+. TypeScript, zero dipendenze runtime, formato
wire Sentry-compatibile identico ai client
closeyourit-ruby e
closeyourit-dart.
Nel browser cattura automaticamente le eccezioni non gestite, i rifiuti di Promise, i log
strutturati e i breadcrumb (click, navigazione, console, richieste HTTP); su Node aggancia
uncaughtException/unhandledRejection e flusha su beforeExit/SIGTERM. In più: metriche
slow_method via measure() e — opt-in — verdetti performance (slow_external_http,
repeated_http, jank). Scrubbing PII lato client, invio fire-and-forget: non fa mai crashare
l'host.
Credenziali — server vs browser. Il progetto ha due credenziali per lo stesso token:
token— il bearer segretocyi_…. È a piena potenza (ingest e read): è una credenziale SERVER-ONLY. Usalo su Node/SSR (Authorization: Bearer). Non deve MAI finire in un bundle browser: chiunque lo estrarrebbe dal tab Network in pochi secondi.publicKey— la DSN public key (esadecimale, come losentry_keydi Sentry). Per design non è segreta: è la credenziale browser-safe. Nel browser l'SDK la usa via?sentry_key=sul path drop-in Sentry, mai il bearer.Nel browser configura
publicKey; se passi solo il bearer segreto, l'SDK resta no-op (avvisa in console) invece di esporlo. Prendi entrambe le credenziali dalla pagina del progetto su CloseYourIt.
Installazione
npm (bundler: Vite, webpack, Rollup, …)
pnpm add @bussolabs/closeyourit-js
# oppure: npm install @bussolabs/closeyourit-jsimport * as CloseYourIt from '@bussolabs/closeyourit-js'
CloseYourIt.init({
endpointUrl: 'https://www.closeyour.it',
publicKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', // DSN public key (browser-safe)
projectId: '00000000-0000-0000-0000-000000000000',
environment: 'production',
release: '[email protected]',
})<script> (IIFE, senza build step)
Il bundle IIFE espone il globale window.CloseYourIt (~7 KB gzip). In produzione pinna la
versione e aggiungi la Subresource Integrity
(integrity + crossorigin) per proteggerti da una compromissione della CDN:
<script
src="https://cdn.jsdelivr.net/npm/@bussolabs/[email protected]/dist/closeyourit.min.js"
integrity="sha384-…"
crossorigin="anonymous"
></script>
<script>
CloseYourIt.init({
endpointUrl: 'https://www.closeyour.it',
publicKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', // DSN public key (browser-safe)
projectId: '00000000-0000-0000-0000-000000000000',
environment: 'production',
})
</script>L'hash integrity reale della versione pubblicata si ottiene da jsDelivr (pulsante "SRI" sulla
pagina del file) o localmente con
curl -s https://cdn.jsdelivr.net/npm/@bussolabs/[email protected]/dist/closeyourit.min.js | openssl dgst -sha384 -binary | openssl base64 -A.
Metti lo <script> il più in alto possibile nel <head>: dopo init, i gestori globali
catturano ogni errore successivo, anche quelli sollevati durante il resto del caricamento.
Node 20.16+
Stesso package, stessa init. Su Node l'SDK installa i gestori di processo
(uncaughtException/unhandledRejection: cattura → flush → se non hai handler tuoi, esce con
codice 1 preservando la semantica crash di default) e flusha i buffer su beforeExit/SIGTERM.
Gli eventi Node portano anche server_name (hostname, parità con la gem): richiede
process.getBuiltinModule (Node ≥20.16) — da cui il minimo dichiarato in engines.
import * as CloseYourIt from '@bussolabs/closeyourit-js'
CloseYourIt.init({
endpointUrl: 'https://www.closeyour.it',
token: process.env.CLOSEYOURIT_TOKEN!, // bearer segreto: lecito lato server
projectId: process.env.CLOSEYOURIT_PROJECT_ID!,
environment: process.env.NODE_ENV ?? 'production',
})
// Cattura manuale + flush esplicito (es. script batch/CLI)
try {
await job()
} catch (e) {
CloseYourIt.captureException(e, { handled: true })
await CloseYourIt.flush()
throw e
}Su Node non c'è una richiesta implicita: nei server imposta il contesto per errore via
client.scope.setRequest({ url, method }) (es. da un middleware) o passa contexts all'hint.
Il wrapper automatico di fetch è solo browser: per l'HTTP server-side usa breadcrumb/measure
manuali.
Uso
Dopo init gli errori non gestiti e i rifiuti di Promise vengono catturati automaticamente. Le
API aggiuntive:
// Cattura manuale di un'eccezione gestita
try {
rischioso()
} catch (e) {
CloseYourIt.captureException(e, { handled: true })
}
// Messaggio diagnostico
CloseYourIt.captureMessage('cache miss sul carrello', 'warning')
// Log strutturato (bufferizzato e inviato in batch)
CloseYourIt.log('info', 'ordine creato', { order_id: 42, total_cents: 1990 })
// Logger nominato con metodi per livello (warn → warning sul wire)
CloseYourIt.logger.warn('risposta lenta dal gateway')
CloseYourIt.logger.named('payments').info('rimborso emesso', { order_id: 42 })
// Metrica slow_method: misura un blocco (sync o async) e invia oltre soglia
const totale = CloseYourIt.measure('checkout.total', () => calcolaTotale(cart))
const dati = await CloseYourIt.measure('api.orders', () => fetchOrders())
// Contesto allegato a errori e messaggi successivi
CloseYourIt.setUser({ id: 'acc_123' }) // senza sendPii viaggia solo l'id
CloseYourIt.setTag('area', 'checkout')
CloseYourIt.setTags({ tenant: 'acme', tier: 'gold' })
CloseYourIt.setExtra('plan', 'pro')
CloseYourIt.setContext('billing', { seats: 3 })
// Breadcrumb manuale (oltre a quelli automatici)
CloseYourIt.addBreadcrumb({ category: 'ui.action', message: 'apre modale pagamento' })
// Correlazione: il trace_id di pagina è generato all'init; sovrascrivibile
CloseYourIt.setTraceId(myRequestId)
CloseYourIt.getTraceId()
// Svuota i log bufferizzati e attende gli invii in volo (idempotente)
await CloseYourIt.flush()
// Spegne l'SDK e ripristina i patch globali
await CloseYourIt.close()Performance issues (opt-in)
Con detectPerformanceIssues: true l'SDK emette verdetti performance_issue su /metrics:
slow_external_http— unfetch/XHR verso un host esterno oltreslowExternalThresholdMs(default 1s). Il path è templatizzato (/v1/charges/<n>,/users/<uuid>), mai la query string.repeated_http— la stessa richiesta (host + path templatizzato) ripetutarepeatedHttpThresholdvolte (default 5) inrepeatedHttpWindowMs(default 5s): l'"N+1 del client". Emesso una volta per finestra.jank— long task del main thread oltrejankThresholdMs(default 100ms) viaPerformanceObserver(longtask); di fatto Chromium-only, altrove è un no-op silenzioso. Route =location.pathnametemplatizzato.
CloseYourIt.init({
/* … */
detectPerformanceIssues: true,
})Indipendentemente dall'opt-in, il breadcrumb http marca slow_external_http: true
(level: warning) sulle chiamate esterne oltre soglia — utile come contesto negli errori.
Pageview tracking (web analytics, opt-in)
Con trackPageviews: true l'SDK invia un pageview a POST /api/v1/projects/{projectId}/pageviews
al caricamento e a ogni navigazione SPA (pushState/replaceState/popstate, dedup sul
pathname: hash change e replaceState sullo stesso path non contano). Sostituisce Plausible:
cookieless, il visitatore è un hash giornaliero calcolato server-side — nel body viaggiano solo
hostname, pathname (mai la query), referrer e le 3 chiavi UTM estratte client-side.
CloseYourIt.init({
endpointUrl: 'https://www.closeyour.it',
publicKey: '…', // DSN public key (browser-safe)
projectId: '…',
environment: 'production',
trackPageviews: true,
})
// Pageview manuale (es. virtual pageview di un wizard)
CloseYourIt.capturePageview({ path: '/wizard/step-2' })Da <script> CDN (siti senza bundler — versione pinnata + SRI come in Installazione):
<script
src="https://cdn.jsdelivr.net/npm/@bussolabs/[email protected]/dist/closeyourit.min.js"
integrity="sha384-…"
crossorigin="anonymous"
></script>
<script>
CloseYourIt.init({
endpointUrl: '…',
publicKey: '…', // DSN public key (browser-safe)
projectId: '…',
trackPageviews: true,
})
</script>I pageview non sono campionati (sampleRate non si applica) e non passano da beforeSend.
Configurazione
init(options) accetta:
| Opzione | Tipo | Default | Descrizione |
| ------------------------- | --------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| endpointUrl | string | — (obbligatorio) | Base URL del backend, es. https://www.closeyour.it (senza path). |
| token | string | — | Bearer segreto (cyi_…), Authorization: Bearer. SERVER-ONLY: Node/SSR, mai nel bundle browser. |
| publicKey | string | — | DSN public key (hex, non segreta): credenziale browser-safe via ?sentry_key=. |
| projectId | string | — (obbligatorio) | UUID del progetto su CloseYourIt. |
| environment | string | 'production' | Nome ambiente. |
| release | string | auto (Node) | Versione/release applicativa. Auto-rilevata su Node dal deploy (APP_GIT_TAG/GIT_TAG semver, poi KAMAL_VERSION/GIT_SHA) se non passata; nel browser va passata esplicita (build-time inject). |
| sampleRate | number | 1.0 | Probabilità di invio di errori/messaggi. Le metriche non sono mai campionate. |
| sendPii | boolean | false | Con false: user ridotto a {id}, URL senza query, referrer strippato. |
| beforeSend | (payload) => payload \| null | — | Hook di redazione/scarto applicato a ogni payload (errori, log, metriche). |
| excludedExceptions | string[] | [] | Nomi di errore (error.name) da non inviare mai (es. "AbortError"). |
| breadcrumbs | boolean | true | Master switch dei breadcrumb automatici. |
| autoBreadcrumbs | {click,navigation,console,http} | tutti true | Interruttori granulari dei breadcrumb. |
| maxBreadcrumbs | number | 50 | Capienza del ring buffer dei breadcrumb. |
| captureRequest | boolean | true | Allega il contesto request (URL pagina, User-Agent, referrer) agli eventi (browser). |
| logsEnabled | boolean | true | Invio dei log strutturati. |
| logsSampleRate | number | 1.0 | Sampling dei log, indipendente da sampleRate. |
| logsBatchSize | number | 50 | Log accumulati che forzano il flush del batch. |
| logsFlushIntervalMs | number | 5000 | Flush periodico dei log. |
| logsMinLevel | LogLevel | 'info' | Livello minimo inviato (sotto soglia scartati client-side). |
| slowMethodThresholdMs | number | 200 | Soglia oltre cui measure() invia la metrica slow_method. |
| captureMethodArguments | boolean | false | Invia gli argomenti passati a measure(..., { args }) (scrubati, troncati). |
| detectPerformanceIssues | boolean | false | Master switch dei verdetti performance. |
| captureExternalHttp | boolean | true | Rileva le HTTP esterne (effettivo solo col detect attivo). |
| slowExternalThresholdMs | number | 1000 | Soglia HTTP esterna lenta: marca il breadcrumb e (col detect) emette il verdetto. 0 disattiva. |
| repeatedHttpThreshold | number | 5 | Richieste identiche nella finestra che emettono repeated_http. |
| repeatedHttpWindowMs | number | 5000 | Ampiezza della finestra repeated_http. |
| detectJank | boolean | true | Long task del main thread (effettivo solo col detect attivo). |
| jankThresholdMs | number | 100 | Durata di un long task oltre cui emettere jank. |
| maxQueue | number | 30 | Invii HTTP in volo prima di scartare (fire-and-forget, mai backpressure). |
| autoInstall | boolean | true | Installa le integrazioni della piattaforma. Disattivabile per test/setup custom. |
| debug | boolean | false | Log diagnostici interni su console. |
| enabled | boolean | true | Master switch: con false l'SDK è interamente no-op. |
Servono sempre endpointUrl e projectId, più una credenziale valida per l'ambiente: token
(bearer segreto) lato server, publicKey (DSN public key) nel browser. Se manca (o enabled: false),
l'SDK resta no-op senza sollevare: tutte le chiamate sono innocue. Nel browser, passare solo il
bearer segreto lascia l'SDK no-op (con console.warn) — non lo espone mai. Una init valida
successiva subentra a una configurazione incompleta; per re-inizializzare con opzioni diverse chiama
prima close().
Usa l'endpoint canonico
www.https://www.closeyour.itrisponde direttamente all'ingest; l'apex (https://closeyour.it) fa301 → wwwe, per unPOST, il browser può degradare la richiesta aGETperdendo il body. Su Node l'SDK segue da solo fino a 2 redirect preservando il POST (solo verso lo stesso host o la variantewww); nel browser punta sempre alwww.
Ricette framework
Nuxt 3 (client + server)
// plugins/closeyourit.client.ts — errori del browser
import * as CloseYourIt from '@bussolabs/closeyourit-js'
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
CloseYourIt.init({
endpointUrl: 'https://www.closeyour.it',
publicKey: config.public.closeyouritPublicKey, // public config: browser-safe
projectId: config.public.closeyouritProjectId,
environment: config.public.appEnv,
release: config.public.appVersion,
})
})// server/plugins/closeyourit.ts — errori SSR/Nitro
import * as CloseYourIt from '@bussolabs/closeyourit-js'
export default defineNitroPlugin((nitroApp) => {
const client = CloseYourIt.init({
endpointUrl: 'https://www.closeyour.it',
token: process.env.CLOSEYOURIT_TOKEN!,
projectId: process.env.CLOSEYOURIT_PROJECT_ID!,
environment: process.env.NODE_ENV ?? 'production',
})
nitroApp.hooks.hook('error', (error, { event }) => {
client.scope.setRequest(event ? { url: event.path, method: event.method } : null)
CloseYourIt.captureException(error, { handled: true })
})
})Nel browser (closeyourit.client.ts) usa la publicKey (DSN public key, non segreta): è
browser-safe, quindi va nella runtimeConfig.public (esposta al client). Il bearer segreto
token vive solo nel plugin server (server/plugins/closeyourit.ts, process.env mai
public): è SERVER-ONLY e non deve mai finire nel bundle browser né raggiungere il client.
Nel plugin server la release può essere omessa: su Node l'SDK la auto-rileva dal deploy
(tag semver APP_GIT_TAG/GIT_TAG, poi KAMAL_VERSION/GIT_SHA come fallback — parità tag-first
con la gem Ruby). Nel browser non c'è auto-detect: passa release esplicita (build-time inject,
es. config.public.appVersion). Passarla comunque su Node ha sempre la precedenza sull'auto-detect.
Angular (ErrorHandler)
// closeyourit.error-handler.ts
import { ErrorHandler, Injectable } from '@angular/core'
import * as CloseYourIt from '@bussolabs/closeyourit-js'
@Injectable()
export class CloseYourItErrorHandler implements ErrorHandler {
handleError(error: unknown): void {
CloseYourIt.captureException(error, { handled: false })
console.error(error)
}
}
// app.config.ts
import { ApplicationConfig } from '@angular/core'
import * as CloseYourIt from '@bussolabs/closeyourit-js'
CloseYourIt.init({
endpointUrl: 'https://www.closeyour.it',
publicKey: environment.closeyouritPublicKey, // DSN public key (browser-safe)
projectId: environment.closeyouritProjectId,
environment: environment.production ? 'production' : 'development',
})
export const appConfig: ApplicationConfig = {
providers: [{ provide: ErrorHandler, useClass: CloseYourItErrorHandler }],
}Angular inghiotte gli errori di zona prima che arrivino a window.onerror: l'ErrorHandler è il
punto giusto. I gestori globali dell'SDK restano utili per gli errori fuori da Angular.
Privacy e PII
Tre livelli di difesa prima dell'invio:
- Policy
sendPii(default off):userviaggia ridotto a{id}(email & co. non partono proprio), gli URL sono privati di query string, il referrer è strippato. ConsendPii: truedichiari esplicitamente l'intento di inviare l'utente intero e gli URL completi. - Scrubbing automatico: i valori le cui chiavi combaciano con la denylist sensibile
(
pass,secret,token,api_key,authorization,cookie,csrf,credit,card,cvv,ssn,iban,email) vengono sostituiti con[FILTERED]in modo ricorsivo, in tutto ciò che viene serializzato (tag, extra, context, attributi dei log,datadei breadcrumb, argomenti dimeasure). Le assegnazionichiave_sensibile=valoredentro ai messaggi di errore sono redatte anch'esse. La denylist è un superset di quella del backend (aggiungeemail); il backend ri-scruba comunque. beforeSend: hook finale sotto il tuo controllo — muta il payload o ritornanullper scartarlo del tutto. Vale per errori, messaggi, log e metriche.
Lo User-Agent custom viaggia solo su Node (nel browser è un forbidden header e allargherebbe la
preflight CORS).
Contratto wire
POST con Content-Type: application/json. L'autenticazione dipende dall'ambiente:
- Server (Node/SSR) —
Authorization: Bearer <token>(bearer segreto) sui path/api/v1/projects/*. - Browser — DSN public key via
?sentry_key=<publicKey>(nessun header di auth). Gli errori/messaggi vanno al drop-in SentryPOST {endpointUrl}/api/{projectId}/store(stesso payload evento); gli altri canali usano gli stessi path/api/v1/projects/*con?sentry_key=.
| Cosa | Endpoint (server, bearer) | Payload |
| ----------------- | -------------------------------------------------------- | ------------------------------------------ |
| Errori / messaggi | POST {endpointUrl}/api/v1/projects/{projectId}/events | oggetto evento (formato Sentry) |
| Metriche | POST {endpointUrl}/api/v1/projects/{projectId}/metrics | kind: slow_method \| performance_issue |
| Log | POST {endpointUrl}/api/v1/projects/{projectId}/logs | array di voci (chunk ≤ 1000 per richiesta) |
Nel browser gli errori/messaggi passano invece dal drop-in Sentry
POST {endpointUrl}/api/{projectId}/store. Campi snake_case, formato Sentry (event_id,
exception.values[], contexts, sdk, trace_id). Il backend risponde 2xx. Nel browser gli invii
usano fetch(keepalive: true) (sotto la quota di 64 KB) così sopravvivono a
pagehide/visibilitychange; il transport non solleva mai e non fa retry loop. getStats() espone i
contatori { enqueued, sent, dropped, failed }.
CORS: l'ingest di CloseYourIt espone CORS sugli endpoint
/api/v1/projects/*e sul drop-in Sentry/api/:id/{store,envelope}(Authorization+Content-Type,POST/OPTIONS). Nessun cookie/credenziale di sessione è coinvolto: l'auth è il Bearer (server) o la DSN public key in query?sentry_key=(browser). La public key non è segreta — la query non allarga la preflight (soloContent-Type); l'abuso dal browser è mitigato lato backend da rate limit e allowlist di Origin, non dalla segretezza della chiave.
Dimensione bundle
| Output | File | Dimensione |
| --------------- | ------------------------- | -------------------------------------------------- |
| IIFE minificato | dist/closeyourit.min.js | ~7 KB gzip |
| ESM | dist/index.mjs | per bundler (tree-shakeable, sideEffects: false) |
| CJS | dist/index.js | per ambienti CommonJS |
| Tipi | dist/index.d.ts | TypeScript |
Sviluppo
Toolchain pinnata con mise (mise.toml): Node 24 + pnpm 11.
mise install
mise exec -- pnpm install
mise exec -- pnpm format:check # prettier (gate anche in CI)
mise exec -- pnpm lint # tsc --noEmit (strict)
mise exec -- pnpm build # tsup -> dist/ (ESM + CJS + IIFE + tipi)
mise exec -- pnpm test # vitest (jsdom + node), con gate coverageI test girano in jsdom (browser) e in ambiente node (*.node.test.ts) con fetch mockato; la
coverage v8 su src/** ha un gate (linee/statement/funzioni ≥ 90 %, branch ≥ 85 %) — le soglie
sono un ratchet, si alzano soltanto.
Licenza
MIT © 2026 Alessio Bussolari
