closeyourit 0.8.0 copy "closeyourit: ^0.8.0" to clipboard
closeyourit: ^0.8.0 copied to clipboard

Client Dart/Flutter per CloseYourIt — errori, metriche e logs verso il backend di osservabilità interno.

closeyourit #

Client Dart/Flutter per CloseYourIt — il backend di osservabilità interno: cattura errori, metriche e logs delle app Flutter e li invia a CloseYourIt, fire-and-forget (non rallenta né crasha l'app).

Versione 0.8.0, pubblicata su pub.flutter-io.cn. Pre-1.0 (semver 0.x): possibili breaking change minori tra le minor.

Rispetto agli altri SDK. Questo client non replica funzione per funzione il gem closeyourit-ruby né il client JS: alcune cose vivono solo qui (la persistenza offline degli errori su disco), altre solo altrove (slow_query e job_metric su Ruby, pageview e session replay sul browser). Cosa c'è in quale SDK — con versione minima e file sorgente per ogni cella — sta nella matrice compatibility/sdk-feature-parity.md del repository interno closeyourit-docs — privato, quindi il percorso resta testuale e non è un file di questo package. Questo README documenta solo ciò che il client Dart fa davvero.

Cosa fa #

  • Errori — cattura automatica delle eccezioni non gestite (FlutterError.onError, PlatformDispatcher.onError, zone) + captureException/captureMessage manuali.
  • Scope & breadcrumbs — user, tag, context, trace_id e cronologia eventi allegati a ogni evento.
  • Metrichemeasure() per il timing dei blocchi (slow_method).
  • Performance issues (opt-in) — verdetti jank, rebuild_storm, slow_external_http e repeated_http con detectPerformanceIssues: true (default off).
  • LogsCloseYourIt.log / CloseYourIt.logger con batching e correlazione trace_id.
  • Telemetria d'uso (opt-in)CloseYourIt.used / usedRoute: quali parti dell'app girano davvero, in un POST per finestra (usageEnabled: true, solo Bearer).
  • Integrazioni — interceptor Dio, observer go_router/Navigator, scrubbing PII di default.

Installazione #

flutter pub add closeyourit

Oppure in pubspec.yaml:

dependencies:
  closeyourit: ^0.8.0

Quick start #

import 'package:closeyourit/closeyourit.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await CloseYourIt.init(
    CloseYourItOptions(
      endpointUrl: const String.fromEnvironment('CLOSEYOURIT_ENDPOINT_URL'),
      publicKey: const String.fromEnvironment('CLOSEYOURIT_PUBLIC_KEY'),
      projectId: const String.fromEnvironment('CLOSEYOURIT_PROJECT_ID'),
      environment: const String.fromEnvironment('CLOSEYOURIT_ENVIRONMENT', defaultValue: 'development'),
      release: const String.fromEnvironment('CLOSEYOURIT_RELEASE'),
    ),
    appRunner: () => runApp(const MyApp()),
  );
}

appRunner avvia l'app dentro una zona protetta e installa i gestori globali degli errori: ogni crash non gestito viene catturato in automatico. I valori CLOSEYOURIT_* arrivano via --dart-define dal vault CloseYourIt dichiarato da ogni app consumer — mai hardcodati.

Credenziale. Un'app distribuita autentica con la DSN public key (publicKey): non è segreta, è di solo ingest e, se estratta dal pacchetto, non può leggere né amministrare il progetto. Il token Bearer (cyi_…) è server-only — porta anche la lettura della telemetria, quindi non va mai incluso in un'app client. Con la sola publicKey vengono inviati gli errori (drop-in Sentry POST /api/{projectId}/store con ?sentry_key=); metriche e logs hanno endpoint solo Bearer e restano ai contesti server-side.

Errori e messaggi #

CloseYourIt.captureException(error, stackTrace: st, handled: true);
CloseYourIt.captureMessage('cache miss', level: 'warning');

Scope e breadcrumbs #

CloseYourIt.setUser(id: account.id);              // senza sendPii viene inviato solo l'id
CloseYourIt.setTag('tenant', 'acme');
CloseYourIt.setContext('billing', {'plan': 'pro'});
CloseYourIt.setTraceId(requestId);                // correla log ↔ errori ↔ metriche
CloseYourIt.addBreadcrumb(message: 'checkout aperto', category: 'ui');
CloseYourIt.configureScope((s) => s.setTag('area', 'checkout'));

Logs #

CloseYourIt.log(LogLevel.info, 'ordine creato', attributes: {'order_id': 1});
CloseYourIt.logger.warn('retry', attributes: {'attempt': 3}); // warn → warning

I log sono bufferizzati e inviati in batch (default 50 voci o ogni 5s, e su pausa app). Gli attributes con chiavi sensibili (password, token, authorization, card…) sono redatti.

Metriche #

final result = await CloseYourIt.measure('checkout.total', () async {
  return await expensiveWork();
});

Se la durata supera slowMethodThresholdMs (default 200ms) viene inviata una metrica slow_method.

Telemetria d'uso (opt-in) #

Risponde a «questa parte dell'app viene ancora usata?»: registra quali simboli girano davvero e li invia in un unico POST per finestra (default ogni 5 minuti). Alimenta la pagina d'uso del progetto e cyi. Opt-in: usageEnabled: true.

CloseYourIt.used('checkout.coupon');       // kind custom: una chiave letterale
CloseYourIt.usedRoute('CheckoutScreen');   // kind route: una schermata
  • La chiave va scritta letterale nel codice, mai interpolata con dati (un id, un nome utente, un path): il formato ammesso è [A-Za-z0-9_:#./-]{1,200}, e un simbolo fuori formato viene scartato in silenzio. Per le schermate passa il nome logico (CheckoutScreen), mai il path percorso — un /ordini/42 porterebbe un dato utente nella symbol table.
  • I conteggi sono indicativi: il dato portante è l'ultima volta che il simbolo è stato visto. Nessun sampling — campionare una schermata aperta tre volte al mese fabbrica esattamente il falso «mai vista» che questo canale esiste per evitare.
  • Il registro si svuota a ogni finestra: usageMaxSymbols (default 2000) limita i simboli distinti di una finestra, non la dimensione del codice. Oltre il tetto la finestra viaggia con truncated: true e lato server squalifica il kind.
  • Solo Bearer: come metriche e logs, il canale usages non ha endpoint pubblico. In un'app distribuita con la sola publicKey la telemetria d'uso resta muta, anche con usageEnabled: true.

Interceptor Dio #

final dio = Dio()..interceptors.add(CloseYourItDioInterceptor());

Le richieste diventano breadcrumb; gli errori 5xx vengono catturati come eccezioni. Gli header sensibili (Authorization, Cookie…) non vengono mai inviati.

Propagazione W3C Trace Context (opt-in) #

L'interceptor può iniettare il W3C Trace Context (traceparent/tracestate) nelle richieste uscenti, così una chiamata mobile e i log del backend risultano correlabili end-to-end. È opt-in e limitata da allowlist:

CloseYourItOptions(
  // …
  propagateTraceContext: true,
  tracePropagationAllowlist: ['api.miodominio.it'], // esatto o per sottodominio
);
  • Gli header partono solo verso gli host elencati: le destinazioni non autorizzate non ricevono nulla (allowlist vuota → nessuna propagazione, anche col flag attivo).
  • Il traceparent riusa il trace_id di correlazione dello scope e genera un parent-id nuovo a ogni richiesta (chiamate concorrenti non condividono lo span). Una richiesta con un traceparent valido a monte ne continua il trace; uno malformato viene scartato.
  • tracestate e baggage sono filtrati con la stessa denylist PII dello scrubbing: nessuna informazione interna verso terze parti.

Performance issues (opt-in) #

La detection è spenta di default. Con detectPerformanceIssues: true il client emette verdetti performance_issue su /metrics. Senza il master switch nessun verdetto viene emesso, il CloseYourItRebuildProbe è un no-op e l'interceptor Dio non cronometra le chiamate. Quattro subtype, con attivazioni diverse:

Verdetto Quando scatta Cosa serve oltre a detectPerformanceIssues: true
jank uno o più frame superano jankFrameBudgetMs (default 32 ms ≈ <30 fps) niente: l'observer di frame timing è installato in automatico da CloseYourIt.init (detectJank default true). I frame oltre budget di una stessa route sono coalescati in una finestra jankWindowMs (default 1 s): un solo verdetto aggregato per finestra (query_count = frame, duration_ms = frame peggiore), attribuito alla route corrente.
rebuild_storm uno stesso widget si ricostruisce rebuildStormThreshold volte (default 20) entro rebuildStormWindowMs (default 1 s) avvolgere i widget sospetti con CloseYourItRebuildProbe — Flutter non espone un conteggio rebuild globale.
slow_external_http una chiamata HTTP esterna supera slowExternalThresholdMs (default 1 s) CloseYourItDioInterceptor sul tuo Dio (captureExternalHttp default true).
repeated_http la stessa richiesta (host + path templatizzato) ripetuta repeatedHttpThreshold volte (default 5) entro repeatedHttpWindowMs (default 5 s) — l'"N+1 del mobile" come sopra: interceptor Dio.

Master switch #

await CloseYourIt.init(
  CloseYourItOptions(
    endpointUrl: const String.fromEnvironment('CLOSEYOURIT_ENDPOINT_URL'),
    token: const String.fromEnvironment('CLOSEYOURIT_TOKEN'),
    projectId: const String.fromEnvironment('CLOSEYOURIT_PROJECT_ID'),
    detectPerformanceIssues: true, // default false: senza, nessun verdetto viene emesso
  ),
);

Attivato l'opt-in, il jank funziona subito: init installa l'observer di frame timing (nessun codice aggiuntivo). Per non trasformare uno scroll janky in decine di POST identici — che saturerebbero la coda condivisa e affamerebbero gli errori concorrenti — i frame oltre budget di una stessa route sono coalescati in una finestra jankWindowMs (default 1 s): a fine finestra parte un unico verdetto aggregato con il numero di frame (query_count) e il frame peggiore (duration_ms).

L'attribuzione per-schermo del jank (e del rebuild_storm) dipende dal CloseYourItNavigatorObserver: senza l'observer registrato — o durante l'avvio, o su route anonime — la rotta corrente è ignota e i verdetti confluiscono nel gruppo dedicato <unknown> (parità col client JS), invece di omettere la rotta e collassare tutti gli schermi in un'unica signature parziale. Registra l'observer per attribuire il jank alla schermata giusta.

rebuild_storm — CloseYourItRebuildProbe #

Flutter non espone un conteggio globale dei rebuild, quindi la sonda è per-widget: avvolgi i sottoalberi che sospetti si ricostruiscano troppo. È trasparente (rende child invariato) e resta un no-op finché la detection è OFF. Alla N-esima ricostruzione entro la finestra emette il verdetto.

CloseYourItRebuildProbe(
  label: 'ProductCard', // identifica il widget nel verdetto (grouping lato backend)
  child: ProductCard(product: product),
)

slow_external_http e repeated_http — interceptor Dio #

I due verdetti HTTP passano dall'interceptor Dio: aggiungilo al tuo Dio e, con detectPerformanceIssues attivo, cronometra le chiamate (slow_external_http) e conta quelle identiche nella finestra (repeated_http). Il path è templatizzato (/users/<uuid>, /orders/<n>), mai la query string; le chiamate verso l'endpoint CloseYourIt sono escluse per non generare loop.

final dio = Dio()..interceptors.add(CloseYourItDioInterceptor());
MaterialApp(
  navigatorObservers: [CloseYourItNavigatorObserver()],
  // ...
);

Oltre a navigazione e HTTP, tre sorgenti aggiungono breadcrumb sintetici che contestualizzano un errore avvenuto poco dopo. Sono opt-in (default OFF), minimali e privi di PII: mai il testo digitato né i dati del widget toccato, solo il tipo di segnale e la rotta. Rispettano breadcrumbsEnabled e finiscono nel ring buffer allegato agli eventi.

Sorgente Opzione Cosa registra Come si attiva
Ciclo di vita captureLifecycleBreadcrumbs data.state = resumed/paused/hidden/detached niente: l'observer di lifecycle è installato da init. Il flush dei log in background resta attivo comunque.
Connettività captureConnectivityBreadcrumbs data.connectivity = wifi/cellular/ethernet/none… — solo il tipo di rete, mai SSID o IP niente: init installa l'observer via connectivity_plus. Piattaforma senza plugin o permessi negati → nessun breadcrumb, mai un crash.
Tap captureTapBreadcrumbs il fatto dell'interazione + data.route (rotta corrente) avvolgi l'app con CloseYourItUserInteraction — non ispeziona l'albero, quindi non può leggere testo o widget.

I cambi di rete troppo rapidi e i tap a raffica sono soppressi da breadcrumbThrottleMs (default 500 ms), così una rete instabile o uno scroll non saturano il ring buffer.

await CloseYourIt.init(
  CloseYourItOptions(
    endpointUrl: const String.fromEnvironment('CLOSEYOURIT_ENDPOINT_URL'),
    publicKey: const String.fromEnvironment('CLOSEYOURIT_PUBLIC_KEY'),
    projectId: const String.fromEnvironment('CLOSEYOURIT_PROJECT_ID'),
    captureLifecycleBreadcrumbs: true,
    captureConnectivityBreadcrumbs: true,
    captureTapBreadcrumbs: true, // richiede CloseYourItUserInteraction attorno all'app
  ),
);

MaterialApp(
  navigatorObservers: [CloseYourItNavigatorObserver()],
  builder: (context, child) =>
      CloseYourItUserInteraction(child: child ?? const SizedBox()),
);

Privacy #

sendPii è false di default: niente email/IP/username. Tutti i payload passano per uno scrubber PII, e puoi aggiungere un hook beforeSend per modificare o scartare (return null) ogni evento. Anche gli eventi persistiti su disco (vedi sotto) sono scrubati prima della scrittura: nessun segreto in chiaro sul filesystem.

Persistenza crash offline #

Il transport è fire-and-forget: senza una coda persistente, il crash che chiude l'app — o un periodo offline — perde l'evento in volo, proprio quello che più interessa. Con persistOffline (default true) ogni errore (captureException/captureMessage) viene scritto sincronamente su disco prima dell'invio e rimosso alla conferma 2xx o su un rifiuto permanente (un 4xx non ritentabile — payload invalido, non autorizzato, troppo grande… — non diventa valido riprovando: viene scartato invece di intasare la coda per sempre). Al successivo avvio, init chiama replayPending che rispedisce gli eventi rimasti; restano ritentabili solo gli esiti transitori — errore di rete, 408, 425, 429 e 5xx (pattern Crashlytics).

  • Store su file (path_provider): un file JSON per evento, payload sempre scrubato.
  • Backstop isolate: init aggancia Isolate.current.addErrorListener per gli errori non gestiti dell'isolate corrente (root) che sfuggono a FlutterError/PlatformDispatcher/zona, catturati come IsolateError fatal e persistiti come gli altri errori. Per i crash di isolate spawnati (Isolate.spawn) passa CloseYourIt.isolateErrorPort come onError allo spawn — un listener sull'isolate corrente da solo non li vede. Gli errori di compute risalgono già via la sua Future (coperta dalla zona di appRunner).
  • Su web (nessun filesystem/isolate) persistenza e backstop isolate sono assenti; il client resta pienamente funzionante.
  • Diagnostica: i contatori persisted, replayed e discarded (rifiuti permanenti scartati dalla coda) in CloseYourIt.stats.
// Instradare a CloseYourIt i crash di un isolate spawnato:
final port = CloseYourIt.isolateErrorPort;
if (port is SendPort) {
  await Isolate.spawn(worker, message, onError: port);
}

Nei test puoi iniettare una coda custom: CloseYourIt.init(options, offlineStore: myStore) (implementa OfflineStore).

Configurazione #

CloseYourItOptions accetta:

Opzione Tipo Default Descrizione
endpointUrl String — (obbligatorio) Base URL del backend, es. https://www.closeyour.it (senza path).
publicKey String '' DSN public key (hex, non segreta): credenziale delle app distribuite. Instrada gli errori sul drop-in POST /api/{projectId}/store con ?sentry_key=, senza Bearer. Serve publicKey oppure token; se impostata prevale sul token.
token String '' Bearer segreto del progetto (cyi_…), inviato come Authorization: Bearer. SERVER-ONLY (ingest + lettura telemetria): mai in un'app client. Opzionale se c'è la publicKey.
projectId String — (obbligatorio) UUID del progetto su CloseYourIt.
environment String 'development' Nome ambiente (development/staging/production).
release String? Versione/release applicativa (auto-detect version+build se assente).
sampleRate double 1.0 Probabilità di invio di errori/metriche (1.0 = tutti, 0.0 = nessuno).
sendPii bool false Master switch PII: con false niente email/IP/username/query.
breadcrumbsEnabled bool true Cattura la cronologia breadcrumb allegata agli eventi.
maxBreadcrumbs int 100 Capienza del ring buffer dei breadcrumb.
captureLifecycleBreadcrumbs bool false Opt-in: breadcrumb a ogni cambio di stato del ciclo di vita (data.state). Il flush in background resta attivo comunque.
captureConnectivityBreadcrumbs bool false Opt-in: breadcrumb a ogni cambio di connettività (data.connectivity = solo il tipo di rete), via connectivity_plus.
captureTapBreadcrumbs bool false Opt-in: breadcrumb di interazione (tipo + rotta) con CloseYourItUserInteraction attorno all'app. Mai testo o dati del widget.
breadcrumbThrottleMs int 500 Intervallo minimo (ms) tra breadcrumb automatici ad alta frequenza (connettività, tap): sopprime toggle di rete rapidi e tap a raffica. 0 disattiva.
captureRequest bool true Allega il contesto request (method/url/headers) agli eventi.
collectDeviceContext bool true Raccoglie all'init i contesti device/OS/app.
requestHeaderAllowlist List<String> [Accept, Content-Type, User-Agent, Referer] Header HTTP ammessi nel context request (mai Authorization/Cookie).
propagateTraceContext bool false Opt-in: propaga il W3C Trace Context (traceparent/tracestate) alle richieste Dio uscenti verso tracePropagationAllowlist. Vedi «Propagazione W3C».
tracePropagationAllowlist List<String> [] Host verso cui propagare il trace context (match esatto o per sottodominio, es. x.it copre api.x.it). Vuota → nessuna propagazione.
slowMethodThresholdMs int 200 Soglia (ms) oltre cui un measure produce una metrica slow_method.
captureMethodArguments bool false Opt-in: includi gli argomenti del blocco misurato (scrubbed).
detectPerformanceIssues bool false Master switch dei verdetti performance (jank/rebuild_storm/slow_external_http/repeated_http).
captureExternalHttp bool true Strumenta le HTTP esterne (interceptor Dio). Effettivo solo col detect attivo.
slowExternalThresholdMs int 1000 Soglia (ms) oltre cui una chiamata HTTP esterna diventa slow_external_http.
repeatedHttpThreshold int 5 N chiamate identiche nella finestra oltre cui scatta repeated_http.
repeatedHttpWindowMs int 5000 Ampiezza (ms) della finestra scorrevole per repeatedHttpThreshold.
detectJank bool true Rileva i frame lenti (jank). Effettivo solo col detect attivo.
jankFrameBudgetMs int 32 Durata (ms) oltre cui un frame è considerato jank (≈ <30 fps).
jankWindowMs int 1000 Ampiezza (ms) della finestra di coalescing del jank: i frame oltre budget di una stessa route confluiscono in un unico verdetto aggregato (query_count = frame, duration_ms = worst) per finestra.
rebuildStormThreshold int 20 N rebuild dello stesso widget (via CloseYourItRebuildProbe) oltre cui scatta rebuild_storm.
rebuildStormWindowMs int 1000 Ampiezza (ms) della finestra scorrevole per rebuildStormThreshold.
logsEnabled bool true Abilita il dominio logs (CloseYourIt.log/logger).
logsSampleRate double 1.0 Sampling dei log, indipendente da sampleRate.
logsBatchSize int 50 Log accumulati che forzano il flush del batch.
logsFlushInterval Duration 5 s Intervallo del flush periodico del buffer logs.
logsMinLevel LogLevel LogLevel.info Livello minimo di log inoltrato (sotto soglia: scartato).
usageEnabled bool false Opt-in: abilita la telemetria d'uso (CloseYourIt.used/usedRoute). Canale solo Bearer: con la publicKey resta muta.
usageFlushInterval Duration 5 min Ampiezza della finestra d'uso: un solo POST per finestra. Duration.zero disattiva il flush periodico.
usageMaxSymbols int 2000 Tetto dei simboli distinti per finestra (mai oltre i 5000 accettati dal backend); oltre, la finestra dichiara truncated.
excludedExceptions List<String> [] Nomi di tipo eccezione da non inviare (match sul runtimeType).
maxQueue int 30 Invii HTTP in volo prima del drop (fire-and-forget, mai backpressure).
persistOffline bool true Persiste su disco gli errori prima dell'invio: sopravvivono a crash/offline e sono rispediti al riavvio. Su web è ininfluente.
maxPersistedEvents int 50 Tetto di eventi persistiti su disco; oltre, sfratta i più vecchi.
beforeSend BeforeSend? Hook di scrub/scarto (return null) applicato a ogni payload.
onDiagnostic DiagnosticCallback? Callback in-process invocata a ogni segnale diagnostico (invio/persistenza/replay o uno scarto col motivo). Vedi Diagnostica.
enabled bool true Master switch: con false il client è interamente no-op.
debug bool false Logga via debugPrint i fallimenti d'invio del transport (status + codice R…).

Se manca uno dei tre campi obbligatori (o enabled: false), il client resta no-op senza sollevare: la telemetria non deve mai impedire l'avvio dell'app.

Wire contract #

  • DSN public key (app distribuite): errori/messaggi su POST {endpointUrl}/api/{projectId}/store con ?sentry_key=<publicKey> (drop-in Sentry, fuori da /api/v1), risposta 200 {id:…}. È l'unico canale della public key: metriche e logs non hanno endpoint pubblico.
  • Bearer (server-side): POST {endpointUrl}/api/v1/projects/{projectId}/{events,metrics,logs,usages} con header Authorization: Bearer cyi_…, risposta 202 {data:…}.

Vedi BUILD.md per i dettagli.

Diagnostica #

Capire se un evento è stato inviato, persistito, ritentato o scartato — e perché — senza leggere i file interni e senza generare nuova telemetria. Tutto è in-process e porta solo enum e interi: nessun payload, nessun dato sensibile.

CloseYourIt.stats; // conteggi di flusso piatti: { enqueued, dropped, sent, failed, persisted, replayed, discarded }

// Snapshot immutabile: conteggi + motivi strutturati di scarto (una fotografia).
final snap = CloseYourIt.diagnostics;
snap[StatKey.sent];                 // invii riusciti
snap.reason(DropReason.sampled);    // eventi soppressi dal campionamento
snap.toMap();                       // { …conteggi…, reasons: { sampled: 3, queueFull: 1 } }

await CloseYourIt.flush(); // forza l'invio (es. prima di un test)
await CloseYourIt.close(); // shutdown

I motivi strutturati (DropReason) coprono transport, worker e offline store, rendendo diagnosticabile ogni punto altrimenti muto: sampled, beforeSend, excluded, duplicate, queueFull, timeout, network, clientClosed, permanentReject, tooLarge (413, oltre il budget payload lato server), maxAttempts, expired (TTL), evicted (capienza), storeCorrupted. Alcuni aggregano un contatore di flusso storico (queueFull → dropped, timeout/network → failed, permanentReject/tooLarge/maxAttempts → discarded); gli altri sono visibili solo tra i motivi. Un rifiuto permanente (incluso il 413) è registrato per ogni invio — errori, metriche e log — non solo per gli eventi in coda offline.

Per reagire in tempo reale, onDiagnostic (in CloseYourItOptions) riceve un DiagnosticEvent a ogni segnale — un incremento di flusso (event.stat) o uno scarto col suo motivo (event.reason). È puramente in-process (nessun evento ricorsivo) e un'eccezione al suo interno viene assorbita:

CloseYourItOptions(
  endpointUrl: 'https://www.closeyour.it',
  publicKey: '...',
  projectId: '...',
  onDiagnostic: (event) {
    if (event.isDrop) debugPrint('closeyourit ha scartato: ${event.reason}');
  },
);

Licenza #

MIT — vedi LICENSE.

0
likes
150
points
305
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Client Dart/Flutter per CloseYourIt — errori, metriche e logs verso il backend di osservabilità interno.

Repository (GitHub)

Topics

#observability #error-tracking #logging #monitoring #crash-reporting

License

MIT (license)

Dependencies

connectivity_plus, device_info_plus, dio, flutter, http, meta, package_info_plus, path_provider, stack_trace

More

Packages that depend on closeyourit