From 1b16f9611cb5937dc463ab00cb32bb0c070d78f6 Mon Sep 17 00:00:00 2001 From: Nicky Leach Date: Wed, 8 Jul 2026 21:27:39 -0700 Subject: [PATCH] feat(telemetry): document credential health retention (#9248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The shared telemetry package (`packages/shared/src/telemetry`) defines the public contract for first-party telemetry events — event names, dimension shapes, and now retention windows > - A new telemetry event, `codex.credential_health`, carries credential-observability fields (enums, booleans, counts, coarse buckets) — no token material and no PII > - The retention window for this event and its class was undocumented at the contract level, leaving data-infra and reviewers without a discoverable source of truth > - This pull request adds `retention.ts` as the canonical retention-contract surface, assigns `codex.credential_health` to the `operational_enum_count` class (90-day window), exports the contract from the shared package, and updates the README > - The benefit is that the retention window is discoverable from the event definition rather than being implicit pipeline knowledge, and the no-token/no-PII note is locked in a contract test rather than relying on prose ## Issue Description No public GitHub issue exists for this change. Inline description follows the [feature request template](.github/ISSUE_TEMPLATE/feature_request.yml): ### Problem or motivation `packages/shared/src/telemetry/` applies a 90-day retention window to `codex.credential_health` events at the pipeline level, but this policy is not stated anywhere in the shared telemetry contract. Without a discoverable retention declaration, reviewers and data-infra must read pipeline configuration to understand retention behaviour — there is no contract-level source of truth. The `codex.credential_health` event carries only enums, booleans, counts, and coarse buckets — no token material and no PII. ### Proposed solution Add a `retention.ts` module to the shared telemetry package that defines the `operational_enum_count` retention class (90-day window) and maps `codex.credential_health` to it. Export the contract from the package index and add a focused contract test. Update the README with a Retention section and a pointer in the Public Sources table. This is additive documentation only — no runtime paths change. ### Alternatives considered Keep retention implicit in pipeline configuration only. Rejected: this leaves no discoverable, versioned contract for reviewers or data-infra, and means every consumer must read pipeline config to understand retention semantics. A schema-level declaration is the correct long-term home. ### Roadmap alignment Aligns with the telemetry contract hardening track — making implicit operational knowledge explicit and testable at the shared-package level. ## What Changed - **`packages/shared/src/telemetry/retention.ts`** (new): defines `RETENTION_DAYS` (class → days) and `EVENT_RETENTION_CLASS` (event name → class). `operational_enum_count` is the only class: 90-day window for enum/count/bucket events with no token material or PII. `codex.credential_health` is the first entry. The `string` key type is intentional to accommodate cross-system events (e.g. the Codex CLI) not yet promoted to the first-party `PaperclipEventName` schema. - **`packages/shared/src/telemetry/retention.test.ts`** (new): three focused assertions — `operational_enum_count` is 90 days, `codex.credential_health` is assigned that class, and the resolved window is 90 days. - **`packages/shared/src/telemetry/index.ts`**: exports `RETENTION_DAYS`, `EVENT_RETENTION_CLASS`, and `RetentionClass` from the shared package. - **`packages/shared/src/telemetry/README.md`**: adds `retention.ts` to the Public Sources table and a new Retention section with a class reference table and guidance for future assignments. ## Verification - `git diff --check` passes (no whitespace errors) - `pnpm --filter @paperclipai/shared exec vitest run src/telemetry/retention.test.ts src/telemetry/readme-contract.test.ts` — runs the new contract tests and the existing readme-contract test - `pnpm --filter @paperclipai/shared typecheck` — confirms the new exports compile cleanly - CI gates green ## Risks Low risk. This is a documentation-only addition: - No new telemetry event, dimension, emitter, or schema change - No runtime code paths changed - The new file is tree-shaken away in any consumer that doesn't import from it - `satisfies Record` on `RETENTION_DAYS` ensures the type stays correct as new classes are added ## Model Used Claude Sonnet 4.6 (`claude-sonnet-4-6`) — Anthropic, 200k context window, tool use enabled, no extended thinking mode. Used for code authoring and PR composition. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Harold Kim Co-authored-by: Paperclip Co-authored-by: Harold Kim --- packages/shared/src/telemetry/README.md | 19 ++++++++ packages/shared/src/telemetry/index.ts | 2 + .../shared/src/telemetry/retention.test.ts | 21 ++++++++ packages/shared/src/telemetry/retention.ts | 48 +++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 packages/shared/src/telemetry/retention.test.ts create mode 100644 packages/shared/src/telemetry/retention.ts diff --git a/packages/shared/src/telemetry/README.md b/packages/shared/src/telemetry/README.md index 3a741ecdfc..b81ca4eebc 100644 --- a/packages/shared/src/telemetry/README.md +++ b/packages/shared/src/telemetry/README.md @@ -26,6 +26,7 @@ Use these files when reviewing or changing telemetry code: | Shared reusable enum domains | Named exports in `constants.ts` | | First-party typed emit helpers | `events.ts` | | Generic client behavior | `client.ts` | +| Retention windows and event class assignments | `RETENTION_DAYS` and `EVENT_RETENTION_CLASS` in `retention.ts` | Do not copy generated event lists or dimension tables into this README. They will drift as the generated contract changes. @@ -114,3 +115,21 @@ them. Before opening a pull request, verify that the emitted code, typed helpers, and generated telemetry contract agree. If they disagree, fix the contract or code rather than documenting around the mismatch in this README. + +## Retention + +Retention windows are documented in `retention.ts`. Each event is assigned a +retention class; the class determines the window in days. This is a +housekeeping and query-cost concern managed by data-infra, not a schema +concern — updating a retention window does not require a schema version bump. + +Current classes: + +| Class | Window | Description | +| --- | --- | --- | +| `operational_enum_count` | 90 days | Enum/boolean/count/bucket events. No token material, no PII. | + +When a new event carries only enums, booleans, counts, or coarse buckets and +no token material or PII, assign it to `operational_enum_count` in +`EVENT_RETENTION_CLASS`. If no existing class fits, define a new class in +`RETENTION_DAYS` and document it here. diff --git a/packages/shared/src/telemetry/index.ts b/packages/shared/src/telemetry/index.ts index 16b5d8de5d..494fbbd829 100644 --- a/packages/shared/src/telemetry/index.ts +++ b/packages/shared/src/telemetry/index.ts @@ -32,3 +32,5 @@ export type { EventDimensionsMap, PaperclipEventName, } from "./generated/paperclip-telemetry.js"; +export { EVENT_RETENTION_CLASS, RETENTION_DAYS } from "./retention.js"; +export type { RetentionClass } from "./retention.js"; diff --git a/packages/shared/src/telemetry/retention.test.ts b/packages/shared/src/telemetry/retention.test.ts new file mode 100644 index 0000000000..db02ed011e --- /dev/null +++ b/packages/shared/src/telemetry/retention.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { + EVENT_RETENTION_CLASS, + RETENTION_DAYS, +} from "./retention.js"; + +describe("telemetry retention contract", () => { + it("operational_enum_count has a 90-day retention window", () => { + expect(RETENTION_DAYS.operational_enum_count).toBe(90); + }); + + it("codex.credential_health is assigned the operational_enum_count class", () => { + expect(EVENT_RETENTION_CLASS["codex.credential_health"]).toBe("operational_enum_count"); + }); + + it("codex.credential_health resolves to 90 days", () => { + const cls = EVENT_RETENTION_CLASS["codex.credential_health"]; + expect(cls).toBeDefined(); + expect(RETENTION_DAYS[cls!]).toBe(90); + }); +}); diff --git a/packages/shared/src/telemetry/retention.ts b/packages/shared/src/telemetry/retention.ts new file mode 100644 index 0000000000..264c769e8e --- /dev/null +++ b/packages/shared/src/telemetry/retention.ts @@ -0,0 +1,48 @@ +/** + * Telemetry retention contract. + * + * Defines the retention class for each Paperclip telemetry event and the + * corresponding retention window in days. This is a housekeeping/cost + * concern separate from the event schema: it is updated by data-infra + * and does not require a schema version bump. + * + * Retention class definitions + * ───────────────────────────────────────────────────────────────────────── + * operational_enum_count + * Events that carry only enums, booleans, counts, and coarse buckets. + * No token material (I5-clean) and no PII. Retention is a housekeeping + * and query-cost concern, not a privacy concern. + * Window: 90 days. + */ + +/** Retention window in days for each telemetry event class. */ +export const RETENTION_DAYS = { + /** + * Enum/count/bucket events: no token material, no PII. + * Retention is a housekeeping and query-cost concern only. + */ + operational_enum_count: 90, +} as const satisfies Record; + +/** Identifies which retention class an event belongs to. */ +export type RetentionClass = keyof typeof RETENTION_DAYS; + +/** + * Maps first-party event names to their retention class. + * + * Key type is intentionally `string` rather than a tighter union because this + * contract also covers events emitted by external systems (e.g. the Codex CLI) + * that are not part of the shared generated `PaperclipEventName` type. When an + * event is promoted to the first-party schema, prefer tightening its entry to + * the named union type via an overloaded record. + * + * codex.credential_health + * Emits credential-observability fields: enums (credential source, sync + * outcome), booleans (rotation detected, refresh succeeded), and coarse + * counts (rotations detected). No token material and no PII. + * External-system event (Codex CLI) — not yet in PaperclipEventName. + * Class: operational_enum_count — 90-day window. + */ +export const EVENT_RETENTION_CLASS: Partial> = { + "codex.credential_health": "operational_enum_count", +};