Conception

Architecture

Deux chaînes séparées

Code
Application Next.js / Node │ ┌────────────┴────────────┐ │ │ @workspace/analytics @workspace/observability │ │ Événements typés SDK OpenTelemetry Consentement, identité Journaux, métriques, traces Files, routage │ │ Collecteur OTLP ┌──────┼──────┐ │ │ │ │ ▼ Rybbit Umami Matomo OpenObserve

Audience et observabilité ne se mélangent pas. Un événement métier ne part pas dans les traces, et un span ne part pas chez un fournisseur d'audience. Seuls traceId et spanId circulent dans les journaux, comme identifiants de corrélation.

Pourquoi packages/ sans monorepo

Le cahier des charges interdit de transformer un dépôt applicatif en monorepo, et ce dépôt n'en est pas un : pnpm-workspace.yaml existe mais ne déclare aucun paquet.

Les paquets vivent donc dans packages/, autonomes, résolus par un alias @workspace/* dans tsconfig.json. Aucune clé packages: n'est ajoutée à pnpm-workspace.yaml, aucune sémantique d'installation ne change. Copier packages/ dans un autre projet et ajouter deux lignes d'alias suffit à le réutiliser.

Frontière navigateur / serveur

EntréeContenuInterdits
@workspace/analyticsClient, consentement, caviardage, adaptateursAucun secret, aucune dépendance Node
@workspace/analytics/serverIdem plus lecture des secretsNe jamais importer côté client
@workspace/observabilityFaçade log / counter / withSpan—
@workspace/observability/serverSDK NodeNe jamais importer côté client

browserConfigFromEnv ne lit que des variables NEXT_PUBLIC_*. Les jetons d'écriture (MATOMO_TOKEN_AUTH, RYBBIT_API_KEY) ne sont lus que par serverConfigFromEnv.

Chemin d'un événement

  1. analytics.track('feature_used', { feature: 'recherche' }) — nom et charge vérifiés à la compilation.
  2. Consentement : refus ou mode non satisfait → rejet compté, rien ne part.
  3. Validation Zod : charge non conforme → rejet compté et diagnostic, rien ne part.
  4. Caviardage : clés interdites et motifs sensibles remplacés.
  5. Enveloppe : eventId, version de schéma, horodatage, environnement, identité anonyme.
  6. Mise en file, une file par fournisseur.
  7. Livraison par lots, avec réessais à délai croissant et gigue.

Aucune de ces étapes ne lève d'exception chez l'appelant. Une panne d'analytics ne doit jamais casser une requête métier.

Isolation des pannes

Une file et un compteur par fournisseur. Un fournisseur injoignable accumule, réessaie, puis abandonne au-delà du plafond — sans affecter les autres. Vérifié par le test de fumée.

Limite connue : les files sont indexées par nom de fournisseur, donc une seule instance par type.

Ce que la livraison garantit

Au mieux, avec déduplication quand le fournisseur la gère via eventId. Pas d'exactement-une- fois. Les abandons sont comptés (droppedOverflow, droppedExpired, droppedExhausted) et lisibles par getStats().

Réutilisation ailleurs

Dépendant du projet : core/events.ts (le catalogue) et les variables d'environnement. Indépendant : tout le reste. Les adaptateurs ne connaissent aucun nom d'événement.