8f2cd4e068
## 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
65 lines
2.4 KiB
TypeScript
65 lines
2.4 KiB
TypeScript
// MUST be the very first import — see apps/portal-bff/src/observability/tracing.ts
|
|
// for the reasoning. Anything `import`ed above this line bypasses the
|
|
// OpenTelemetry auto-instrumentations and is silently un-traced.
|
|
import './observability/tracing';
|
|
|
|
import { ValidationPipe } from '@nestjs/common';
|
|
import { NestFactory } from '@nestjs/core';
|
|
import { Logger } from 'nestjs-pino';
|
|
import { AppModule } from './app/app.module';
|
|
import { assertDatabaseUrl } from './config/check-database-url';
|
|
|
|
// Fail fast on a malformed DATABASE_URL (most often a special char in
|
|
// the password that needs URL-encoding) rather than letting Prisma
|
|
// surface a cryptic "invalid connection string" error mid-request.
|
|
assertDatabaseUrl();
|
|
|
|
async function bootstrap() {
|
|
// `bufferLogs: true` holds early-bootstrap log lines until the
|
|
// Pino-based Logger is wired in below, so we don't lose anything
|
|
// emitted before `app.useLogger()`.
|
|
const app = await NestFactory.create(AppModule, { bufferLogs: true });
|
|
app.useLogger(app.get(Logger));
|
|
|
|
// CORS — minimal dev-time allowlist. The SPA running on
|
|
// http://localhost:4200 issues fetches to the BFF and must be
|
|
// able to send the W3C `traceparent` (and `tracestate`) headers
|
|
// that `@opentelemetry/instrumentation-fetch` injects, so the BFF
|
|
// can pick up the parent span id and emit child spans on the same
|
|
// trace. The full security-grade allowlist (per-environment
|
|
// origins, credentials policy, helmet stack, etc.) lands with the
|
|
// phase-2 security ADR — for now this is the minimum needed for
|
|
// end-to-end tracing.
|
|
app.enableCors({
|
|
origin: (process.env['CORS_ALLOWED_ORIGINS'] ?? 'http://localhost:4200')
|
|
.split(',')
|
|
.map((o) => o.trim())
|
|
.filter(Boolean),
|
|
allowedHeaders: ['Content-Type', 'Accept', 'Authorization', 'traceparent', 'tracestate'],
|
|
credentials: true,
|
|
});
|
|
|
|
app.useGlobalPipes(
|
|
new ValidationPipe({
|
|
whitelist: true,
|
|
forbidNonWhitelisted: true,
|
|
transform: true,
|
|
}),
|
|
);
|
|
|
|
// Phase-2 security ADRs will harden the above: helmet, real CORS
|
|
// allowlist, cookie-session, CSRF protection, rate limiting, auth
|
|
// guards, structured error filter.
|
|
|
|
const globalPrefix = 'api';
|
|
app.setGlobalPrefix(globalPrefix);
|
|
const port = process.env['PORT'] ?? 3000;
|
|
await app.listen(port);
|
|
|
|
app
|
|
.get(Logger)
|
|
.log(`Application is running on: http://localhost:${port}/${globalPrefix}`, 'Bootstrap');
|
|
}
|
|
|
|
bootstrap();
|