Module: Foam::Otel::SessionStitching

Defined in:
lib/foam/otel/session_stitching.rb

Overview

Session stitching (owner ruling 2026-07-26): the browser package stamps session.id on every signal and propagates it to backends as W3C Baggage (baggage: session.id=... beside traceparent). Every server package's floor gains this tiny baggage→attributes pass: when an inbound request carries the session baggage, foam copies session.id (and session.previous_id across a rotation) onto every span and log record it emits — so one user session stitches browser → backend with no ingest-side join. No-op when the baggage is absent (backend-only traffic pays one hash lookup per key). Never raises (rule 9).

Foam's W3C propagator set already includes Baggage (init.rb configure_propagation), so the inbound extraction is free; the CORS side (Access-Control-Allow-Headers: traceparent, baggage) is the FDE's wiring obligation, documented in the README.

These processors ride FOAM'S OWN pipeline only (door 1) — a door-2 tap never mutates the customer's records (additive readers, ingest.rb).

Defined Under Namespace

Classes: LogRecordProcessor, SpanProcessor

Constant Summary collapse

SESSION_ID_KEY =
"session.id"
SESSION_PREVIOUS_ID_KEY =
"session.previous_id"
BAGGAGE_KEYS =
[SESSION_ID_KEY, SESSION_PREVIOUS_ID_KEY].freeze

Class Method Summary collapse

Class Method Details

.session_attributes(context) ⇒ Object

The session baggage entries present in context, as an attributes hash — {} when none (the no-op path). Values pass through VERBATIM except for encoding hygiene: the baggage extractor percent-DECODES header values, so a hostile inbound baggage: session.id=%FF%FE would stamp invalid UTF-8 onto EVERY span/log of the request and the upstream OTLP encoder would drop each WHOLE batch (rule 15) — scrub_utf8 replaces invalid bytes with U+FFFD and touches nothing else (the same capture-time pass api.rb applies).



39
40
41
42
43
44
45
46
47
48
49
# File 'lib/foam/otel/session_stitching.rb', line 39

def session_attributes(context)
  BAGGAGE_KEYS.each_with_object({}) do |key, out|
    value = OpenTelemetry::Baggage.value(key, context: context)
    next unless value.is_a?(String) && !value.empty?

    value = Redaction.scrub_utf8(value)
    out[key] = value if value.is_a?(String) && !value.empty?
  end
rescue StandardError, SystemStackError
  {}
end