Repository navigation
Add opt-in self-host collector analytics - #636
Conversation
Add CollectorTelemetrySink, an opt-in TelemetrySink that projects engine observations and exceptions through a typed allowlist into a versioned export envelope and POSTs bounded batches to an installation-local collector Service using the agent-relay.analytics.batch/v1 wire contract (infra PR 84). The default no-op sink and the telemetry port are unchanged; nothing is exported unless the collector URL is configured, and there is no hosted fallback. - telemetry/exportEnvelope.ts: pure projection; required facts per kind plus a small per-kind optional allowlist; actor type only, never the actor id, credential hash, sender, content, tokens or request data; exceptions export only error class name, code-shaped code, allowlisted operation and workspace id. - telemetry/batcher.ts: bounded queue, one batch in flight, bounded retries with backoff, split and terminal hints, shutdown drain with deadline, counters (enqueued, exported, dropped, queueFull, rejected, failedBatches, retries, splits), rate-limited stderr line, never re-captured into a sink. - telemetry/httpTransport.ts: the only wire module; schema id, field mapping, bearer credential, response table (400/401/403/other 4xx and redirects terminal, 413 split, 429/503/507/5xx/timeouts retried with a capped Retry-After), https-only except loopback, *.svc and single-label hosts, redirects never followed, bodies never read. - Node adapter and startServer accept telemetryCollector; stop() drains. - docker entrypoint: RELAYCAST_TELEMETRY_COLLECTOR_URL, _TOKEN(_FILE), _DEPLOYMENT, _QUEUE_MAX, _BATCH_MAX, _FLUSH_MS; malformed values and a pinned engine without collector support exit 64 before engine import. - Tests for projection, batcher, transport, sink, entrypoint parsing and a Node wiring integration test against a local HTTP collector. - docs/self-hosting.md and changelogs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…cations
- Only 204 is acceptance; any other 2xx is a retryable protocol error.
- The collector URL is the complete endpoint (no path appended); a query
string or fragment is refused, in the transport and the entrypoint.
- Retry-After: unsigned decimal delta-seconds only, capped at 30 s, clipped
to the delivery and shutdown deadlines; HTTP-dates and malformed values
are ignored and 0 never allows extra attempts.
- Per-event total delivery deadline (deliveryDeadlineMs, default 60 s) from
enqueue across queued time, attempts and waits; splits and retries keep
it, per-attempt timeouts are clipped to it, exhaustion counts `expired`.
- error_name / error_code come from registered allowlists ('unregistered'
or omitted otherwise); tenant_id is never sent.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 6 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
Privacy: replace the regex-shaped fact filter with a telemetry-owned analytics allowlist. ANALYTICS_FIELD_RULES gives every exported field a value rule (opaque id, count, number, flag or fixed enum -> 'other'); ANALYTICS_EVENT_FIELDS lists each kind's fields; ANALYTICS_DROPPED_FIELDS names the port fields never exported (agent_name, invited_agent_name, channel_name, command, action_name, slug, skill, emoji, provider). Origin strings become registered enums (origin_app, origin_actor_type, origin_client); the actor name and client version are gone, as is the hostname `instance`. Tests enumerate every ENGINE_OBSERVATION_FACTS field. Classification: error names and codes are exported only from the registries; code-shaped strings are no longer a path. Delivery: only 204 is acceptance (200/202/206 retried in budget, then lost). 401/403 are terminal configuration failures (configFailures counter, CONFIGURATION FAILURE log). Redirects are manual and terminal. 413 splits to a bounded depth and counts oversized events. Per-event, per-batch and queue budgets are measured in UTF-8 bytes; oversized events are dropped and counted; queue exhaustion drops the newest event and counts queueFull. trace_id is always set (request id, else a random UUID) and stays stable across retries and splits. Config: when the collector is enabled, a bearer token and an explicit deployment id are required (no hostname fallback), refused with missing_telemetry_collector_token / missing_telemetry_collector_deployment_id in the engine and the container entrypoint. All bounds are configurable through options and RELAYCAST_TELEMETRY_* env with conservative producer defaults (infra#85 publishes no numbers), documented in docs/self-hosting.md and the changelogs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
The claim that At that exact source head, the synthetic source-level reproducer confirmed: Please retain #636 as HOLD and queue repairs with causal tests on the same source branch. PostgreSQL implementation may remain the first priority; this comment does not request a priority switch. The canonical contract is |
|
Corrected exact SHA for the repairs:
|
|
Review of exact fix head
Validation at this exact head: Bun 1.4.2, one Vitest worker, no file parallelism, all through Container release gap remains separate: pinned engine 12.8.0 still refuses collector mode; no compatible released image or retained-file end-to-end proof follows from this source review. Please return the corrected frozen head and causal tests. |
Repairs the #636 review (HOLD) and re-review findings against the agent-relay.analytics.batch/v1 collector contract, each with a causal test (named F1..F10) that fails on 48a2086 and passes here. - F1/F2: properties are re-projected at the wire through the per-kind allowlist and the fixed exception classification (exportProperties). - F3: only 204 is acceptance; other 2xx are counted protocol errors. - F4: trace_id is mandatory and never invented; no tenant_id; fixed scope registry (EXPORT_EVENT_SCOPES) enforced by validateExportEvent in the sink and the transport. - F5: RELAYCAST_TELEMETRY_DESTINATION / telemetryDestination; collector mode without URL or token is a hard error; token charset is validated without echoing it. - F6: a deployment id equal to a replica hostname is refused. - F7/F8: in-flight batches count against queue bounds; split halves inherit the parent deadline; attempts are abandoned at the deadline even if the transport ignores timeoutMs; BACKOFF_MS and RETRY_AFTER_MAX_MS knobs (Retry-After capped at 30 s). - F9: workspace_id must match the native workspace-id grammar (isArWorkspaceId); other text is counted invalid and never exported. - F10: at the shutdown deadline the in-flight attempt is aborted through BatchAttempt.signal, counted once, and a late completion is ignored. Delivery stays at-least-once at best; no exactly-once guarantee. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Fix head for review 6717272197: Residual 1 (workspace attribution grammar) Residual 2 (shutdown cancels an in-flight attempt) Also in this head (repairs from the first review that were only partly covered at 48a2086, each with a causal test): F1 per-kind allowlist re-applied in Causality check 48a2086 source files restored under the new tests: 124 of 229 vitest tests fail and 10 of 47 container tests fail; the only F-tagged tests that already held on 48a2086 are the 129-char exception negative and the single-Authorization-header guard, which is now named as a guard, not a causal test. Receipts at
🤖 Generated with Claude Code |
|
Selected delta review at f6da7b3, against the previous reviewed head 48a2086: the two P2 findings in 6717272197 are resolved at this head.
New P3: immediate shutdown accounting is not fully settled. A real-clock reproduction through Independent validation on this exact head: real This is selected source evidence, not landing or runtime clearance: the PR is conflicting against its current base, the pinned released engine/container still refuses collector mode, and the deployed collector has no Relaycast event registry yet. Compose onto current main, obtain exact-head CI, coordinate the typed collector registry, then qualify the compatible package/image before enabling a producer. No release, credentials, chart enablement or live export was performed by this review. |
Keep the single collector lifecycle across the SQLite and PostgreSQL entrypoints. Settle outstanding queue accounting before shutdown resolves, and clear a pump that expires every queued event synchronously. Validated with the real worktree npm ci, all three workspace builds, 280 focused tests, and 47 container-entrypoint tests on Node 24.22.2. Relative to main the diff passes git diff --check.
| */ | ||
| const SNOWFLAKE_ID = /^[0-9]{1,20}$/; | ||
| const UUID_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; | ||
| const PREFIXED_ID = /^(?:dm|dmch|gdm|sub|wh|whd|inv|act|file|aud|cmd|node)_[A-Za-z0-9-]{8,96}$/; |
There was a problem hiding this comment.
🟡 Identity events lose their audit IDs
When an agent identity changes, isOpaqueId rejects its aid_ audit ID. Exported takeover and revocation events omit the audit record identifier.
Learn more
The engine generates identity audit records with aid_ IDs in agentIdentity. The takeover and revocation routes pass those IDs as audit_id to emitServerEvent. The analytics field rule for audit_id calls isOpaqueId, which rejects every generated aid_ ID. Both events therefore export without their audit IDs.
Example: An identity takeover creates aid_1189827393826947072. Its exported agent.identity.taken.over event retains agent_id but has no audit_id, preventing correlation with that audit record.
Recommended fix: Add the aid prefix to isOpaqueId and test both identity event projections using an aid_ ID generated by the engine.
| const PREFIXED_ID = /^(?:dm|dmch|gdm|sub|wh|whd|inv|act|file|aud|cmd|node)_[A-Za-z0-9-]{8,96}$/; | |
| const PREFIXED_ID = /^(?:dm|dmch|gdm|sub|wh|whd|inv|act|file|aud|aid|cmd|node)_[A-Za-z0-9-]{8,96}$/; |
Was this helpful? React with 👍 or 👎 to provide feedback.
| */ | ||
| export function projectException(err: unknown, context: unknown, identity: ExportIdentity): ExportEvent | null { | ||
| const ctx: Record<string, unknown> = context && typeof context === 'object' ? context as Record<string, unknown> : {}; | ||
| const traceId = traceIdOf(ctx.request_id ?? ctx.requestId); |
There was a problem hiding this comment.
🟡 Exception context gains forbidden casing fallback
When request_id is absent, projectException accepts requestId as its trace. This mixed-case field fallback violates the repository's explicit casing rule.
| const traceId = traceIdOf(ctx.request_id ?? ctx.requestId); | |
| const traceId = traceIdOf(ctx.request_id); |
Was this helpful? React with 👍 or 👎 to provide feedback.
| - Collector delivery: queue count, queue bytes, batch count, batch bytes, event bytes, flush interval, attempts, request timeout, delivery deadline and shutdown timeout are all bounded and configurable (`queueMax` 1000, `queueMaxBytes` 1048576, `batchMax` 50, `batchMaxBytes` 262144, `eventMaxBytes` 8192, `flushIntervalMs` 2000, `maxAttempts` 3, `requestTimeoutMs` 5000, `deliveryDeadlineMs` 60000, `shutdownTimeoutMs` 5000). These are conservative producer defaults; the collector contract does not publish numbers. Byte budgets are measured in serialized UTF-8 bytes (`utf8Bytes`, `wireEventBytes`, `WIRE_BATCH_FRAME_BYTES`): an event over `eventMaxBytes` is dropped and counted as `oversized`, batches are formed under both `batchMax` and `batchMaxBytes`, and when the queue count or byte budget is exhausted the newest event is dropped and counted as `queueFull`. The per-event delivery deadline starts at enqueue and covers queued time, all attempts and all waits; a batch uses its earliest event's deadline, retries and `413` splits never reset it, each request timeout is clipped to the time left, and exhausted events are dropped and counted as `expired`. Only `204` is acceptance; `200`, `202`, `206` and any other `2xx` are protocol errors, retried within the budget and then counted as loss. Collector responses follow the collector contract: `400` and other `4xx` are terminal and counted as `rejected`; `401`/`403` are terminal configuration failures (counted in `configFailures` and logged with a `CONFIGURATION FAILURE` prefix under their own rate limit); redirects use `redirect: 'manual'` and are never followed or retried with the token; `413` splits the batch to a bounded depth and a single event that is still too large is counted as `oversized`; `429`/`503`/`507`/other `5xx`/timeouts are retried. `Retry-After` is honored only as unsigned decimal delta-seconds, capped at 30 s and clipped to the delivery and shutdown deadlines (HTTP-dates and malformed values are ignored; `0` never allows extra attempts). `stats()` reports `enqueued`, `exported`, `dropped`, `queueFull`, `oversized`, `configFailures`, `rejected`, `expired`, `failedBatches`, `retries`, `splits` and `queuedBytes`. Failures log a rate-limited stderr line and are never re-captured. The URL is the complete endpoint including `/v1/events` (no path is appended) and must not carry credentials, a query string or a fragment; it must be https except for loopback, `*.svc` / `*.svc.cluster.local` and single-label hosts. | ||
| - `createNodeRuntime` / `startServer` accept `telemetryCollector: { url, bearerToken, deployment, queueMax?, queueMaxBytes?, batchMax?, batchMaxBytes?, eventMaxBytes?, flushIntervalMs?, maxAttempts?, requestTimeoutMs?, deliveryDeadlineMs?, shutdownTimeoutMs? }` (an explicit `telemetry` still wins). `bearerToken` and `deployment` are required, with no hostname fallback: a missing one throws `CollectorConfigError` with code `missing_telemetry_collector_token` or `missing_telemetry_collector_deployment_id`, and a malformed deployment id throws `invalid_telemetry_collector_deployment_id`. `NodeRuntime.closeTelemetry()` drains the sink and `RunningServer.stop()` awaits that drain. Without the option the no-op sink is unchanged. | ||
|
|
||
| - Collector review repairs (#636): `exportProperties(event)` re-projects properties at the wire through the per-kind allowlist and the fixed exception classification, so a hand-built `ExportEvent` cannot export `agent_name`, other product facts, or raw exception names/codes. `EXPORT_EVENT_SCOPES` fixes each kind's scope (observations: workspace; `exception`: installation-only, `workspace_id` always null) and `validateExportEvent(event, identity?)` returns an `ExportEventInvalidReason` (`unregistered_kind`, `missing_trace_id`, `missing_event_id`, `missing_workspace_id`, `unauthorized_workspace_id`, `tenant_id_not_allowed`, `deployment_mismatch`, `component_mismatch`); the sink counts refused events in `stats().invalid` (`CollectorTelemetryStats`) and the HTTP transport refuses such a batch before sending. Non-`204` `2xx`, `507` and redirects are counted in `protocolErrors`, `storageFailures` and `redirects`. The bearer token must match `BEARER_TOKEN_PATTERN` (visible ASCII, no whitespace), else `invalid_telemetry_collector_token`; a deployment id equal to the replica hostname (`replicaHostnames`, by default `os.hostname()` and `HOSTNAME`) is refused with `invalid_telemetry_collector_deployment_id`. `resolveTelemetryDestination` / `TELEMETRY_DESTINATIONS` and `telemetryDestination` on `createNodeRuntime` / `startServer` select `none` or `collector` explicitly (`missing_telemetry_collector_config`, `unsupported_telemetry_destination`). New options `backoffMs` (500) and `retryAfterMaxMs` (30000, at most 30000). The batch being sent counts against `queueMax` / `queueMaxBytes` (`inFlight`, `inFlightBytes`); `413` split halves inherit the parent's delivery deadline; an attempt that outlives its timeout is abandoned even if the transport ignores `timeoutMs` (`attemptTimeouts`). `workspace_id` must match the native workspace-id grammar (`isArWorkspaceId`, exported as `workspaceIdOf`); observations with any other workspace text are counted `invalid` and never sent, and exceptions never carry a workspace id. At the shutdown deadline the batcher aborts the in-flight attempt through the new `BatchAttempt.signal` (the HTTP transport cancels its fetch), counts that batch once as dropped and ignores a late completion; no exactly-once delivery is implied. |
There was a problem hiding this comment.
🟡 Collector changelog publishes review backstory
The Collector review repairs (#636) entry exposes internal review notes in release notes. The changelog rules require concise, impact-first entries without PR references or implementation backstory.
Learn more
The package changelog is published as user-facing release guidance. Its new collector entries span several long implementation lists, and the review-repairs bullet includes a PR number and internal review narrative. The repository's changelog rules require one short, impact-first bullet per user-visible change and exclude internal review notes and PR references.
Example: A release reader encounters Collector review repairs (#636) and detailed notes about internal fixes instead of a short description of what opting into collector analytics provides.
Recommended fix: Consolidate the new collector bullets into concise user-visible release notes; remove the review-repairs bullet and PR reference, preserving API details only where they help users migrate or configure the feature.
Was this helpful? React with 👍 or 👎 to provide feedback.
[Medium impact] The PR adds an opt-in analytics path across self-host server startup and engine observations. The repository’s field-naming and changelog requirements must be satisfied before merging; the image-support warning also needs correction.
|
| */ | ||
| export function projectException(err: unknown, context: unknown, identity: ExportIdentity): ExportEvent | null { | ||
| const ctx: Record<string, unknown> = context && typeof context === 'object' ? context as Record<string, unknown> : {}; | ||
| const traceId = traceIdOf(ctx.request_id ?? ctx.requestId); |
There was a problem hiding this comment.
projectException adds ctx.request_id ?? ctx.requestId, which violates the repository directive “Do not introduce mixed-case field fallbacks.” Accept only request_id and remove the test that requires the alternate spelling. This repository requirement must be satisfied before merging.
Context Used: CLAUDE.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/engine/src/telemetry/exportEnvelope.ts
Line: 545
Comment:
**Two request-id spellings**
`projectException` adds `ctx.request_id ?? ctx.requestId`, which violates the repository directive “Do not introduce mixed-case field fallbacks.” Accept only `request_id` and remove the test that requires the alternate spelling. This repository requirement must be satisfied before merging.
**Context Used:** CLAUDE.md ([source](https://github.com/agentworkforce/relaycast/blob/main/CLAUDE.md))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| - Collector delivery: queue count, queue bytes, batch count, batch bytes, event bytes, flush interval, attempts, request timeout, delivery deadline and shutdown timeout are all bounded and configurable (`queueMax` 1000, `queueMaxBytes` 1048576, `batchMax` 50, `batchMaxBytes` 262144, `eventMaxBytes` 8192, `flushIntervalMs` 2000, `maxAttempts` 3, `requestTimeoutMs` 5000, `deliveryDeadlineMs` 60000, `shutdownTimeoutMs` 5000). These are conservative producer defaults; the collector contract does not publish numbers. Byte budgets are measured in serialized UTF-8 bytes (`utf8Bytes`, `wireEventBytes`, `WIRE_BATCH_FRAME_BYTES`): an event over `eventMaxBytes` is dropped and counted as `oversized`, batches are formed under both `batchMax` and `batchMaxBytes`, and when the queue count or byte budget is exhausted the newest event is dropped and counted as `queueFull`. The per-event delivery deadline starts at enqueue and covers queued time, all attempts and all waits; a batch uses its earliest event's deadline, retries and `413` splits never reset it, each request timeout is clipped to the time left, and exhausted events are dropped and counted as `expired`. Only `204` is acceptance; `200`, `202`, `206` and any other `2xx` are protocol errors, retried within the budget and then counted as loss. Collector responses follow the collector contract: `400` and other `4xx` are terminal and counted as `rejected`; `401`/`403` are terminal configuration failures (counted in `configFailures` and logged with a `CONFIGURATION FAILURE` prefix under their own rate limit); redirects use `redirect: 'manual'` and are never followed or retried with the token; `413` splits the batch to a bounded depth and a single event that is still too large is counted as `oversized`; `429`/`503`/`507`/other `5xx`/timeouts are retried. `Retry-After` is honored only as unsigned decimal delta-seconds, capped at 30 s and clipped to the delivery and shutdown deadlines (HTTP-dates and malformed values are ignored; `0` never allows extra attempts). `stats()` reports `enqueued`, `exported`, `dropped`, `queueFull`, `oversized`, `configFailures`, `rejected`, `expired`, `failedBatches`, `retries`, `splits` and `queuedBytes`. Failures log a rate-limited stderr line and are never re-captured. The URL is the complete endpoint including `/v1/events` (no path is appended) and must not carry credentials, a query string or a fragment; it must be https except for loopback, `*.svc` / `*.svc.cluster.local` and single-label hosts. | ||
| - `createNodeRuntime` / `startServer` accept `telemetryCollector: { url, bearerToken, deployment, queueMax?, queueMaxBytes?, batchMax?, batchMaxBytes?, eventMaxBytes?, flushIntervalMs?, maxAttempts?, requestTimeoutMs?, deliveryDeadlineMs?, shutdownTimeoutMs? }` (an explicit `telemetry` still wins). `bearerToken` and `deployment` are required, with no hostname fallback: a missing one throws `CollectorConfigError` with code `missing_telemetry_collector_token` or `missing_telemetry_collector_deployment_id`, and a malformed deployment id throws `invalid_telemetry_collector_deployment_id`. `NodeRuntime.closeTelemetry()` drains the sink and `RunningServer.stop()` awaits that drain. Without the option the no-op sink is unchanged. | ||
|
|
||
| - Collector review repairs (#636): `exportProperties(event)` re-projects properties at the wire through the per-kind allowlist and the fixed exception classification, so a hand-built `ExportEvent` cannot export `agent_name`, other product facts, or raw exception names/codes. `EXPORT_EVENT_SCOPES` fixes each kind's scope (observations: workspace; `exception`: installation-only, `workspace_id` always null) and `validateExportEvent(event, identity?)` returns an `ExportEventInvalidReason` (`unregistered_kind`, `missing_trace_id`, `missing_event_id`, `missing_workspace_id`, `unauthorized_workspace_id`, `tenant_id_not_allowed`, `deployment_mismatch`, `component_mismatch`); the sink counts refused events in `stats().invalid` (`CollectorTelemetryStats`) and the HTTP transport refuses such a batch before sending. Non-`204` `2xx`, `507` and redirects are counted in `protocolErrors`, `storageFailures` and `redirects`. The bearer token must match `BEARER_TOKEN_PATTERN` (visible ASCII, no whitespace), else `invalid_telemetry_collector_token`; a deployment id equal to the replica hostname (`replicaHostnames`, by default `os.hostname()` and `HOSTNAME`) is refused with `invalid_telemetry_collector_deployment_id`. `resolveTelemetryDestination` / `TELEMETRY_DESTINATIONS` and `telemetryDestination` on `createNodeRuntime` / `startServer` select `none` or `collector` explicitly (`missing_telemetry_collector_config`, `unsupported_telemetry_destination`). New options `backoffMs` (500) and `retryAfterMaxMs` (30000, at most 30000). The batch being sent counts against `queueMax` / `queueMaxBytes` (`inFlight`, `inFlightBytes`); `413` split halves inherit the parent's delivery deadline; an attempt that outlives its timeout is abandoned even if the transport ignores `timeoutMs` (`attemptTimeouts`). `workspace_id` must match the native workspace-id grammar (`isArWorkspaceId`, exported as `workspaceIdOf`); observations with any other workspace text are counted `invalid` and never sent, and exceptions never carry a workspace id. At the shutdown deadline the batcher aborts the in-flight attempt through the new `BatchAttempt.signal` (the HTTP transport cancels its fetch), counts that batch once as dropped and ignores a late completion; no exactly-once delivery is implied. |
There was a problem hiding this comment.
The new entries repeat the configuration and delivery specification, and this entry includes internal review history. AGENTS.md requires one short, impact-first bullet per user-visible change and says to omit internal review notes. Summarize the shipped behavior and link to the detailed guide before merging. The same pattern appears in CHANGELOG.md:27 and packages/engine/CHANGELOG.md:21, among other new entries.
Context Used: AGENTS.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/engine/CHANGELOG.md
Line: 24
Comment:
**Release notes bury the change**
The new entries repeat the configuration and delivery specification, and this entry includes internal review history. `AGENTS.md` requires one short, impact-first bullet per user-visible change and says to omit internal review notes. Summarize the shipped behavior and link to the detailed guide before merging. The same pattern appears in `CHANGELOG.md:27` and `packages/engine/CHANGELOG.md:21`, among other new entries.
**Context Used:** AGENTS.md ([source](https://github.com/agentworkforce/relaycast/blob/main/AGENTS.md))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| The container image pins a published `@relaycast/engine`; until a release that | ||
| includes collector support is pinned there, setting | ||
| `RELAYCAST_TELEMETRY_COLLECTOR_URL` stops startup with exit code `64` and | ||
| `unsupported_telemetry_collector` rather than silently ignoring the setting. |
There was a problem hiding this comment.
Image support is described incorrectly
This paragraph says collector support requires updating the pinned npm release, but Dockerfile builds the checked-out engine and copies its dist over the installed package. An image built from this branch therefore includes collector support without changing that pin. Operators may wait for an unnecessary update or expect a startup refusal that will not happen. Describe support in terms of the selected image, and reserve unsupported_telemetry_collector for an engine that actually lacks the export.
| The container image pins a published `@relaycast/engine`; until a release that | |
| includes collector support is pinned there, setting | |
| `RELAYCAST_TELEMETRY_COLLECTOR_URL` stops startup with exit code `64` and | |
| `unsupported_telemetry_collector` rather than silently ignoring the setting. | |
| The container image builds the engine from its checkout and replaces the | |
| installed package's `dist`, so an image built from this source includes collector | |
| support without changing the npm pin. Select an image that includes this support. | |
| If its bundled engine lacks the collector export, setting | |
| `RELAYCAST_TELEMETRY_COLLECTOR_URL` stops startup with exit code `64` and | |
| `unsupported_telemetry_collector` rather than silently ignoring the setting. |
Knowledge Base Used: Container deployment
Prompt To Fix With AI
This is a comment left during a code review.
Path: docs/self-hosting.md
Line: 161-164
Comment:
**Image support is described incorrectly**
This paragraph says collector support requires updating the pinned npm release, but `Dockerfile` builds the checked-out engine and copies its `dist` over the installed package. An image built from this branch therefore includes collector support without changing that pin. Operators may wait for an unnecessary update or expect a startup refusal that will not happen. Describe support in terms of the selected image, and reserve `unsupported_telemetry_collector` for an engine that actually lacks the export.
```suggestion
The container image builds the engine from its checkout and replaces the
installed package's `dist`, so an image built from this source includes collector
support without changing the npm pin. Select an image that includes this support.
If its bundled engine lacks the collector export, setting
`RELAYCAST_TELEMETRY_COLLECTOR_URL` stops startup with exit code `64` and
`unsupported_telemetry_collector` rather than silently ignoring the setting.
```
**Knowledge Base Used:** [Container deployment](https://app.greptile.com/-/switch-tenant?tenant=agent-relay&next=%2Fcustom-context%2Fknowledge-base%2Fagentworkforce%2Frelaycast%2F-%2Fdocs%2Fcontainer-deployment.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Adds a default-off self-host analytics producer for Relaycast. A typed projector and bounded asynchronous batcher send a closed vocabulary to the installation collector. Existing hosted telemetry adapters keep their current configuration.
The reviewed privacy repairs bind native workspace attribution and keep exception classifications closed. The updated head reconciles the branch with current main and wires the same collector lifecycle through the SQLite and PostgreSQL entrypoints. Shutdown settles abandoned batches before reporting completion, and a queue consisting entirely of expired events releases its pump.
Exact head
25eee4e0passed the hosted container, lint, build, engine, and SDK checks. A clean worktree install, the full Node engine suite (2,945 passed), and 47 container entrypoint tests also passed. The built arm64 image delivered an authenticated direct message and exported its typed event to a trusted HTTPS collector. With an untrusted certificate, messaging still worked and the collector received no event. The matching collector vocabulary round-tripped all 60 kinds in 136 producer envelopes.This PR ships source support with the default off. A deployment must select an image that includes the producer and provision a collector registration with a dedicated credential, matching installation ID, and CA.