Skip to main content

Adding a new product event

This guide is the canonical workflow for adding a new event to the product-events channel. The channel is gated by a small set of files in source control; this page lists every gate, the order in which they must be touched, and the questions a reviewer should ask before approving the PR.

The workflow is a single PR that touches the backend, the client, and these docs together. Splitting the changes across PRs is allowed if the backend allow-list lands first (otherwise client traffic would be rejected) and the docs follow as soon as possible — but the same-PR default is strongly preferred so the registry never drifts.

Naming convention

Event names follow a dotted-segment pattern:

<domain>.<entity>.<verb-past-tense>

The existing seed events are the reference shape:

  • project.transfer.requested
  • project.transfer.confirmed

The domain is broad (project, user, billing); the entity is the thing the event is about (transfer, invite, subscription); the verb is past-tense and describes what happened. Use lower-case ASCII letters and underscores within a segment; never spaces, never camelCase. Names that don't fit the pattern will be flagged at review.

Backend limits enforced

Numerical limits are enforced by the backend ingest endpoint. The backend spec is the source of truth; they are repeated here so you don't have to open another file while sizing your payload.

LimitValueNotes
Max events per request50Whole request rejected on overflow.
Max request body size256 KiBWhole request rejected on overflow.
Max properties per event16 KiB serializedThat event rejected; the rest of the batch is still processed.
Per-actor rate limit60 requests / 5-minute rolling windowOver-limit requests get a rate-limit error and persist nothing.
Per-source-IP rate limit60 requests / 5-minute rolling windowSame as above, independent of authentication.
Default retention window365 daysOps-tunable. After this window, rows are anonymized in place.
User-deletion SLO24 hoursA user's rows are anonymized within 24h of auth.users.deletion.

If your event needs to carry more than 16 KiB of properties per emission, the right move is usually to split it into multiple events or to drop the redundant fields, not to ask for a higher cap.

Client emission

The client emits events via the public primitive recordProductEvent exposed by gnostikon-client/src/services/telemetry/record.ts. Do not introduce a private helper — the primitive already handles authentication checks, debounced flushing, and the offline outbox.

A minimal call, modelled on the project-transfer seed:

import { recordProductEvent } from "@/services/telemetry";

recordProductEvent({
event: "project.transfer.requested",
properties: {
project_id: payload.parent_id,
destination_kind: payload.destination_kind,
destination_id: payload.destination_id,
},
});

The call is fire-and-forget; failures are logged in development and silently dropped in production rather than thrown. If the caller does not have an authenticated session, the call is dropped with a development-mode warning.

PR checklist

Copy this block into the description of the PR that adds the event:

- [ ] Allow-list entry: added the event name to
`gnostikon/src/modules/product_events/application/allow_list.py`
`ALLOWED_EVENT_NAMES`.
- [ ] PII paths (if applicable): added any property dotted paths that
carry identifier-like data to
`gnostikon/src/modules/product_events/application/pii_paths.py`
`PII_PROPERTY_PATHS`.
- [ ] Registry entry: added a complete `###`-heading entry on
/docs/product-events/registry (description, owner, added date,
emitted-from link, property table with PII annotations).
- [ ] PII policy (if PII paths changed): cross-referenced the new path
on /docs/product-events/pii-policy.
- [ ] Client emission: emitted the event via the public primitive
`recordProductEvent({ event, properties })` from
`gnostikon-client/src/services/telemetry/`. Did not introduce a
private helper.
- [ ] Staging verification: observed the event reaching the configured
destination listed on /docs/product-events/dashboards.

The order matters: allow-list precedes registry (registry mirrors allow-list); PII paths precede PII policy cross-reference; client emission precedes staging verification.

Reviewing an event PR

If you are the reviewer, walk this list before approving:

  • Is the event name on the backend allow-list (allow_list.py)?
  • If the event carries PII-bearing properties, are those property paths on the backend PII paths list?
  • Does the registry have an entry matching the new allow-list name, with every property annotated (type, description, PII flag)?
  • Does the property table mark every PII path with the "Hashed at ingest" link to the PII policy?
  • Does the emission use recordProductEvent, not a private helper?
  • Has the contributor observed the event in staging? Where on Dashboards of reference did they see it?

If any of those answers is "no", the PR is not ready.

Adjacent topics

Deprecating an event (future)

There is no published deprecation workflow yet. When an event is first deprecated, this section will be expanded to describe the rename / deletion / sunset sequence and how the registry surfaces deprecation status.