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

@racoelho/analisa-desktop

v0.1.0

Published

Analisa analytics SDK for desktop apps (Electron and Tauri) on Windows, macOS and Linux

Readme

@racoelho/analisa-desktop

SDK do Analisa (analytics com privacidade) para apps desktop Electron e Tauri v2 no Windows, macOS e Linux. Implementa o protocolo de ingestão v1: telas, sessões, ciclo de vida do app, crashes e fila offline persistente.

English version below — jump to English.

npm install @racoelho/analisa-desktop

| Import | Onde roda | |---|---| | @racoelho/analisa-desktop/electron | Processo principal do Electron | | @racoelho/analisa-desktop/electron/preload | Preload do Electron (window.analisa no renderer) | | @racoelho/analisa-desktop/tauri | Front-end (webview) do Tauri v2 | | @racoelho/analisa-desktop | Núcleo agnóstico (AnalisaCore) e tipos do protocolo |

Configure o app no painel

Apps desktop mandam eventos sem Origin de navegador. No painel, em Configurações do app:

  1. Ligue "Receber eventos de apps" (allow_native). Sem isso o ingestor responde 403 native_not_allowed e o SDK para de enviar (e avisa no console).
  2. Opcional: preencha "App ids permitidos" (allowed_app_ids) com o app id que o SDK manda (appId no Electron; o identifier do tauri.conf.json no Tauri).
  3. Nada a fazer para o Tauri: os Origins dos webviews do Tauri (tauri://localhost no macOS/Linux e http(s)://tauri.localhost no WebView2 do Windows) são tratados como app nativo, ou seja, valem as regras dos itens 1 e 2. Não precisa mexer em "Origens permitidas".

Electron

1. Processo principal

// main.ts
import { app, BrowserWindow } from 'electron'
import { init } from '@racoelho/analisa-desktop/electron'

const analisa = init({
  siteKey: 'pk_...',
  appId: 'com.empresa.app',   // o appId do electron-builder; vai em X-Analisa-App-Id e context.app.id
  appBuild: '451',            // opcional
  // endpoint: 'https://seu-ingestor/v1/i',
})

app.whenReady().then(() => {
  const win = new BrowserWindow({
    webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true },
  })
  analisa.screen('Home')
})

O que acontece sozinho:

  • Contexto: process.platform (win32→windows, darwin→macos, linux), process.getSystemVersion() (fallback os.release()), app.getVersion(), app id, app.getLocale(), fuso, tela principal (screen.getPrimaryDisplay(), atualizada quando muda), device.type: "desktop", framework: "electron", sdk.name: "analisa-electron".
  • Envio do processo principal com o fetch do Node (Electron ≥ 28): sem Origin, com User-Agent e X-Analisa-App-Id. Não envia quando net.isOnline() é falso.
  • Persistência em app.getPath('userData')/analisa/ (state.json, queue.json, crash.json).
  • Ciclo de vida: app_open {launch:"cold"} no ready (com first_launch na primeira vez), app_update quando a versão/build mudou, app_background/app_foreground quando nenhuma janela tem foco (browser-window-blur/focus, powerMonitor suspend/resume/lock). Com backgroundOn: 'visibility', conta só janelas escondidas/minimizadas. No before-quit o SDK segura o quit por até 2 s para mandar a fila.
  • Crashes: uncaughtExceptionMonitor grava o erro no disco de forma síncrona (sem rede) e ele sai no próximo lançamento com o ts, a sessão e a versão do crash, antes do app_open (error_kind: "crash"). Se o processo sobrevive (ex.: só apareceu o diálogo de erro do Electron), vai na hora como unhandled. unhandledRejection vira unhandled (o SDK repete o aviso no console). render-process-gone e child-process-gone (exceto clean-exit/killed) viram crash com process_type e exit_reason.

2. Preload (bridge para o renderer)

// preload.ts
import { exposeAnalisa } from '@racoelho/analisa-desktop/electron/preload'
exposeAnalisa() // window.analisa; também manda erros não tratados do renderer

Com sandbox: true (padrão desde o Electron 20) o preload só pode dar require('electron'): empacote o preload (Vite, esbuild, webpack — o electron-vite já faz isso). O helper não importa o núcleo; ele só repassa as chamadas por IPC (analisa:call) para o processo principal, que valida os argumentos. Desligue o bridge com init({ ipc: false }).

3. Renderer

declare global { interface Window { analisa: import('@racoelho/analisa-desktop/electron').AnalisaBridge } }

window.analisa.screen('Settings')                 // → page_view path "/settings"
window.analisa.track('export_clicked', { format: 'pdf' })
window.analisa.identify('user_123', { plan: 'pro' }, 'plan')

Alternativa: SDK web no renderer. Se preferir o @racoelho/analisa no renderer, declare a plataforma real com config.context (o contexto vem do main pelo bridge):

import { Analytics } from '@racoelho/analisa'
const context = await window.analisa.getContext() // { platform: 'macos', framework: 'electron', app: {...} }
const analytics = new Analytics({ siteKey: 'pk_...', context, autoWebVitals: false })

Nesse caso a página carregada por file:// manda Origin: null (tratado como app nativo: precisa de allow_native); servida por http(s):// é origem web (allowed origins). O SDK web guarda anon_id no localStorage do renderer — use um dos dois (SDK do main + bridge, ou SDK web no renderer), não os dois, ou o mesmo usuário vira duas pessoas. Ciclo de vida e crashes do processo principal só existem no SDK do main.

Tauri v2

// src/analytics.ts (front-end)
import { init } from '@racoelho/analisa-desktop/tauri'
import * as app from '@tauri-apps/api/app'
import * as win from '@tauri-apps/api/window'
import * as os from '@tauri-apps/plugin-os'        // opcional
import * as store from '@tauri-apps/plugin-store'  // opcional (senão: localStorage)
import * as http from '@tauri-apps/plugin-http'    // opcional (senão: fetch do webview)

export const analisa = init({
  siteKey: 'pk_...',
  modules: { app, window: win, os, store, http },
})
analisa.screen('Home')

Os módulos do Tauri são opcionais e injetados. Sem modules, o SDK tenta window.__TAURI__ (app.withGlobalTauri: true) e as APIs internas do Tauri v2 (__TAURI_INTERNALS__.invoke, __TAURI_OS_PLUGIN_INTERNALS__), e no fim o navigator. Ele não faz import() dos pacotes por conta própria: um import de pacote que o app não instalou quebra o build do Vite/Webpack.

  • Contexto: plataforma e versão do SO (plugin-os), getVersion(), getIdentifier() (app id), locale, fuso, screen + devicePixelRatio, framework: "tauri", sdk.name: "analisa-tauri".
  • Envio: transport: 'auto' (padrão) usa o fetch do plugin-http quando injetado, senão o fetch do webview com Content-Type: text/plain e sem headers customizados (sem preflight; o app id vai em context.app.id). O ingestor responde CORS ecoando o Origin, então o fetch do webview funciona; se a sua CSP bloquear, inclua a URL do ingestor em connect-src ou use o plugin-http (sem CORS/CSP). O plugin-http também manda Origin (tauri://localhost ou http://tauri.localhost, ambos tratados como nativos); compilado com a feature unsafe-headers, stripOrigin: true faz o plugin remover o header.
  • Persistência: plugin-store (analisa.json) quando injetado, senão localStorage.
  • Ciclo de vida: visibilitychange, focus/blur e onFocusChanged da janela (backgroundOn: 'visibility' para ignorar perda de foco). Inicie o SDK só na janela principal.
  • Erros: error/unhandledrejection do webview viram error_kind: "unhandled". Panics do Rust não passam pelo JS — não são capturados.

Rust (src-tauri/src/lib.rs), só para os plugins que você usar:

tauri::Builder::default()
    .plugin(tauri_plugin_os::init())
    .plugin(tauri_plugin_store::Builder::new().build())
    .plugin(tauri_plugin_http::init())

Permissões (src-tauri/capabilities/default.json):

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "core:app:allow-version",
    "core:app:allow-name",
    "core:app:allow-identifier",
    "core:window:default",
    "os:default",
    "store:default",
    {
      "identifier": "http:default",
      "allow": [{ "url": "https://function-bun-production-3339.up.railway.app/*" }]
    }
  ]
}

(Troque a URL se usar outro endpoint. os:, store: e http: só se usar esses plugins.)

API (Electron e Tauri)

| Método | O que faz | |---|---| | track(name, props?) | Evento customizado (snake_case). | | screen(name, props?) | page_view com path de rota ("ProductDetail" → /product-detail; um nome começando com / é usado como path) e props.screen_name. Ids vão em props, não no path. | | identify(userId, traits?, idProp?) | Associa o usuário (persistido até reset()); não reenvia o mesmo userId em < 5 min. | | reset() | Logout: limpa o usuário e abre sessão nova (mantém anon_id). | | captureException(error, props?) | error com error_kind: "handled". | | setConsent('granted' \| 'denied') / optOut() | denied para a coleta, limpa fila e usuário, e vale após reiniciar. | | flush() | Envia a fila agora. | | signup, login, logout, search, purchase | Mesmos nomes/props do SDK web. | | ready | Promise: estado carregado e app_open enfileirado. | | shutdown() | Remove listeners, grava a fila e tenta um último envio. |

Opções comuns: endpoint, debug, trackLifecycle (padrão true), captureErrors (true), defaultConsent ('granted'), flushIntervalMs (10 000), flushAt (20), maxBatchSize (50), maxQueueSize (1000), sessionTimeoutMs (30 min), requestTimeoutMs (10 000), context (sobrescreve campos detectados), fetch, logger.

Entrega

  • Cada evento recebe um id (UUID v4) na criação, salvo com ele e reusado em toda nova tentativa (idempotência no ingestor).
  • Sessão: nova após 30 min sem eventos (persistida; reiniciar o app dentro de 30 min continua a sessão). Voltar do segundo plano depois de 30 min emite app_open {launch:"warm"}.
  • Fila persistente de até 1000 eventos (descarta os mais antigos); eventos com mais de 7 dias são descartados no envio.
  • Flush com 20 eventos, a cada 10 s em primeiro plano, ao ir para segundo plano e no início. Até 50 eventos por request, um request por vez, sent_at em cada tentativa.
  • Rede/timeout/408/429/5xx: backoff min(2^n s, 5 min) ±20 % (respeita Retry-After). 413: divide o lote ao meio (um evento sozinho grande demais é descartado). Outros 4xx: descarta o lote. native_not_allowed, app_id_required, app_id_not_allowed, site_inactive, 401, 404: descarta e para de enviar neste processo (a coleta continua na fila e vai no próximo lançamento, depois de corrigir o painel). 402: descarta e pausa 1 h.

Privacidade

  • Nenhum dado pessoal por padrão: anon_id é um UUID aleatório do app (não vem de hardware nem de ids de publicidade; reinstalar gera outro). Sem IP, sem fingerprinting no cliente.
  • Stacks de erro têm caminhos absolutos removidos (que podem conter o nome do usuário do SO).
  • A site key num binário é pública: allow_native/allowed_app_ids são mitigação de spam, não autenticação. Nunca ponha a secret key no app.
  • setConsent('denied') apaga fila e usuário e para tudo, inclusive após reiniciar.

Testes

npm test -w @racoelho/analisa-desktop          # unitários (vitest, electron/tauri falsos)
# E2E contra um ingestor real + Postgres:
ANALISA_E2E_ENDPOINT=http://localhost:8787/v1/i ANALISA_E2E_SITE_KEY=pk_... \
ANALISA_E2E_SITE_KEY_OFF=pk_sem_allow_native DATABASE_URL=postgresql://... \
  npm run test:e2e -w @racoelho/analisa-desktop
# Smoke com Electron real (headless com xvfb), depois de `npm run build`:
bash packages/sdk-desktop/e2e/real-electron/run.sh

English

Analisa SDK for Electron and Tauri v2 desktop apps (Windows, macOS, Linux), implementing the ingestion protocol v1: screens, sessions, app lifecycle, crashes and a persistent offline queue.

Dashboard setup

  1. Turn on "Receive events from apps" (allow_native) in the app settings. Otherwise the ingestor answers 403 native_not_allowed and the SDK stops sending (with a console message).
  2. Optional: list your app id in "Allowed app ids" (appId on Electron, the tauri.conf.json identifier on Tauri).
  3. Nothing else for Tauri: its webview origins (tauri://localhost on macOS/Linux, http(s)://tauri.localhost on Windows WebView2) are treated as native requests, so rules 1 and 2 apply. No need to touch Allowed origins.

Electron

  • Main process: init({ siteKey, appId, appBuild? }) from @racoelho/analisa-desktop/electron. Sends from the main process with Node fetch (no Origin) plus User-Agent and X-Analisa-App-Id; context from process.platform, process.getSystemVersion()/os.release(), app.getVersion(), app.getLocale(), screen.getPrimaryDisplay() (after ready), device.type: desktop, framework: electron. State, queue and pending crashes live in userData/analisa/*.json.
  • Lifecycle: app_open (cold, first_launch) on ready, app_update when version/build changed, app_background/app_foreground when no app window is focused (or, with backgroundOn: 'visibility', when all windows are hidden/minimized) and on powerMonitor suspend/resume. before-quit waits up to 2 s to flush.
  • Crashes: main-process uncaught exceptions are written synchronously to disk (uncaughtExceptionMonitor, no network) and sent on the next launch with the crash's own ts, session and app version, before app_open. If the process survives, it is sent right away as unhandled. Unhandled rejections → unhandled. render-process-gone / child-process-gone → crash.
  • Preload: exposeAnalisa() from @racoelho/analisa-desktop/electron/preload exposes window.analisa through contextBridge (IPC to the main process, arguments validated there). With the sandbox on, bundle your preload. window.analisa.getContext() returns the main-process context if you prefer the web SDK in the renderer: new Analytics({ siteKey, context }) — then use that instead of the main-process SDK, not both (two anon_ids).

Tauri v2

init({ siteKey, modules: { app, window, os, store, http } }) from @racoelho/analisa-desktop/tauri, in the main window only. Tauri modules are optional and injected; without them the SDK falls back to window.__TAURI__, Tauri v2 internals and navigator. It never import()s them itself (a missing optional package breaks Vite/Webpack builds). Transport: the http plugin when provided (no CORS), otherwise webview fetch with text/plain and no custom headers (no preflight; the ingestor echoes the Origin in CORS). Storage: plugin-store, otherwise localStorage. See the capabilities JSON and Rust plugin setup above. Rust panics are not captured.

API

track, screen (→ page_view with a route-like path and props.screen_name), identify, reset, captureException, setConsent/optOut, flush, helpers signup/login/logout/ search/purchase, plus ready and shutdown(). Delivery follows the protocol exactly: UUID per event reused on retry, 30-minute sessions, persistent queue capped at 1000, flush at 20 events / every 10 s / on background, ≤ 50 events per request, one request in flight, backoff min(2^n s, 5 min) ±20 %, split on 413, drop on other 4xx, stop on native_not_allowed/app_id_*/site_inactive/401/404, pause 1 h on 402.

Privacy

No personal data by default; random install-scoped anon_id; absolute paths stripped from stack traces; the site key in a binary is public (spam mitigation, not auth) — never ship the secret key; setConsent('denied') clears the queue and user and persists.