Skip to content

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 propagation traceparent — et le point 4 passe en errors-only : instrumentations réduites (plus de web vitals / vues / navigation, console.error seul) et http_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 mode e2e ne doit jamais initialiser Faro.
  • Le repo a des cycles d'imports réels (cf. setBackButtonRouter dans main.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.

  1. Exclusion du bundle par DCE, précédent Eruda (BuchardApp.vue) : le seul import runtime de @grafana/faro-web-sdk du repo est un import() dynamique dans une branche gardée par des comparaisons littérales import.meta.env.MODE === 'development' || === 'staging'. esbuild replie la condition en false sur 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 une VITE_* ne serait pas tree-shaké (vitejs/vite#15256).
  2. Garde d'URL dans la branche : l'init exige VITE_FARO_COLLECTOR_URL (committée dans .env.development et .env.staging, jamais dans .env que la prod charge). Les VITE_* é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.
  3. Façade feuille : telemetry.ts n'importe rien de src/ (seul un import 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/*.
  4. 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 ADR 0017 — errors-only : instrumentations explicites Errors/Console (error seul)/Session, http_request sur échec uniquement)
    • app.config.errorHandler + router.onError (main.ts) → trackError — indispensable : les erreurs attrapées par Vue n'atteignent jamais window.onerror ;
    • handleError/logError (services/error-handler.ts) → trackError (population disjointe du handler Vue : ce sont des erreurs déjà catchées) ;
    • router.afterEachsetView(route.name) uniquement — pas d'event route_change en plus (double comptage, faro-web-sdk#349) ;
    • les deux funnels HTTP (publicFetch, ApiClient.request) → event http_request{ method, path, status, duration_ms }. Query string strippée (origin + pathname) : la query porte du PII (customer.exists y met l'email) et des cache-busters ?_=. La durée côté ApiClient englobe refresh de token + retry 401 (latence perçue). customer.exists (appel CapacitorHttp.get direct) reste volontairement non tracké.
    • setUser({ id }) au fetchCurrentCustomer (+ restauration au boot depuis le store persisté), resetUser() au logout — id client seul, pas d'email/nom dans Loki.
  5. Pas de @grafana/faro-web-tracing pour l'instant (voir Contexte). Si le tracing distribué devient nécessaire : injection manuelle de traceparent dans les deux funnels + Horizon instrumenté OTel côté backend — sans quoi les spans ne se lient à rien. (Amendé par ADR 0017 : faro-web-tracing installé, spans HTTP manuels client-only, toujours sans traceparent.)

Config Alloy de référence

alloy
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 build puis grep -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 /collect non mockée ferait échouer le test (teardown assertNoUnmatched).
  • Activation = renseigner l'URL : tant que VITE_FARO_COLLECTOR_URL est 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 »).

Contributors

No contributors

Changelog

No recent changes