Skip to content

feat(telemetry): read X-Relaycast-Harness header and stamp on events (P0 of relay#881)#132

Open
willwashburn wants to merge 2 commits into
mainfrom
feat/telemetry-harness-header
Open

feat(telemetry): read X-Relaycast-Harness header and stamp on events (P0 of relay#881)#132
willwashburn wants to merge 2 commits into
mainfrom
feat/telemetry-harness-header

Conversation

@willwashburn
Copy link
Copy Markdown
Member

@willwashburn willwashburn commented May 18, 2026

Server side of the harness instrumentation in AgentWorkforce/relay#881. Reads the X-Relaycast-Harness HTTP header and the harness WS query param, sanitises both, and stamps the value on every server-side PostHog event.

What

  • New helper extractHarness(headers) in packages/server/src/lib/origin.ts — lowercase, ASCII-only ([a-zA-Z0-9._-]), 40-char cap. Anything off-shape falls back to 'unknown'.
  • loggerMiddleware reads the header once at request entry and stashes it on c.var.harness (typed via AppVariables.harness: string in env.ts) so every downstream emitter and log line sees the same value without re-parsing the header.
  • emitServerEvent prefers the context variable, falls back to direct header read, then to UNKNOWN_HARNESS. The value is injected into every server-side PostHog event's properties.harness.
  • For WS sessions the value is forwarded via search-params to AgentDO / WorkspaceStreamDO so the eventual ws_session_ended event in the DO also carries it.

Rename note (commit 455b091)

Originally named everything orchestrator_harness to be unambiguous. Renamed across all three PRs (relay#888 + relaycast#132 + relaycast#133) to drop the redundant orchestrator_ prefix — the HTTP header was always X-Relaycast-Harness, the SDK origin field is harness, so the server-side property name might as well match. Renaming now (before events land in PostHog with the longer name) keeps the property schema honest.

Server-side renames in this PR:

  • PostHog event property: orchestrator_harnessharness
  • Helper function: extractOrchestratorHarness()extractHarness()
  • Constants: ORCHESTRATOR_HARNESS_HEADERHARNESS_HEADER, UNKNOWN_ORCHESTRATOR_HARNESSUNKNOWN_HARNESS
  • Hono context var: c.get('orchestratorHarness')c.get('harness')
  • DO connection meta field and WS query param: orchestrator_harnessharness

Tests

  • packages/server/src/lib/__tests__/origin.test.ts — 7 tests on extractHarness: missing / empty / lowercases / accepts unknown / oversized / disallowed chars.
  • packages/server/src/lib/__tests__/serverTelemetry.test.ts — 5 tests on emitServerEvent covering both context-var and header fallback paths, plus the unknown default.
  • Existing test suites unchanged.

Paired PRs

  • relay#888 — CLI/broker detects harness, stamps X-Relaycast-Harness on outgoing requests (currently via a fetch interceptor; will switch to the SDK origin path once feat(sdk): optional harness on InternalOrigin → X-Relaycast-Harness #133 publishes).
  • relaycast#133@relaycast/sdk 1.2.0 adds harness to InternalOrigin so the relay-side caller can replace the fetch interceptor with a clean origin-field set.

🤖 Generated with Claude Code

Read the X-Relaycast-Harness HTTP header (set by relay's MCP client per
relay#881) and propagate it through to every server-side PostHog event
as the orchestrator_harness property. Lets us segment usage by the host
orchestrator (claude-code, cursor, codex, ...) without depending on the
free-form User-Agent.

The value is sanitized (lowercased, ASCII-only, length-capped) but not
strict-enum-validated, so a relay client can ship a new harness ident
without a server release. Missing / oversized / non-ASCII falls back to
"unknown".

Plumbing:
- New extractOrchestratorHarness helper in lib/origin.ts.
- loggerMiddleware reads the header and stashes it on c.var so every
  request log line and downstream telemetry call sees the same value.
- emitServerEvent injects orchestrator_harness into the per-event
  properties.
- For WS sessions the value is forwarded via search-params to AgentDO /
  WorkspaceStreamDO so the eventual ws_session_ended event in the DO
  also carries it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Extracts a harness identifier from the X-Relaycast-Harness header, normalizes and validates it, stores it on request context, stamps it into server telemetry events, and propagates it through WebSocket upgrade URLs into DO connection metadata for session-ended telemetry.

Changes

Orchestrator Harness Tracking

Layer / File(s) Summary
Header extraction and validation
packages/server/src/lib/origin.ts, packages/server/src/lib/__tests__/origin.test.ts
Adds HARNESS_HEADER and UNKNOWN_HARNESS, and extractHarness(headers) which trims, enforces max length, validates allowed chars, lowercases, and falls back to 'unknown'. Tests cover missing/blank, normalization, oversized, and disallowed-character cases.
Request context and logging
packages/server/src/env.ts, packages/server/src/middleware/logger.ts
AppVariables includes harness: string. loggerMiddleware calls extractHarness, sets c.set('harness', ...), and includes harness in the per-request logger fields and metadata used for warn/error logs.
Server telemetry stamping
packages/server/src/lib/serverTelemetry.ts, packages/server/src/lib/__tests__/serverTelemetry.test.ts
emitServerEvent derives harness from c.get('harness'), falls back to extractHarness on raw headers, then to UNKNOWN_HARNESS; it adds harness to telemetry properties. Tests mock PostHog to assert harness stamping from context, header, and default.
WebSocket propagation & DO storage
packages/server/src/worker.ts, packages/server/src/durable-objects/agent.ts, packages/server/src/durable-objects/workspaceStream.ts
Worker reads harness from context and forwards it as the harness query parameter to agent and workspace WS proxies. Agent and Workspace DOs store harness in connection metadata and include it in relaycast_server_ws_session_ended telemetry properties on close.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I sniff the header, small and spry,
A harness name I trim and try,
Into context, logs, and webs it hops,
Telemetry hears my happy chops,
A tiny hop that ties the sky 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding X-Relaycast-Harness header reading and stamping on telemetry events (P0 of relay#881).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description clearly explains the changes: reading the X-Relaycast-Harness HTTP header, sanitizing it, and stamping the value on server-side PostHog events, with detailed notes on implementation, testing, and related PRs.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/telemetry-harness-header

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/server/src/lib/serverTelemetry.ts">

<violation number="1" location="packages/server/src/lib/serverTelemetry.ts:49">
P2: `orchestrator_harness` can be overridden by `properties` because `...normalizedProperties` is spread after it. Move the stamped field after the spread so request-derived value always wins.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Fix all with cubic | Re-trigger cubic

Comment on lines 49 to 50
orchestrator_harness: orchestratorHarness,
...normalizedProperties,
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: orchestrator_harness can be overridden by properties because ...normalizedProperties is spread after it. Move the stamped field after the spread so request-derived value always wins.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/server/src/lib/serverTelemetry.ts, line 49:

<comment>`orchestrator_harness` can be overridden by `properties` because `...normalizedProperties` is spread after it. Move the stamped field after the spread so request-derived value always wins.</comment>

<file context>
@@ -39,6 +46,7 @@ export function emitServerEvent(
       origin: requiredOriginInfo(c.req.raw),
       properties: {
         workspace_id: workspaceId,
+        orchestrator_harness: orchestratorHarness,
         ...normalizedProperties,
       },
</file context>
Suggested change
orchestrator_harness: orchestratorHarness,
...normalizedProperties,
...normalizedProperties,
orchestrator_harness: orchestratorHarness,
Fix with Cubic

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

@github-actions
Copy link
Copy Markdown

Preview deployed!

Environment URL
API https://pr132-api.relaycast.dev
Health https://pr132-api.relaycast.dev/health
Observer https://pr132-observer.relaycast.dev/observer

This preview shares the staging database and will be cleaned up when the PR is merged or closed.

Run E2E tests

npm run e2e -- https://pr132-api.relaycast.dev --ci

Open observer dashboard

https://pr132-observer.relaycast.dev/observer

`harness` alone is unambiguous in this context and pairs with the
`X-Relaycast-Harness` HTTP header name (already correct). Renaming
before events land in PostHog with the longer name keeps the property
schema honest.

Server-side renames:
  - PostHog event property: `orchestrator_harness` → `harness` (logger
    middleware request log line + `emitServerEvent` body + DO
    `ws_session_ended` event).
  - Helper function: `extractOrchestratorHarness()` → `extractHarness()`.
  - Constants: `ORCHESTRATOR_HARNESS_HEADER` → `HARNESS_HEADER`,
    `UNKNOWN_ORCHESTRATOR_HARNESS` → `UNKNOWN_HARNESS`.
  - Hono context var: `c.get('orchestratorHarness')` → `c.get('harness')`
    (typed via `AppVariables.harness: string` in `env.ts`).
  - DO connection meta field: `orchestrator_harness` → `harness` on
    both `WorkspaceConnectionMeta` and the agent DO equivalent.
  - WS query param forwarded from worker → DOs: `orchestrator_harness`
    → `harness` (matches the SDK-side rename in relaycast#133).
  - Tests updated to assert the new shape.

Paired with relay#888 (CLI/broker stamps `harness` on every event) and
relaycast#133 (SDK forwards `harness` on origin / WS query param).
HTTP header name `X-Relaycast-Harness` was already correct — unchanged
in the wire shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/server/src/lib/serverTelemetry.ts`:
- Around line 34-39: The final "?? UNKNOWN_HARNESS" is dead code because
extractHarness(...) already always returns a string; update the harness
derivation to remove the unreachable fallback so it reads: prefer
c.get('harness') and otherwise use extractHarness(c.req.raw.headers). Locate the
harness assignment (variable harness) and the call to extractHarness in
serverTelemetry.ts and simplify the expression to remove the third fallback
(UNKNOWN_HARNESS).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 64530d64-f767-4b20-90f4-45e73b92c7ed

📥 Commits

Reviewing files that changed from the base of the PR and between 109864b and 455b091.

📒 Files selected for processing (9)
  • packages/server/src/durable-objects/agent.ts
  • packages/server/src/durable-objects/workspaceStream.ts
  • packages/server/src/env.ts
  • packages/server/src/lib/__tests__/origin.test.ts
  • packages/server/src/lib/__tests__/serverTelemetry.test.ts
  • packages/server/src/lib/origin.ts
  • packages/server/src/lib/serverTelemetry.ts
  • packages/server/src/middleware/logger.ts
  • packages/server/src/worker.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/server/src/durable-objects/agent.ts

Comment on lines +34 to +39
// Prefer the value stashed by the logger middleware. Fall back to reading
// the header directly so emitters that bypass middleware (e.g. test harnesses
// or routes mounted before loggerMiddleware) still get a sane value.
const harness = c.get('harness')
?? extractHarness(c.req.raw.headers)
?? UNKNOWN_HARNESS;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unreachable fallback in harness derivation chain.

The third fallback ?? UNKNOWN_HARNESS is unreachable because extractHarness always returns a string (never null or undefined). When c.get('harness') is undefined, extractHarness(c.req.raw.headers) is called and will return either a valid harness string or UNKNOWN_HARNESS directly, so the final ?? never triggers.

🧹 Proposed fix to remove dead code
-  const harness = c.get('harness')
-    ?? extractHarness(c.req.raw.headers)
-    ?? UNKNOWN_HARNESS;
+  const harness = c.get('harness') ?? extractHarness(c.req.raw.headers);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Prefer the value stashed by the logger middleware. Fall back to reading
// the header directly so emitters that bypass middleware (e.g. test harnesses
// or routes mounted before loggerMiddleware) still get a sane value.
const harness = c.get('harness')
?? extractHarness(c.req.raw.headers)
?? UNKNOWN_HARNESS;
// Prefer the value stashed by the logger middleware. Fall back to reading
// the header directly so emitters that bypass middleware (e.g. test harnesses
// or routes mounted before loggerMiddleware) still get a sane value.
const harness = c.get('harness') ?? extractHarness(c.req.raw.headers);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/lib/serverTelemetry.ts` around lines 34 - 39, The final
"?? UNKNOWN_HARNESS" is dead code because extractHarness(...) already always
returns a string; update the harness derivation to remove the unreachable
fallback so it reads: prefer c.get('harness') and otherwise use
extractHarness(c.req.raw.headers). Locate the harness assignment (variable
harness) and the call to extractHarness in serverTelemetry.ts and simplify the
expression to remove the third fallback (UNKNOWN_HARNESS).

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/server/src/lib/serverTelemetry.ts">

<violation number="1" location="packages/server/src/lib/serverTelemetry.ts:49">
P1: This rename changes the telemetry property key from `orchestrator_harness` to `harness`, which breaks the expected event schema for server telemetry.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Fix all with cubic | Re-trigger cubic

origin: requiredOriginInfo(c.req.raw),
properties: {
workspace_id: workspaceId,
harness,
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This rename changes the telemetry property key from orchestrator_harness to harness, which breaks the expected event schema for server telemetry.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/server/src/lib/serverTelemetry.ts, line 49:

<comment>This rename changes the telemetry property key from `orchestrator_harness` to `harness`, which breaks the expected event schema for server telemetry.</comment>

<file context>
@@ -46,7 +46,7 @@ export function emitServerEvent(
       properties: {
         workspace_id: workspaceId,
-        orchestrator_harness: orchestratorHarness,
+        harness,
         ...normalizedProperties,
       },
</file context>
Suggested change
harness,
orchestrator_harness: harness,
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant