ADR 0016 — Télémétrie frontend Grafana Faro (dev/staging), exclue du bundle prod
Retiré par ADR 0020 : Faro est supprimé du repo, les funnels d'erreur remontent vers PostHog (
captureError). Conservé comme archive.
Amendé par ADR 0017 : le point 5 (« pas de
@grafana/faro-web-tracing») est révisé — spans HTTP manuels vers Tempo, toujours sans propagationtraceparent— et le point 4 passe en errors-only : instrumentations réduites (plus de web vitals / vues / navigation, console.error seul) ethttp_requestémis uniquement sur échec.
Contexte
On veut de l'observabilité frontend « boilerplate » (erreurs, métriques web, sessions, activité de navigation) sur les builds dev et staging, envoyée vers une instance Grafana Alloy self-hosted (composant faro.receiver → Loki). La production n'embarque aucune télémétrie pour l'instant : un tracking opt-in production viendra plus tard — d'ici là, la librairie ne doit même pas exister dans le bundle prod.
Contraintes du repo :
- CapacitorHttp est activé (
capacitor.config.ts,CapacitorHttp: { enabled: true }) : sur device,window.fetch/XHR sont patchés vers la couche HTTP native. L'auto-instrumentation OpenTelemetry fetch/XHR (@grafana/faro-web-tracing) serait donc aveugle aux appels API sur device — et pèserait ~26-90 KB gzip (OTel + zone.js) pour rien. - La suite e2e (ADR
0015) échoue fort sur toute requête sortante non mockée (assertNoUnmatched()) : le modee2ene doit jamais initialiser Faro. - Le repo a des cycles d'imports réels (cf.
setBackButtonRouterdansmain.ts) : un module de télémétrie importé par les services, les stores ET le router doit être une feuille. - Vue 3 n'a pas de package Faro officiel (React uniquement ; feature request ouverte faro-web-sdk#1521) — l'intégration est manuelle.
Décision
@grafana/faro-web-sdk (2.8.x) + Alloy faro.receiver, derrière une façade feuille src/services/telemetry.ts, dead-code-éliminée hors dev/staging.
- Exclusion du bundle par DCE, précédent Eruda (
BuchardApp.vue) : le seul import runtime de@grafana/faro-web-sdkdu repo est unimport()dynamique dans une branche gardée par des comparaisons littéralesimport.meta.env.MODE === 'development' || === 'staging'. esbuild replie la condition enfalsesur les autres modes et Rollup supprime la branche et le chunk Faro (production, e2e, debug). ⚠️ Le garde doit rester des===littéraux : un.includes()ou un test de truthiness sur uneVITE_*ne serait pas tree-shaké (vitejs/vite#15256). - Garde d'URL dans la branche : l'init exige
VITE_FARO_COLLECTOR_URL(committée dans.env.developmentet.env.staging, jamais dans.envque la prod charge). LesVITE_*étant inlinées au build, une URL absente/commentée fait tomber tout le code Faro du bundle même en dev/staging (vérifié) ; un dev peut opter out localement via.env.development.local. - Façade feuille :
telemetry.tsn'importe rien desrc/(seul unimport type, effacé à la compilation). Wrappers no-op tant que Faro n'est pas initialisé, chacun en try/catch — la télémétrie ne casse jamais l'app. Personne d'autre n'importe@grafana/*. - Signaux émis (instrumentations par défaut
getWebInstrumentations(): erreurs window/unhandledrejection, web vitals, session persistante localStorage, vues, console error/warn, performance) plus le câblage manuel : (amendé par ADR0017— errors-only : instrumentations explicitesErrors/Console(error seul)/Session,http_requestsur échec uniquement)app.config.errorHandler+router.onError(main.ts) →trackError— indispensable : les erreurs attrapées par Vue n'atteignent jamaiswindow.onerror;handleError/logError(services/error-handler.ts) →trackError(population disjointe du handler Vue : ce sont des erreurs déjà catchées) ;router.afterEach→setView(route.name)uniquement — pas d'eventroute_changeen plus (double comptage, faro-web-sdk#349) ;- les deux funnels HTTP (
publicFetch,ApiClient.request) → eventhttp_request{ method, path, status, duration_ms }. Query string strippée (origin + pathname) : la query porte du PII (customer.existsy met l'email) et des cache-busters?_=. La durée côtéApiClientenglobe refresh de token + retry 401 (latence perçue).customer.exists(appelCapacitorHttp.getdirect) reste volontairement non tracké. setUser({ id })aufetchCurrentCustomer(+ restauration au boot depuis le store persisté),resetUser()au logout — id client seul, pas d'email/nom dans Loki.
- Pas de
@grafana/faro-web-tracingpour l'instant (voir Contexte). Si le tracing distribué devient nécessaire : injection manuelle detraceparentdans les deux funnels + Horizon instrumenté OTel côté backend — sans quoi les spans ne se lient à rien. (Amendé par ADR0017: faro-web-tracing installé, spans HTTP manuels client-only, toujours sanstraceparent.)
Config Alloy de référence
faro.receiver "buchard_mobile" {
server {
listen_address = "0.0.0.0"
listen_port = 12347 // endpoint: http://<host>:12347/collect
cors_allowed_origins = ["http://localhost:5173", "http://<lan-ip>:5173"]
// CORS requis uniquement pour les sessions navigateur (npm run dev) : sur
// device, CapacitorHttp route le POST par la couche native, hors CORS.
// api_key = sys.env("FARO_API_KEY") // optionnel → VITE_FARO_API_KEY
}
output {
logs = [loki.write.default.receiver]
traces = [otelcol.exporter.otlp.tempo.input] // depuis ADR 0017
}
}Conséquences
- Garantie vérifiable :
npx vite buildpuisgrep -rl "@grafana\|initializeFaro" dist/assets→ vide en prod/e2e. En staging/dev avec URL configurée, le SDK est un chunk lazy séparé (~96 KB min / ~32 KB gzip) chargé uniquement si le garde passe — l'entry n'embarque rien. - La suite e2e reste un tripwire permanent : si Faro fuyait dans le build e2e, la première requête
/collectnon mockée ferait échouer le test (teardownassertNoUnmatched). - Activation = renseigner l'URL : tant que
VITE_FARO_COLLECTOR_URLest commentée dans.env.development/.env.staging, la télémétrie est morte ET absente du bundle. La renseigner suffit (aucun changement de code). - Les mesures atterrissent dans Loki, pas Prometheus : le
faro.receiverémet logs, events, exceptions ET web-vitals comme lignes de log — dashboards en LogQL (ou recording rules). - Limites assumées : transport fire-and-forget (pas de queue offline — perte du dernier batch si l'app est tuée), web vitals partiels sur WKWebView iOS, session persistante perdue si l'origin WebView change (upgrade Capacitor).
- Toute nouvelle émission de télémétrie passe par les wrappers de
services/telemetry.ts(cf.patterns.md→ « Émettre de la télémétrie »).

