From 8f125d2a90bf7778ecb9c373f49669b80074bcfc Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Mon, 11 May 2026 12:48:55 +0200 Subject: [PATCH] feat(portal-shell): wire environment.ts per ADR-0018 (#90) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary First implementation step of ADR-0018. Create [`src/environments/environment.ts`](apps/portal-shell/src/environments/environment.ts) holding the two SPA per-environment values the ADR calls out — `bffApiBaseUrl` and `otlpEndpoint` — and replace the hard-coded URLs at the two SPA call sites that needed them. ## What changes - **New `environment.ts`** with dev defaults (`http://localhost:3000/api` and `http://localhost:4318/v1/traces`). Header comment links to ADR-0018, documents the constraint that per-environment siblings must share the same shape, and notes that nothing here is a secret (the SPA bundle is public). - **`observability/tracing.ts`** reads `environment.otlpEndpoint` for the exporter, and **derives** the `propagateTraceHeaderCorsUrls` regex from `environment.bffApiBaseUrl` — a future change to the BFF origin propagates `traceparent` to the right host automatically, no second edit needed. - **`home-status.service.ts`** builds `/health` as `${environment.bffApiBaseUrl}/health`. ## What this PR explicitly does NOT do - **No `environment.staging.ts` / `environment.prod.ts`** yet. The ADR says "ship later" for those, and the real prod / staging URLs are unknown until the infrastructure ADR lands. Dropping plausible-but-wrong URLs into the repo would be worse than waiting. - **No `fileReplacements` configuration in `project.json`** — it depends on the per-environment files existing. Wired in the same PR that introduces them. - **No BFF-side audit pool split** (`AUDIT_DATABASE_URL` validator, second Prisma client, boot-time UPDATE-rejection self-test). Also in the ADR's Confirmation list, but it touches `AuditModule` and deserves its own review. Separate PR. - **No `SERVICE_VERSION` wiring** in `tracing.ts`. Still hard-coded to `'dev'`; the build-time version source (same one that will feed the footer's dev-only version badge) is its own small chantier. ## Test plan - [x] `pnpm exec nx run-many -t lint test build --projects=portal-shell` — green (36 / 36 specs unchanged, no new tests needed). - [x] Production build size unchanged (121 kB gzip initial — `environment.ts` is one literal object inlined by the bundler). - [ ] Manual: `pnpm exec nx serve portal-shell` → home page loads, the health widget hits the BFF, Jaeger shows the SPA `document_load` + `fetch` + BFF child span trace. - [ ] Manual: temporarily change `bffApiBaseUrl` to `http://localhost:9999/api` → the fetch fails (expected), and the `traceparent` propagation regex no longer matches `:3000` (verifiable in Network panel — header is absent on cross-origin requests). --------- Co-authored-by: Julien Gautier Reviewed-on: https://git.unespace.com/julien/apf_portal/pulls/90 --- .../src/app/pages/home/home-status.service.ts | 8 ++--- .../src/environments/environment.ts | 34 +++++++++++++++++++ .../portal-shell/src/observability/tracing.ts | 34 +++++++++++-------- 3 files changed, 58 insertions(+), 18 deletions(-) create mode 100644 apps/portal-shell/src/environments/environment.ts diff --git a/apps/portal-shell/src/app/pages/home/home-status.service.ts b/apps/portal-shell/src/app/pages/home/home-status.service.ts index be58888..a0ca886 100644 --- a/apps/portal-shell/src/app/pages/home/home-status.service.ts +++ b/apps/portal-shell/src/app/pages/home/home-status.service.ts @@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { catchError, map, of } from 'rxjs'; +import { environment } from '../../../environments/environment'; export interface BackendHealth { status: 'ok'; @@ -21,13 +22,12 @@ export type HomeStatus = * stack (CORS, OTel trace propagation, Pino log correlation — * everything we wired in the observability foundation PRs). * - * The base URL is hard-coded for now; it moves to Angular's - * `environment.ts` mechanism with the env-config PR (same one that - * will host the OTLP endpoint). + * The BFF base URL comes from `environment.ts` (per ADR-0018); the + * per-environment build target swaps the file via `fileReplacements`. */ @Injectable({ providedIn: 'root' }) export class HomeStatusService { - private static readonly HEALTH_URL = 'http://localhost:3000/api/health'; + private static readonly HEALTH_URL = `${environment.bffApiBaseUrl}/health`; private readonly http = inject(HttpClient); readonly status = toSignal( diff --git a/apps/portal-shell/src/environments/environment.ts b/apps/portal-shell/src/environments/environment.ts new file mode 100644 index 0000000..cb13b92 --- /dev/null +++ b/apps/portal-shell/src/environments/environment.ts @@ -0,0 +1,34 @@ +/** + * Per-environment configuration for the SPA, per ADR-0018. + * + * This file holds the **dev defaults** and is the one checked in. + * Per-environment siblings (`environment.staging.ts`, + * `environment.prod.ts`) land alongside this file later; the + * production / staging build configurations in `project.json` declare + * a `fileReplacements` entry that swaps this file for the target one + * at build time. + * + * Constraint: every sibling must export an `environment` object of + * the same shape. TypeScript will catch missing keys at the + * consuming site once a sibling exists. + * + * Nothing here is a secret. The SPA is a static bundle; secrets that + * ever needed to live here would, by definition, be public. Per-user + * / per-tenant secrets live in the BFF session payload (ADR-0010). + */ +export const environment = { + /** + * Origin + prefix of the BFF HTTP API. The SPA prepends this to + * every backend call (`${bffApiBaseUrl}/health`, etc.) and derives + * the OTel trace-header propagation pattern from its origin (see + * `observability/tracing.ts`). + */ + bffApiBaseUrl: 'http://localhost:3000/api', + + /** + * OTLP/HTTP traces endpoint. Targets the Collector in + * `infra/local/dev.compose.yml`. CORS is enabled there — see + * `infra/local/otel-collector.yaml`. + */ + otlpEndpoint: 'http://localhost:4318/v1/traces', +}; diff --git a/apps/portal-shell/src/observability/tracing.ts b/apps/portal-shell/src/observability/tracing.ts index f6cb14b..8eacebe 100644 --- a/apps/portal-shell/src/observability/tracing.ts +++ b/apps/portal-shell/src/observability/tracing.ts @@ -30,12 +30,9 @@ * * Endpoint configuration * ────────────────────── - * The OTLP/HTTP endpoint is hard-coded for v1 because env vars do - * not flow into the browser bundle natively (Angular's - * `environment.ts` mechanism is the standard alternative; we'll - * adopt it when prod build needs it). The default targets the - * Collector that ships in `infra/local/dev.compose.yml`. CORS is - * enabled on that Collector receiver — see `otel-collector.yaml`. + * The OTLP/HTTP endpoint and the BFF origin both come from + * `src/environments/environment.ts` (per ADR-0018). Per-environment + * siblings of that file ship under `fileReplacements`. */ import { WebTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-web'; @@ -47,16 +44,28 @@ import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch'; import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load'; import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction'; +import { environment } from '../environments/environment'; + const SERVICE_NAME = 'portal-shell'; +// Static for now; a follow-up wires this to the build-time version +// (same source as the footer's dev-only version badge). const SERVICE_VERSION = 'dev'; -const OTLP_ENDPOINT = 'http://localhost:4318/v1/traces'; + +// Derive the trace-header propagation pattern from the BFF base URL +// so a deploy-time change to `bffApiBaseUrl` automatically propagates +// `traceparent` to the right origin. RegExp special chars are escaped +// before going into the source. +const bffOrigin = new URL(environment.bffApiBaseUrl).origin; +const bffOriginRegex = new RegExp(`^${bffOrigin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/.*`); const provider = new WebTracerProvider({ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: SERVICE_NAME, [ATTR_SERVICE_VERSION]: SERVICE_VERSION, }), - spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({ url: OTLP_ENDPOINT }))], + spanProcessors: [ + new BatchSpanProcessor(new OTLPTraceExporter({ url: environment.otlpEndpoint })), + ], }); provider.register(); @@ -65,13 +74,10 @@ registerInstrumentations({ instrumentations: [ // Times the initial page load — fires once. new DocumentLoadInstrumentation(), - // Wraps every `fetch` call. Propagates `traceparent` to the - // listed origins; the BFF dev server is the one we care about - // today. Add prod origins once they exist. + // Wraps every `fetch` call. Propagates `traceparent` to the BFF + // origin derived from the environment. new FetchInstrumentation({ - propagateTraceHeaderCorsUrls: [ - /^http:\/\/localhost:3000\/.*/, // BFF dev server - ], + propagateTraceHeaderCorsUrls: [bffOriginRegex], }), // Auto-spans on click / keypress / submit (and a small allow-list // beyond that). Quiet in dev, useful in staging perf