Skip to content
Open
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
3 changes: 3 additions & 0 deletions packages/server/src/durable-objects/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type AgentConnectionMeta = {
origin_surface?: string;
origin_client?: string;
origin_version?: string;
harness?: string;
};

/**
Expand Down Expand Up @@ -320,6 +321,7 @@ export class AgentDO implements DurableObject {
agentId: url.searchParams.get('agent_id') ?? undefined,
connectedAtMs: Date.now(),
sessionScope: url.searchParams.get('session_scope') ?? 'agent',
harness: url.searchParams.get('harness') ?? 'unknown',
...normalizeTelemetryOrigin({
origin_surface: url.searchParams.get('origin_surface') ?? undefined,
origin_client: url.searchParams.get('origin_client') ?? undefined,
Expand Down Expand Up @@ -502,6 +504,7 @@ export class AgentDO implements DurableObject {
origin,
properties: {
workspace_id: resolvedWorkspaceId,
harness: meta.harness ?? 'unknown',
session_scope: meta.sessionScope ?? 'agent',
duration_ms: durationMs,
close_code: _code,
Expand Down
3 changes: 3 additions & 0 deletions packages/server/src/durable-objects/workspaceStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type WorkspaceConnectionMeta = {
origin_surface?: string;
origin_client?: string;
origin_version?: string;
harness?: string;
};

/**
Expand Down Expand Up @@ -46,6 +47,7 @@ export class WorkspaceStreamDO implements DurableObject {
workspaceId: url.searchParams.get('workspace_id') ?? undefined,
connectedAtMs: Date.now(),
sessionScope: url.searchParams.get('session_scope') ?? 'workspace',
harness: url.searchParams.get('harness') ?? 'unknown',
...normalizeTelemetryOrigin({
origin_surface: url.searchParams.get('origin_surface') ?? undefined,
origin_client: url.searchParams.get('origin_client') ?? undefined,
Expand Down Expand Up @@ -110,6 +112,7 @@ export class WorkspaceStreamDO implements DurableObject {
}),
properties: {
workspace_id: workspaceId,
harness: meta?.harness ?? 'unknown',
session_scope: meta?.sessionScope ?? 'workspace',
duration_ms: durationMs,
close_code: code,
Expand Down
6 changes: 6 additions & 0 deletions packages/server/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export interface AppVariables {
db: ReturnType<typeof import('./db/index.js').getDb>;
logger: Logger;
requestId: string;
/**
* Harness identifier derived from the X-Relaycast-Harness request header.
* Always set (defaults to `'unknown'`). Stamped on every server-side
* telemetry event for the request via `emitServerEvent`.
*/
harness: string;
}

/** The Hono Env type used throughout the app */
Expand Down
44 changes: 44 additions & 0 deletions packages/server/src/lib/__tests__/origin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';
import {
HARNESS_HEADER,
UNKNOWN_HARNESS,
extractHarness,
} from '../origin.js';

function headers(init: Record<string, string>): Headers {
return new Headers(init);
}

describe('extractHarness', () => {
it('returns unknown when the header is missing', () => {
expect(extractHarness(headers({}))).toBe(UNKNOWN_HARNESS);
});

it('returns unknown when the header is empty or whitespace', () => {
expect(extractHarness(headers({ [HARNESS_HEADER]: '' }))).toBe(UNKNOWN_HARNESS);
expect(extractHarness(headers({ [HARNESS_HEADER]: ' ' }))).toBe(UNKNOWN_HARNESS);
});

it('lowercases well-formed values', () => {
expect(extractHarness(headers({ [HARNESS_HEADER]: 'Claude-Code' }))).toBe('claude-code');
expect(extractHarness(headers({ [HARNESS_HEADER]: 'CURSOR' }))).toBe('cursor');
});

it('accepts unknown identifiers (we segment server-side later)', () => {
expect(extractHarness(headers({ [HARNESS_HEADER]: 'my-new-harness' }))).toBe('my-new-harness');
});

it('rejects oversized values', () => {
const long = 'a'.repeat(64);
expect(extractHarness(headers({ [HARNESS_HEADER]: long }))).toBe(UNKNOWN_HARNESS);
});

it('rejects disallowed characters (whitespace, slashes, etc.)', () => {
expect(extractHarness(headers({ [HARNESS_HEADER]: 'claude code' }))).toBe(UNKNOWN_HARNESS);
expect(extractHarness(headers({ [HARNESS_HEADER]: 'claude/code' }))).toBe(UNKNOWN_HARNESS);
expect(extractHarness(headers({ [HARNESS_HEADER]: 'claude;code' }))).toBe(UNKNOWN_HARNESS);
// Non-ASCII values are rejected at the platform Headers boundary before
// reaching this code; the regex provides defense-in-depth for any that
// slip through (e.g. via test harnesses that don't enforce the spec).
});
});
97 changes: 95 additions & 2 deletions packages/server/src/lib/__tests__/serverTelemetry.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import { describe, expect, it } from 'vitest';
import { normalizeRoutePathForTelemetry } from '../serverTelemetry.js';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Context } from 'hono';
import type { AppEnv } from '../../env.js';
import { emitServerEvent, normalizeRoutePathForTelemetry } from '../serverTelemetry.js';
import { HARNESS_HEADER } from '../origin.js';

vi.mock('../posthog.js', () => {
const mockCapture = vi.fn();
return {
getPostHogClient: vi.fn(() => ({
capture: mockCapture,
shutdown: vi.fn().mockResolvedValue(undefined),
})),
flushAllPostHogClients: vi.fn().mockResolvedValue(undefined),
telemetryEnabled: vi.fn(() => true),
};
});

describe('normalizeRoutePathForTelemetry', () => {
it('strips query/hash and normalizes repeated slashes', () => {
Expand All @@ -16,3 +31,81 @@ describe('normalizeRoutePathForTelemetry', () => {
expect(normalizeRoutePathForTelemetry('/v1/webhooks/wh_k3x9q2p7z1n4')).toBe('/v1/webhooks/:id');
});
});

type CapturedCall = { distinctId: string; event: string; properties: Record<string, unknown> };

function fakeContext(opts: {
headers?: Record<string, string>;
vars?: Record<string, unknown>;
}): Context<AppEnv> {
const headers = new Headers(opts.headers ?? {});
const vars = new Map<string, unknown>(Object.entries(opts.vars ?? {}));
const raw = new Request('https://example.com/v1/test', { headers });

return {
env: {
ENVIRONMENT: 'development',
POSTHOG_API_KEY: 'phc_test',
POSTHOG_HOST: 'https://us.i.posthog.com/',
} as unknown as AppEnv['Bindings'],
req: { raw },
get: (key: string) => vars.get(key),
set: (key: string, value: unknown) => {
vars.set(key, value);
},
} as unknown as Context<AppEnv>;
}

async function lastCaptureCall(): Promise<CapturedCall> {
// Allow the queued runInBackground microtask to drain.
await new Promise((resolve) => setTimeout(resolve, 0));
const { getPostHogClient } = await import('../posthog.js');
const clientMock = (getPostHogClient as ReturnType<typeof vi.fn>).mock.results.at(-1)?.value as
| { capture: ReturnType<typeof vi.fn> }
| undefined;
if (!clientMock) throw new Error('PostHog client was not constructed');
const call = clientMock.capture.mock.calls.at(-1);
if (!call) throw new Error('capture was not called');
return call[0] as CapturedCall;
}

describe('emitServerEvent — harness stamping', () => {
beforeEach(() => {
vi.clearAllMocks();
});

afterEach(() => {
vi.unstubAllGlobals();
});

it('stamps the harness from the request context variable', async () => {
const c = fakeContext({ vars: { harness: 'claude-code' } });
emitServerEvent(c, 'ws_123', 'relaycast_server_search_executed', {
query_length: 4,
result_count: 1,
});
const call = await lastCaptureCall();
expect(call.properties.harness).toBe('claude-code');
expect(call.properties.workspace_id).toBe('ws_123');
});

it('falls back to reading the header when the context variable is missing', async () => {
const c = fakeContext({ headers: { [HARNESS_HEADER]: 'cursor' } });
emitServerEvent(c, 'ws_123', 'relaycast_server_search_executed', {
query_length: 4,
result_count: 1,
});
const call = await lastCaptureCall();
expect(call.properties.harness).toBe('cursor');
});

it('defaults to "unknown" when neither context nor header is present', async () => {
const c = fakeContext({});
emitServerEvent(c, 'ws_123', 'relaycast_server_search_executed', {
query_length: 4,
result_count: 1,
});
const call = await lastCaptureCall();
expect(call.properties.harness).toBe('unknown');
});
});
38 changes: 38 additions & 0 deletions packages/server/src/lib/origin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,44 @@ import { normalizeTelemetryOrigin, type TelemetryOrigin } from '@relaycast/types

export type OriginInfo = Partial<TelemetryOrigin>;

/**
* HTTP header used by harnesses (Claude Code, Cursor, etc.) to identify
* themselves to the relaycast server. See relay#881.
*/
export const HARNESS_HEADER = 'X-Relaycast-Harness';

/** Fallback value when the header is missing or invalid. */
export const UNKNOWN_HARNESS = 'unknown';

/** Sanity-cap on the header value — long enough for any reasonable identifier. */
const HARNESS_MAX_LENGTH = 40;

/**
* Read and sanitize the `X-Relaycast-Harness` header from a request.
*
* Returns a lowercase identifier (kebab-case by convention, e.g. `claude-code`,
* `cursor`, `codex`). We intentionally do NOT enforce an enum here — accepting
* any well-formed value lets us discover new harnesses without shipping a
* relaycast release first. Segmentation/normalization happens downstream in
* the analytics layer.
*
* Drops empty, oversized, or non-ASCII values to `'unknown'`.
*/
export function extractHarness(headers: Headers): string {
const raw = headers.get(HARNESS_HEADER);
if (!raw) return UNKNOWN_HARNESS;

const trimmed = raw.trim();
if (!trimmed) return UNKNOWN_HARNESS;
if (trimmed.length > HARNESS_MAX_LENGTH) return UNKNOWN_HARNESS;
// Restrict to printable ASCII to keep PostHog property values clean. Allow
// letters, digits, and the small set of separators harness names tend to
// use. Anything else falls back to `unknown`.
if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) return UNKNOWN_HARNESS;

return trimmed.toLowerCase();
}

function sanitizeOriginPart(value: string | null | undefined, maxLen: number): string | undefined {
if (!value) return undefined;
const normalized = value.trim();
Expand Down
10 changes: 9 additions & 1 deletion packages/server/src/lib/serverTelemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { InternalTelemetryEvent } from '@relaycast/types';
import type { Context } from 'hono';
import type { AppEnv } from '../env.js';
import { runInBackground } from '../routes/background.js';
import { requiredOriginInfo } from './origin.js';
import { extractHarness, requiredOriginInfo, UNKNOWN_HARNESS } from './origin.js';
import { captureInternalTelemetryBatched, workspaceDistinctId } from './telemetry.js';

type ServerEvent = `relaycast_server_${string}`;
Expand Down Expand Up @@ -31,6 +31,13 @@ export function emitServerEvent(
normalizedProperties.route_path = normalizeRoutePathForTelemetry(normalizedProperties.route_path);
}

// 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;
Comment on lines +34 to +39
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).


runInBackground(
c,
captureInternalTelemetryBatched(c.env, {
Expand All @@ -39,6 +46,7 @@ export function emitServerEvent(
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

...normalizedProperties,
},
}),
Expand Down
6 changes: 5 additions & 1 deletion packages/server/src/middleware/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import crypto from 'node:crypto';
import type { MiddlewareHandler } from 'hono';
import type { AppEnv } from '../env.js';
import { createRequestLogger } from '../lib/logger.js';
import { deriveClientName, extractOriginInfo } from '../lib/origin.js';
import { deriveClientName, extractHarness, extractOriginInfo } from '../lib/origin.js';

type WaitUntilContext = {
executionCtx?: {
Expand Down Expand Up @@ -110,11 +110,14 @@ export const loggerMiddleware: MiddlewareHandler<AppEnv> = async (c, next) => {
const requestHeaders = c.req.raw.headers;
const clientName = deriveClientName(requestHeaders);
const originInfo = extractOriginInfo(c.req.raw, clientName);
const harness = extractHarness(requestHeaders);
c.set('harness', harness);

const logger = createRequestLogger(c, 'request', {
request_id: requestId,
...(clientName ? { client_name: clientName } : {}),
...originInfo,
harness,
});
c.set('logger', logger);

Expand Down Expand Up @@ -147,6 +150,7 @@ export const loggerMiddleware: MiddlewareHandler<AppEnv> = async (c, next) => {
...(connectingIp ? { ip_hash: hashIdentifier(connectingIp) } : {}),
...(userAgent ? { ua_hash: hashIdentifier(userAgent) } : {}),
...originInfo,
harness,
...errorInfo,
};

Expand Down
4 changes: 4 additions & 0 deletions packages/server/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ app.get('/v1/ws', async (c) => {
const workspaceId = agent.workspaceId;
const agentId = agent.id;
const origin = requiredOriginInfo(c.req.raw);
const harness = c.get('harness') ?? 'unknown';

// Register the agent as online in PresenceDO (fire-and-forget)
const presenceDoId = c.env.PRESENCE_DO.idFromName(workspaceId);
Expand All @@ -206,6 +207,7 @@ app.get('/v1/ws', async (c) => {
url.searchParams.set('origin_surface', origin.origin_surface);
url.searchParams.set('origin_client', origin.origin_client);
url.searchParams.set('origin_version', origin.origin_version);
url.searchParams.set('harness', harness);

const response = await stub.fetch(new Request(url.toString(), c.req.raw));
if (response.status === 101) {
Expand All @@ -232,11 +234,13 @@ app.get('/v1/ws', async (c) => {
const doId = c.env.WORKSPACE_STREAM_DO.idFromName(workspace.id);
const stub = c.env.WORKSPACE_STREAM_DO.get(doId);
const origin = requiredOriginInfo(c.req.raw);
const harness = c.get('harness') ?? 'unknown';
url.searchParams.set('workspace_id', workspace.id);
url.searchParams.set('session_scope', 'workspace');
url.searchParams.set('origin_surface', origin.origin_surface);
url.searchParams.set('origin_client', origin.origin_client);
url.searchParams.set('origin_version', origin.origin_version);
url.searchParams.set('harness', harness);

const response = await stub.fetch(new Request(url.toString(), c.req.raw));
if (response.status === 101) {
Expand Down
Loading