feat(portal-shell): wire environment.ts per ADR-0018
CI / scan (pull_request) Successful in 2m21s
CI / commits (pull_request) Successful in 2m21s
CI / check (pull_request) Successful in 2m31s
CI / a11y (pull_request) Successful in 2m38s
CI / perf (pull_request) Successful in 4m51s

Create `src/environments/environment.ts` holding the two SPA
per-environment values referenced by ADR-0018: `bffApiBaseUrl`
(default `http://localhost:3000/api`) and `otlpEndpoint` (default
`http://localhost:4318/v1/traces`). The dev defaults match the
current local Docker compose stack — `nx serve portal-shell` keeps
working unchanged.

Replace the hard-coded URLs at the two call sites:

- `observability/tracing.ts` reads `environment.otlpEndpoint` for the
  OTLP exporter, and derives the `propagateTraceHeaderCorsUrls`
  regex from `environment.bffApiBaseUrl` — a future deploy-time
  change to the BFF origin propagates `traceparent` to the right
  place automatically.
- `home-status.service.ts` builds the `/health` URL as
  `${environment.bffApiBaseUrl}/health`.

Per-environment siblings (`environment.staging.ts`,
`environment.prod.ts`) and the `fileReplacements` configuration in
`project.json` land later — explicitly out of scope today per the
ADR ("ship later"); their values are not known yet and dropping
plausible-but-wrong production URLs into the repo would be worse
than waiting for the infrastructure ADR.
This commit is contained in:
Julien Gautier
2026-05-11 12:45:38 +02:00
parent c5e91f240b
commit 03733feeb1
3 changed files with 58 additions and 18 deletions
@@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop'; import { toSignal } from '@angular/core/rxjs-interop';
import { catchError, map, of } from 'rxjs'; import { catchError, map, of } from 'rxjs';
import { environment } from '../../../environments/environment';
export interface BackendHealth { export interface BackendHealth {
status: 'ok'; status: 'ok';
@@ -21,13 +22,12 @@ export type HomeStatus =
* stack (CORS, OTel trace propagation, Pino log correlation — * stack (CORS, OTel trace propagation, Pino log correlation —
* everything we wired in the observability foundation PRs). * everything we wired in the observability foundation PRs).
* *
* The base URL is hard-coded for now; it moves to Angular's * The BFF base URL comes from `environment.ts` (per ADR-0018); the
* `environment.ts` mechanism with the env-config PR (same one that * per-environment build target swaps the file via `fileReplacements`.
* will host the OTLP endpoint).
*/ */
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class HomeStatusService { 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); private readonly http = inject(HttpClient);
readonly status = toSignal( readonly status = toSignal(
@@ -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',
};
+20 -14
View File
@@ -30,12 +30,9 @@
* *
* Endpoint configuration * Endpoint configuration
* ────────────────────── * ──────────────────────
* The OTLP/HTTP endpoint is hard-coded for v1 because env vars do * The OTLP/HTTP endpoint and the BFF origin both come from
* not flow into the browser bundle natively (Angular's * `src/environments/environment.ts` (per ADR-0018). Per-environment
* `environment.ts` mechanism is the standard alternative; we'll * siblings of that file ship under `fileReplacements`.
* 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`.
*/ */
import { WebTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-web'; 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 { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction'; import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction';
import { environment } from '../environments/environment';
const SERVICE_NAME = 'portal-shell'; 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 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({ const provider = new WebTracerProvider({
resource: resourceFromAttributes({ resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: SERVICE_NAME, [ATTR_SERVICE_NAME]: SERVICE_NAME,
[ATTR_SERVICE_VERSION]: SERVICE_VERSION, [ATTR_SERVICE_VERSION]: SERVICE_VERSION,
}), }),
spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({ url: OTLP_ENDPOINT }))], spanProcessors: [
new BatchSpanProcessor(new OTLPTraceExporter({ url: environment.otlpEndpoint })),
],
}); });
provider.register(); provider.register();
@@ -65,13 +74,10 @@ registerInstrumentations({
instrumentations: [ instrumentations: [
// Times the initial page load — fires once. // Times the initial page load — fires once.
new DocumentLoadInstrumentation(), new DocumentLoadInstrumentation(),
// Wraps every `fetch` call. Propagates `traceparent` to the // Wraps every `fetch` call. Propagates `traceparent` to the BFF
// listed origins; the BFF dev server is the one we care about // origin derived from the environment.
// today. Add prod origins once they exist.
new FetchInstrumentation({ new FetchInstrumentation({
propagateTraceHeaderCorsUrls: [ propagateTraceHeaderCorsUrls: [bffOriginRegex],
/^http:\/\/localhost:3000\/.*/, // BFF dev server
],
}), }),
// Auto-spans on click / keypress / submit (and a small allow-list // Auto-spans on click / keypress / submit (and a small allow-list
// beyond that). Quiet in dev, useful in staging perf // beyond that). Quiet in dev, useful in staging perf