> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wrappy.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Logging

> Comment l'API et les serveurs SSR écrivent leurs logs : une ligne par requête enrichie au fil du traitement, des lignes à part pour les incidents, et l'export vers PostHog Logs en OpenTelemetry. Ce que le navigateur remonte, lui, passe par les exceptions et le replay.

## Le principe

L'API ne raconte pas ce que fait son code, elle dit **ce qui est arrivé à chaque requête**. Une
requête produit une seule ligne, `request completed`, écrite à sa sortie et qui réunit tout ce qu'on
a appris en la traitant : qui, quoi, combien de temps, comment ça s'est terminé.

```json theme={null}
{
  "level": 30,
  "msg": "request completed",
  "reqId": "4f1c…",
  "provenance": "studio",
  "method": "POST",
  "path": "/wrappys/w_1/send",
  "route": "/wrappys/:id/send",
  "status": 200,
  "durationMs": 184,
  "userId": "u_1",
  "posthogDistinctId": "u_1",
  "sessionId": "0199…",
  "wrappyId": "w_1",
  "recipientCount": 3
}
```

Une ligne riche se filtre et se regroupe sur n'importe lequel de ses champs (« tous les 5xx de
`/wrappys/:id/send` pour ce compte »), là où dix lignes éparses obligent à recoller les morceaux.

## Le circuit d'une requête

| Étape | Fichier | Rôle |
| - | - | - |
| Contexte | `infrastructure/middlewares/request-context.ts` | Crée `c.var.reqLogger` (enfant pino lié à `reqId`, `provenance`, `origin`, `method`, `path`) et l'événement vide `c.var.requestEvent`. |
| Traitement | routes et services | Ajoutent leur contexte avec `annotate({ … })`. |
| Erreur | `infrastructure/middlewares/error-handler.ts` | Annote `errorType`, `errorMessage` et le détail de l'erreur ; joint l'erreur complète (stack) pour un 5xx ou une erreur base. N'écrit pas de ligne. |
| Sortie | `infrastructure/middlewares/request-logger.ts` | Écrit la ligne `request completed` : `route`, `status`, `durationMs`, `userId`, `orgId`, `posthogDistinctId`, `sessionId`, les annotations, l'erreur. Rien pour `/health`. |

`annotate` retrouve la requête en cours par `contextStorage` de Hono : un service l'appelle sans
recevoir de logger. Hors requête (planificateur, script), il n'y a pas de ligne de fin, et
`annotate` écrit alors son contexte sur une ligne `info` à part.

```ts theme={null}
annotate({ wrappyId: gift.id, recipientCount: body.recipientEmails.length });
```

<Warning>
  `annotate` ne prend que des scalaires. Un objet ou une réponse d'API dans un log gonfle le volume
  et finit par y faire entrer des données personnelles.
</Warning>

## Le lien avec le replay

Studio et open initialisent posthog-js avec `tracing_headers` sur l'hôte de l'API : chaque appel
porte `X-POSTHOG-SESSION-ID` et `X-POSTHOG-DISTINCT-ID` (autorisés par le CORS de l'API). La ligne de
fin les reprend sous `sessionId` et `posthogDistinctId`, les noms que PostHog Logs reconnaît : depuis
une ligne, on ouvre le replay de la session et la personne. `posthogDistinctId` vaut l'id du compte
quand il est connecté — celui que le studio identifie — et l'id anonyme du navigateur sinon (un
destinataire sur open).

Les appels faits par le serveur du studio (SSR) n'ont pas de session : ces deux champs y sont vides.

## Les niveaux

La ligne de fin prend son niveau du statut de la réponse :

| Statut | Niveau |
| - | - |
| `5xx` | `error` |
| `4xx` sauf `404` | `warn` |
| `404`, `2xx`, `3xx` | `info` — les robots produisent des 404 en continu |

Les incidents s'écrivent sur une ligne à part, en `warn` (inattendu sans échec) ou `error` (quelqu'un
doit agir) : litige bancaire, remboursement sans commande, envoi échoué après paiement. C'est aussi
le cas de ce qui se passe **après** la réponse (promesses lancées sans attendre) ou hors requête : la
ligne de fin est déjà partie.

```ts theme={null}
logger.error({ err, wrappyId }, "send after payment failed, payment stays acquired");
```

Le message est une phrase fixe en anglais, l'erreur toujours sous `err` : c'est la seule clé que pino
sérialise avec son message et sa stack.

## Où partent les logs

| Environnement | Sortie |
| - | - |
| Dev (`NODE_ENV=development`) | `pino-pretty` dans le terminal : `[studio] request completed POST /wrappys 201 12ms {"userId":"u_1"}` |
| Prod | JSON sur stdout (capturé par Dokploy) et, si `POSTHOG_LOGS_ENABLED=true`, export OTLP vers PostHog Logs |

L'export vers PostHog se fait **dans le process** (`packages/observability/src/logger.ts`) : un
`LoggerProvider` OpenTelemetry avec un `BatchLogRecordProcessor` envoie les logs par lots à
`${POSTHOG_HOST}/i/v1/logs`, authentifiés par le token projet (`phc_…`). Le niveau pino devient la
sévérité OTel, le nom du service (`wrappy-api`) l'attribut de ressource `service.name`, les autres champs les
attributs du log.

<Note>
  Pas de transport pino pour cet export : un transport tourne dans un worker qui charge son module
  depuis `node_modules`, absent de l'image de prod qui ne copie que `dist/`.
</Note>

À l'arrêt du conteneur (`SIGTERM`), `shutdownLogs()` envoie le dernier lot avant de quitter. Un
plantage hors requête (`uncaughtException`, `unhandledRejection`) écrit une ligne `fatal`
`process crashed` avec l'erreur, envoie le dernier lot, puis quitte en code 1.

## Les serveurs SSR des fronts

Landing, open et studio sont servis par le même serveur Bun (`packages/shared/src/server/production-server.ts`).
Il reçoit de l'app le logger de `@wrappy/observability/logger` et suit les mêmes règles que l'API :

* une ligne `request completed` par page rendue (`reqId`, `method`, `path`, `status`, `durationMs`),
  au niveau que mérite le statut, l'erreur sous `err` si le rendu plante ; les assets statiques
  n'en écrivent pas ;
* export vers PostHog Logs sous le `service.name` `wrappy-landing`, `wrappy-open` ou `wrappy-studio` quand
  `POSTHOG_LOGS_ENABLED=true` et que le token et l'hôte sont fournis au runtime ;
* même arrêt propre au `SIGTERM` et même ligne `fatal` au plantage.

Le serveur pose un `x-request-id` sur chaque requête. Pendant le rendu, studio et open le
transmettent à leurs appels à l'API : la page SSR et la ligne API qu'elle a déclenchée partagent le
même `reqId`.

## Le navigateur

Le navigateur n'écrit pas de logs. Ce qu'il fait passe par PostHog : les événements produit, le
replay, et les exceptions (`capture_exceptions`, plus `captureException` dans les écrans d'erreur).
Une requête API qui échoue est déjà loguée par l'API, rattachée au replay par `sessionId` : la
reloguer côté navigateur la compterait deux fois.

Sur open, un lien inconnu ou expiré (404 de l'API) affiche la page introuvable ; tout autre échec
de la route `$token` est un incident, remonté à PostHog avec un écran « Quelque chose s'est mal
passé ».

## Ce qui ne sort pas

Pino masque en `[REDACTED]` les clés sensibles avant toute écriture, à la racine et un niveau plus
bas : `password`, `token`, `accessToken`, `refreshToken`, `apiKey`, `secret`, `email`, `mail`, plus
l'en-tête `authorization`. Le masquage est un filet, pas une permission : les corps de requête et de réponse ne
sont jamais logués, ni les données personnelles. Pour rattacher une ligne à un compte, `userId`
suffit.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.