## 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>
Telemetry Data Contract
This document explains how contributors should use Paperclip's public telemetry contract. It intentionally does not list individual events or dimensions.
The canonical source for first-party event names, dimensions, optionality,
allowed primitive value types, and enum descriptions is
packages/shared/src/telemetry/generated/paperclip-telemetry.ts.
Shared enum constants live in packages/shared/src/constants.ts. Use those
constants when code needs a reusable domain, but treat the generated telemetry
types as the final authority for emitted first-party telemetry shapes.
Public Sources
Use these files when reviewing or changing telemetry code:
| Contract item | Public source |
|---|---|
| First-party event names | PaperclipEventName in generated/paperclip-telemetry.ts |
| Per-event dimensions and optionality | EventDimensionsMap in generated/paperclip-telemetry.ts |
| Enum descriptions for telemetry dimensions | PAPERCLIP_ENUM_DESCRIPTIONS in generated/paperclip-telemetry.ts |
| Schema version and event envelope helpers | SCHEMA_VERSION, makeEvent(), and makeBatch() in generated/paperclip-telemetry.ts |
| Runtime-safe event names and dimensions | TelemetryEventName and TelemetryEventDimensions in types.ts |
| Allowed primitive dimension values | TelemetryDimensionValue in types.ts |
| 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.
Emission Boundary
Paperclip telemetry uses named events with explicit dimension fields. Treat open-ended string dimensions as public contract values, not as a place for user content or private operational data. Do not send PII, secrets, credentials, private paths, prompts, model output, or other sensitive values through telemetry dimensions.
Telemetry emitters send raw dimension values. They must not pre-normalize enum-like values into a reporting form just to match today's known domain.
The receiving layer owns canonicalization. Keeping canonicalization in one place means emitters can stay simple and accurate: emit what the product observed, use the generated contract for required and optional fields, and let the receiving layer decide how legacy spellings, aliases, unknown names, and future values map to a stable reporting shape.
Do not add client-side lowercasing, alias mapping, or fallback mapping unless the generated telemetry contract specifically requires that emitted value.
If a dimension is privacy-protected before emission, emit only the protected value and its matching public marker as defined by the typed helper or generated contract. Do not emit private source material in telemetry dimensions.
Dimension Values
Telemetry dimension values must be primitives. Use only the value types allowed
by TelemetryDimensionValue:
stringnumberboolean
Do not emit null, undefined, arrays, or objects as dimension values. Optional
dimensions should be omitted when absent.
When a dimension is enum-like, use the shared constant from constants.ts when
one exists. If no shared constant exists, use the generated telemetry type as the
domain. In all cases, the generated telemetry type remains the source of truth
for the emitted value.
Required, Optional, And Sentinel Values
Required and optional dimensions are defined by EventDimensionsMap.
Required dimensions must be present for every event of that name. Optional dimensions should be emitted only when the value is known and useful.
Sentinel values are only for required fields that have no observed raw value at the emitting layer. Do not use a sentinel to hide a concrete value that is new, custom, or not yet represented by a shared constant. Emit the concrete raw value and let the receiving layer canonicalize it.
Adding Or Changing Telemetry
Client code is responsible for emitting approved telemetry events at the right place in the product. It is not responsible for deciding which new events should exist. Do not introduce ad hoc event names, dimensions, or enum domains in client code; they must exist in the generated telemetry contract before emitters use them.
- Start from
generated/paperclip-telemetry.ts. The generated types are what reviewers use to verify event names, dimensions, optionality, value types, enum descriptions, and schema version. - Choose stable event and dimension names. Do not include user content, local machine details, secrets, credentials, private paths, or values that are not part of the public event contract.
- Use only
string,number, orbooleandimension values. - Reuse a shared constant from
constants.tsfor enum-like dimensions when one exists. If the generated telemetry domain has values beyond a shared constant, keep the emitter aligned with the generated telemetry type. - Keep emitters raw. Do not normalize, alias-map, or lowercase enum-like values in the client unless the generated contract explicitly calls for that emitted value.
- Add or update a typed helper in
events.tswhen the event is first-party and should have a stable helper API. - Update tests for helper behavior, including raw pass-through for enum-like values when that is the intended boundary.
- Update this README only when the contributor workflow, source-of-truth pointers, or durable invariants change. Do not add an event catalog here.
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.