Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 25 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,44 @@ agents image (exposed at `/instrumentations/browser/agent.js`, mounted on nodes
`/var/odigos/browser`).

Unlike Odigos' server-side agents (PHP, Ruby, Node.js, ...), the browser agent does not run
inside the pod. It runs in the **end user's browser**. Odigos delivers it by injecting a
`<script>` tag into HTML responses via the `odigos-browser-proxy` sidecar, which also proxies
the browser's OTLP/HTTP telemetry back to the node-local collector (same-origin, so no CORS or
public ingress is required).
inside the pod. It runs in the **end user's browser**. Odigos delivers it via a **hardened
same-origin gateway** (`odigos-browser-proxy` on Kubernetes, `BrowserProxyController` on VMs)
that injects CSP-safe external `<script>` tags into HTML and relays authenticated OTLP/HTTP
telemetry to the local collector (no public collector ingress required).

## Docs

| Doc | Contents |
| --- | --- |
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Ecosystem map across agent / k8s / enterprise / vm-agent |
| [docs/DATA_FLOW.md](docs/DATA_FLOW.md) | How config and telemetry move through the system |
| [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md) | Threats that blocked the original design + mitigations |
| [docs/SECURITY.md](docs/SECURITY.md) | Concrete gateway + agent security controls |

## Architecture

```mermaid
flowchart TD
User["End-user browser"] -->|"GET / (HTML)"| SC["odigos-browser-proxy sidecar"]
User["End-user browser"] -->|"GET / (HTML)"| SC["Browser gateway"]
SC -->|"forward"| App["web server container<br/>(nginx/serve/etc)"]
App -->|"HTML response"| SC
SC -->|"inject script tag + recompress"| User
SC -->|"inject external script tags + recompress"| User
User -->|"GET /__odigos/config.js"| SC
User -->|"GET /__odigos/agent.js"| SC
User -->|"POST /__odigos/v1/traces (OTLP)"| SC
User -->|"POST /__odigos/v1/logs (OTLP)"| SC
SC -->|"forward + CORS"| NC["node-local collector :4318"]
User -->|"POST /__odigos/v1/traces\|logs<br/>Bearer exportToken"| SC
SC -->|"validate + rate limit + forward"| NC["node-local collector :4318"]
```

## What the bundle does

On load, `agent.js`:

1. Reads runtime configuration from `window.__ODIGOS__` (injected by the sidecar before this script).
1. Reads runtime configuration from `window.__ODIGOS__` (assigned by `/__odigos/config.js` before this script).
2. Initializes a `WebTracerProvider` with W3C trace-context propagation and a `BatchSpanProcessor`.
3. Initializes a `LoggerProvider` with a `BatchLogRecordProcessor` for browser events.
4. Exports traces and logs over OTLP/HTTP to same-origin paths served by the sidecar
(defaults `/__odigos/v1/traces` and `/__odigos/v1/logs`).
4. Exports traces and logs over OTLP/HTTP to same-origin gateway paths
(defaults `/__odigos/v1/traces` and `/__odigos/v1/logs`), attaching `Authorization: Bearer`
when `exportToken` is set.
5. Registers instrumentations (see below).

### Instrumentation stack
Expand Down Expand Up @@ -72,8 +82,9 @@ transitional span packages above.
| Field | Type | Default | Description |
| ------------------------------ | -------- | --------------------- | --------------------------------------------------------------------------------- |
| `serviceName` | string | page hostname | `service.name` resource attribute. |
| `tracesPath` | string | `/__odigos/v1/traces` | Same-origin OTLP/HTTP traces endpoint exposed by the sidecar. |
| `logsPath` | string | `/__odigos/v1/logs` | Same-origin OTLP/HTTP logs/events endpoint exposed by the sidecar. |
| `tracesPath` | string | `/__odigos/v1/traces` | Same-origin OTLP/HTTP traces endpoint exposed by the gateway. |
| `logsPath` | string | `/__odigos/v1/logs` | Same-origin OTLP/HTTP logs/events endpoint exposed by the gateway. |
| `exportToken` | string | _(required in prod)_ | Bearer token for OTLP POSTs; minted by the gateway into `config.js`. |
| `resourceAttributes` | object | `{}` | Extra resource attributes (e.g. `k8s.namespace.name`). |
| `propagateTraceHeaderCorsUrls` | string[] | same-origin | URLs that may receive trace-context headers. Wrap a value in `/.../` for a regex. |
| `samplingRatio` | number | `1` | Head sampling ratio in `[0, 1]`. |
Expand Down
75 changes: 75 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Browser Instrumentation Architecture

This document maps the Odigos browser (RUM) instrumentation ecosystem: how the agent
bundle is produced, how it is delivered into end-user browsers, and how telemetry flows
back into Odigos collectors on Kubernetes and VMs.

## Why browser is different

Server-side Odigos agents attach to a **process** (env vars, mounts, eBPF). The browser
SDK runs in the **end-user's browser**, so there is no PID to instrument. Delivery and
collection must happen at the HTTP layer in front of the HTML-serving workload.

## Ecosystem components

| Component | Repo | Role |
| --- | --- | --- |
| `agent.js` | `odigos-io/opentelemetry-browser` | Browser OpenTelemetry SDK bundle |
| `odigos-browser-proxy` | `odigos-io/odigos` (`browser-proxy/`) | K8s sidecar: HTML inject, asset serve, hardened OTLP gateway |
| Instrumentor webhook | `odigos-io/odigos` | Injects sidecar + redirect init for `browser-community` |
| Enterprise wiring | `odigos-io/odigos-enterprise` | Distro map + odiglet agent image pin |
| `BrowserProxyController` | `odigos-io/vm-agent` | In-process reverse proxy on VMs (explicit listen/upstream) |
| UI | `odigos-io/ui-kit` | Browser language logo / selection (no security surface) |

## High-level data flow

```mermaid
flowchart TD
User["End-user browser"] -->|"GET / (HTML)"| GW["Browser gateway<br/>(k8s sidecar or vm-agent proxy)"]
GW -->|"forward"| App["Web server"]
App -->|"text/html"| GW
GW -->|"inject external script tags<br/>+ recompress"| User
User -->|"GET /__odigos/config.js"| GW
User -->|"GET /__odigos/agent.js"| GW
User -->|"POST /__odigos/v1/traces\|logs<br/>Authorization: Bearer …"| GW
GW -->|"validate token + rate limit<br/>then forward"| Col["Node-local / managed collector"]
Col --> Dest["Configured destinations"]
```

## Delivery contract

The gateway never injects **inline** JavaScript into HTML (that breaks CSP `script-src`
without `'unsafe-inline'`). It injects only same-origin external tags:

```html
<script src="/__odigos/config.js"></script>
<script src="/__odigos/agent.js" async></script>
```

`/__odigos/config.js` is generated per gateway process and assigns `window.__ODIGOS__`
(service name, OTLP paths, resource attributes, **export token**, …). `agent.js` reads
that global and starts the SDK. See [DATA_FLOW.md](./DATA_FLOW.md) and
[SECURITY.md](./SECURITY.md).

## Platform variants

### Kubernetes

1. User sets Source `containerOverrides[].otelDistroName: browser-community`.
2. Instrumentor injects `odigos-browser-proxy` sidecar and an iptables redirect init
container (skipped when Istio/Linkerd sidecars are present).
3. Sidecar fronts the app container port, injects scripts, serves assets, relays OTLP
to `LocalTrafficOTLPHttpDataCollectionEndpoint`.

### VM agent

1. Operator configures a Source with `config.browser.listen` + `upstream` (explicit
reverse-proxy mode; no iptables in Phase 1–2).
2. `BrowserProxyController` owns the listener and talks to the managed local collector.

Both platforms share the same agent bundle and the same `window.__ODIGOS__` contract.

## Opt-in only

Browser workloads are not auto-detected from `/proc`. Instrumentation is always
explicit so SSR/Node containers are not accidentally switched to the browser distro.
94 changes: 94 additions & 0 deletions docs/DATA_FLOW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Data flow

End-to-end path of configuration and telemetry for Odigos browser instrumentation.

## 1. Build & distribute `agent.js`

```
opentelemetry-browser (this repo)
npm run build → dist/agent.js
release.Dockerfile → image with /instrumentations/browser/agent.js
├─▶ odiglet image (k8s) copies into node path /var/odigos/browser
└─▶ vm-agent image copies into /instrumentations/browser
```

The bundle is static. Per-workload values are **not** baked in; they arrive at runtime
via `window.__ODIGOS__` from `/__odigos/config.js`.

## 2. Page load (delivery)

```
Browser Gateway App
│ GET /page │ │
│─────────────────────────▶│ GET /page │
│ │─────────────────────────────▶│
│ │◀──── text/html (+gzip) ──────│
│ │ decompress → inject tags →
│ │ recompress gzip if needed
│◀── HTML with │
│ <script src=/__odigos/config.js>
│ <script src=/__odigos/agent.js async>
│ GET /__odigos/config.js │
│─────────────────────────▶│ dynamic JS assigning
│◀── window.__ODIGOS__=… │ exportToken + paths
│ GET /__odigos/agent.js │
│─────────────────────────▶│ static file from agent dir
│◀── agent.js │
│ agent start() reads window.__ODIGOS__
```

### `window.__ODIGOS__` fields used at runtime

| Field | Purpose |
| --- | --- |
| `serviceName` | `service.name` resource attribute |
| `tracesPath` / `logsPath` | Same-origin OTLP/HTTP paths on the gateway |
| `exportToken` | Bearer token required on OTLP POSTs |
| `resourceAttributes` | Extra resource attributes |
| `propagateTraceHeaderCorsUrls` | Fetch/XHR trace-context targets |
| `samplingRatio` | Head sampler ratio |
| `debug` | Console diagnostics |

## 3. Telemetry export (collection)

```
Browser Gateway Collector
│ POST /__odigos/v1/traces │ │
│ Authorization: Bearer <tok> │ │
│ Content-Type: … │ │
│──────────────────────────────▶│ 1. OPTIONS/CORS (strict) │
│ │ 2. Validate Bearer token │
│ │ 3. Rate-limit (IP + token) │
│ │ 4. Enforce body size cap │
│ │ 5. Same-site Origin/Referer check │
│ │ POST /v1/traces │
│ │─────────────────────────────────────▶│
│◀──────── status ──────────────│◀──────── status ─────────────────────│
```

Logs use `/__odigos/v1/logs` with the same auth and limits.

The gateway **strips** the browser `Authorization` header before forwarding to the
collector (the node-local collector is not public and does not expect that token).

## 4. Distributed tracing stitch

Fetch/XHR instrumentations attach W3C `traceparent` / `tracestate` to backends listed
in `propagateTraceHeaderCorsUrls` (default: same-origin only). Backend services
instrumented by Odigos continue the trace, so browser spans and server spans share a
`trace_id`.

## Reserved paths

| Path | Owner | Auth |
| --- | --- | --- |
| `/__odigos/config.js` | Gateway | Public (contains token; short cache) |
| `/__odigos/agent.js` | Gateway | Public (static SDK) |
| `/__odigos/healthz` | Gateway (k8s) | Public (probes only) |
| `/__odigos/v1/*` | Gateway → collector | **Bearer token + rate limit** |

Anything else is reverse-proxied to the upstream web server.
81 changes: 81 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Security controls (implementation)

Concrete controls shared by the k8s `odigos-browser-proxy` sidecar and the
vm-agent `BrowserProxyController`. The browser agent consumes the resulting
contract.

## 1. CSP-safe script injection

**Do not** inject inline scripts.

Injected markup:

```html
<script src="/__odigos/config.js"></script>
<script src="/__odigos/agent.js" async></script>
```

If the upstream `Content-Security-Policy` (or `-Report-Only`) contains a
`script-src` / `default-src` nonce, the gateway copies `nonce="<value>"` onto
both tags.

## 2. Export token on OTLP

- Gateway generates a cryptographically random token at process start (or loads
`ODIGOS_BROWSER_PROXY_EXPORT_TOKEN` / vm equivalent when provided).
- Token is embedded in `config.js` as `exportToken`.
- Agent sends `Authorization: Bearer <exportToken>` on traces and logs exports.
- Gateway rejects OTLP requests without a matching bearer token (`401`).
- Gateway does **not** forward the bearer header to the collector.

## 3. Rate limiting

OTLP paths use a token-bucket limiter keyed by client IP and by token:

| Dimension | Default (v2) |
| --- | --- |
| Per client IP | 120 requests / minute |
| Per export token | 240 requests / minute |
| Max body size | 1 MiB (OTLP) |

Exceeded limits return `429`.

## 4. Same-site checks & CORS

For OTLP POST:

- If `Origin` is present, its host must match the request `Host` (ignore port
differences for local dev only when both are loopback).
- Else if `Referer` is present, apply the same host match.
- Else allow (non-browser or privacy-restricted clients that still present a
valid bearer) — rate limits still apply.
- CORS preflight echoes the request `Origin` only when it passes the same-site
check; never `*`.
- `Access-Control-Allow-Headers` includes `authorization`, `content-type`, and
W3C trace context headers.

## 5. Response headers for static assets

| Path | Headers |
| --- | --- |
| `/__odigos/agent.js` | `Content-Type: application/javascript`, `X-Content-Type-Options: nosniff`, `Cache-Control: public, max-age=300` |
| `/__odigos/config.js` | same nosniff, `Cache-Control: private, max-age=60` |

## 6. Agent expectations

The agent **must**:

- Read `exportToken` from `window.__ODIGOS__` and attach the bearer header when set.
- Ignore its own OTLP URLs in network instrumentations (`resolveIgnoreUrls`).
- Default propagation targets to same-origin only.

The agent **must not** scrape passwords, cookies, or form inputs beyond what the
registered OpenTelemetry instrumentations emit.

## 7. Operator checklist

1. Enable browser instrumentation only on HTML-serving containers/sources you trust.
2. Ensure app CSP allows scripts from `'self'` (or add nonces / allow `/__odigos/`).
3. Keep the gateway in the request path healthy (k8s probes are mandatory).
4. Do not expose the node-local collector publicly; browsers should only talk to
`/__odigos/v1/*` on the app origin.
Loading
Loading