closeyourit 0.8.0
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-rubyné il client JS: alcune cose vivono solo qui (la persistenza offline degli errori su disco), altre solo altrove (slow_queryejob_metricsu Ruby,pageviewe session replay sul browser). Cosa c'è in quale SDK — con versione minima e file sorgente per ogni cella — sta nella matricecompatibility/sdk-feature-parity.mddel repository internocloseyourit-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/captureMessagemanuali. - Scope & breadcrumbs — user, tag, context,
trace_ide cronologia eventi allegati a ogni evento. - Metriche —
measure()per il timing dei blocchi (slow_method). - Performance issues (opt-in) — verdetti
jank,rebuild_storm,slow_external_httperepeated_httpcondetectPerformanceIssues: true(default off). - Logs —
CloseYourIt.log/CloseYourIt.loggercon batching e correlazionetrace_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. IltokenBearer (cyi_…) è server-only — porta anche la lettura della telemetria, quindi non va mai incluso in un'app client. Con la solapublicKeyvengono inviati gli errori (drop-in SentryPOST /api/{projectId}/storecon?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/42porterebbe 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 contruncated: truee lato server squalifica il kind. - Solo Bearer: come metriche e logs, il canale
usagesnon ha endpoint pubblico. In un'app distribuita con la solapublicKeyla telemetria d'uso resta muta, anche conusageEnabled: 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
traceparentriusa iltrace_iddi correlazione dello scope e genera unparent-idnuovo a ogni richiesta (chiamate concorrenti non condividono lo span). Una richiesta con untraceparentvalido a monte ne continua il trace; uno malformato viene scartato. tracestateebaggagesono 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());
Navigazione #
MaterialApp(
navigatorObservers: [CloseYourItNavigatorObserver()],
// ...
);
Breadcrumb automatici (opt-in) #
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:
initagganciaIsolate.current.addErrorListenerper gli errori non gestiti dell'isolate corrente (root) che sfuggono aFlutterError/PlatformDispatcher/zona, catturati comeIsolateErrorfatale persistiti come gli altri errori. Per i crash di isolate spawnati (Isolate.spawn) passaCloseYourIt.isolateErrorPortcomeonErrorallo spawn — un listener sull'isolate corrente da solo non li vede. Gli errori dicomputerisalgono già via la suaFuture(coperta dalla zona diappRunner). - Su web (nessun filesystem/isolate) persistenza e backstop isolate sono assenti; il client resta pienamente funzionante.
- Diagnostica: i contatori
persisted,replayedediscarded(rifiuti permanenti scartati dalla coda) inCloseYourIt.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}/storecon?sentry_key=<publicKey>(drop-in Sentry, fuori da/api/v1), risposta200 {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 headerAuthorization: Bearer cyi_…, risposta202 {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.