npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 segreto cyi_…. È 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 lo sentry_key di 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-js
import * 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 — un fetch/XHR verso un host esterno oltre slowExternalThresholdMs (default 1s). Il path è templatizzato (/v1/charges/<n>, /users/<uuid>), mai la query string.
  • repeated_http — la stessa richiesta (host + path templatizzato) ripetuta repeatedHttpThreshold volte (default 5) in repeatedHttpWindowMs (default 5s): l'"N+1 del client". Emesso una volta per finestra.
  • jank — long task del main thread oltre jankThresholdMs (default 100ms) via PerformanceObserver(longtask); di fatto Chromium-only, altrove è un no-op silenzioso. Route = location.pathname templatizzato.
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.it risponde direttamente all'ingest; l'apex (https://closeyour.it) fa 301 → www e, per un POST, il browser può degradare la richiesta a GET perdendo il body. Su Node l'SDK segue da solo fino a 2 redirect preservando il POST (solo verso lo stesso host o la variante www); nel browser punta sempre al www.

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): user viaggia ridotto a {id} (email & co. non partono proprio), gli URL sono privati di query string, il referrer è strippato. Con sendPii: true dichiari 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, data dei breadcrumb, argomenti di measure). Le assegnazioni chiave_sensibile=valore dentro ai messaggi di errore sono redatte anch'esse. La denylist è un superset di quella del backend (aggiunge email); il backend ri-scruba comunque.
  • beforeSend: hook finale sotto il tuo controllo — muta il payload o ritorna null per 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 Sentry POST {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 (solo Content-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 coverage

I 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