feat(telemetry): document credential health retention (#9248)

## 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<string, number>` 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 <harold.kim@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Harold Kim <harold-kim@paperclip.ing>
This commit is contained in:
Nicky Leach 2026-07-08 21:27:39 -07:00 committed by GitHub
parent eedc7ddef2
commit 1b16f9611c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 90 additions and 0 deletions

View File

@ -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.

View File

@ -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";

View File

@ -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);
});
});

View File

@ -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<string, number>;
/** 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<Record<string, RetentionClass>> = {
"codex.credential_health": "operational_enum_count",
};