feat(portal-shell): wire spa-side opentelemetry tracing (#72)
## Summary Phase 2 of ADR-0012 — closes the loop SPA → BFF → DB. After this PR, a single user action (initial page load, click, form submit) produces one trace whose root span is owned by the SPA and whose child spans cover the BFF request, Postgres queries through Prisma, and (eventually) Redis / downstream-API hops. ## What lands **Browser-side OTel libs** (production deps): - `@opentelemetry/sdk-trace-web` — browser tracer + provider - `@opentelemetry/exporter-trace-otlp-http` — OTLP/HTTP+JSON exporter - `@opentelemetry/instrumentation` — auto-instrumentation runtime - `@opentelemetry/instrumentation-fetch` — `fetch` + W3C `traceparent` propagation - `@opentelemetry/instrumentation-document-load` — initial-paint timings - `@opentelemetry/instrumentation-user-interaction` — click / keypress / submit No `@opentelemetry/context-zone`: the workspace is zoneless per ADR-0004; the default `StackContextManager` covers the auto-instrumented paths. Custom spans across `await` will need explicit `context.with(...)` plumbing — fine, encountered as code lands. **Code**: - [`apps/portal-shell/src/observability/tracing.ts`](apps/portal-shell/src/observability/tracing.ts) — `WebTracerProvider` bootstrap. Documents the load-order constraint inline (same pattern as the BFF: must be the very first import of `main.ts`, otherwise auto-instrumentations miss everything imported above). - `apps/portal-shell/src/main.ts` now imports the tracing module as line 1. **CORS plumbing** for end-to-end trace propagation: - BFF (`apps/portal-bff/src/main.ts`) calls `enableCors` with a minimal dev allowlist (`http://localhost:4200`) and explicit permission for the W3C `traceparent` / `tracestate` headers. The full security-grade CORS (per-environment allowlists, helmet, cookie-session, CSRF) belongs to the future phase-2 security ADR — this PR adds the strict minimum for the SPA→BFF trace context to survive cross-origin pre-flight. - OTel Collector (`infra/local/otel-collector.yaml`) gains a `cors` block on its OTLP/HTTP receiver so the browser's own OTLP POST clears its pre-flight. **ADR-0012 §Confirmation** rewritten: a new "Wired in the SPA foundation PR (phase 2)" block enumerates what landed here; the carry-over "Wired as features land" list drops the SPA-side SDK item and adds a follow-up note about the security-grade CORS. ## Verification ```bash pnpm exec nx run-many -t lint test build # 8 projects green pnpm audit # 0 vulns ./infra/local/dev.sh up observability # bring up Collector + Jaeger ./infra/local/dev.sh # (separately, BFF stack — your choice) pnpm nx serve portal-bff # localhost:3000 pnpm nx serve portal-shell # localhost:4200 ``` Open http://localhost:4200 → a `document_load` trace appears in http://localhost:16686 with `service.name=portal-shell`. From DevTools, run `fetch('http://localhost:3000/api/health').then(r => r.json())` → a fetch span appears with a child BFF span on the same trace. ## Test plan - [ ] CI green on this PR. - [ ] After local up, `document_load` span visible in Jaeger UI for the SPA. - [ ] Cross-origin fetch from SPA carries `traceparent` (visible in Network tab) and produces a single end-to-end trace SPA → BFF in Jaeger. - [ ] DevTools console shows no CORS warnings about `traceparent`, `tracestate`, or the `localhost:4318/v1/traces` POST. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #72
This commit was merged in pull request #72.
This commit is contained in:
@@ -197,15 +197,23 @@ The BFF refuses to start if `LOG_USER_ID_SALT` or `OTEL_SERVICE_NAME` is missing
|
||||
- `apps/portal-bff/src/health/health.controller.ts` exposes `GET /api/health` (cheap liveness — process metadata only, no DB / Redis / downstream check).
|
||||
- `apps/portal-bff/.env.example` declares `LOG_LEVEL`, `OTEL_SERVICE_NAME`, `OTEL_SERVICE_VERSION`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_PROTOCOL`, `OTEL_TRACES_SAMPLER` with sensible dev defaults.
|
||||
|
||||
**Wired in the SPA foundation PR (phase 2):**
|
||||
|
||||
- `apps/portal-shell/src/observability/tracing.ts` initialises a `WebTracerProvider` with the OTLP/HTTP+JSON exporter targeting the same Collector. Auto-instrumentations active: `document-load` (initial-paint timings), `fetch` (every outgoing request, with `traceparent` propagation to the BFF dev origin), `user-interaction` (clicks / keypresses / submits).
|
||||
- `apps/portal-shell/src/main.ts` `import`s `./observability/tracing` as its very first line — same patching constraint as the BFF.
|
||||
- BFF `enableCors` allows the SPA dev origin (`http://localhost:4200`) and the `traceparent` / `tracestate` headers, so the W3C trace context survives the cross-origin pre-flight.
|
||||
- The OTel Collector receiver (`infra/local/otel-collector.yaml`) enables CORS for the SPA dev origin so the browser's OTLP POST clears its own pre-flight.
|
||||
- Endpoint is hard-coded to the local-dev Collector for v1; per-environment configuration via Angular's `environment.ts` lands when the first non-dev build target is set up.
|
||||
|
||||
**Wired as the corresponding features land:**
|
||||
|
||||
- CLS keys `session_id`, `user_id_hash`, `audience` — populated by guards/interceptors when ADR-0009 / ADR-0010 land.
|
||||
- `LOG_USER_ID_SALT` enforced (BFF refuses to start without it) — same trigger.
|
||||
- Pino `redact` list for PII paths — wired alongside the first DTO that carries a redactable field.
|
||||
- Custom spans `tracer.startActiveSpan('domain.<verb>', …)` — added per service method that warrants one (creating a row, calling a downstream, etc.).
|
||||
- SPA-side `@opentelemetry/sdk-trace-web` initialiser, OTLP/HTTP exporter targeting the Collector, `traceparent` header propagated to the BFF — phase-2 PR.
|
||||
- Custom spans `tracer.startActiveSpan('domain.<verb>', …)` — added per service method that warrants one (creating a row, calling a downstream, etc.). On the SPA side, custom spans across `await` need explicit `context.with(...)` since we are zoneless.
|
||||
- Integration tests: full SPA → BFF → DB trace with correct parent-child structure; every log line for one request carries the same `trace_id`; redact list strips its targets — wired with the auth + first real screen.
|
||||
- A separate `/readiness` endpoint that probes Postgres / Redis / OBO cache — wired with the first dependency that has a readiness story to tell.
|
||||
- Full security-grade CORS (per-environment allowlists, helmet, cookie-session, CSRF) — phase-2 security ADR.
|
||||
- OTel Collector deployment configuration in the runtime manifest — phase 3b on-prem ADR. The application stays backend-agnostic (any OTLP-capable Collector works).
|
||||
|
||||
## Pros and Cons of the Options
|
||||
|
||||
Reference in New Issue
Block a user