ANALYTICS.md
Rama main · réplica de lectura del repositorio original.
# Product analytics
Use this guide for product analytics emitted to PostHog from the frontend,
backend, workers, and lifecycle/webhook processors. An event is a durable data
contract: define what it means and who owns it before adding a capture call or
dashboard.
## Design an event
Answer these questions before implementation:
1. **Question:** Which product decision will this event inform? Do not collect
data without a concrete consumer.
2. **Owner:** Which single producer can establish the fact most reliably?
3. **Firing condition:** What exact transition emits it, and can retries or
rerenders emit it twice?
4. **Source of truth:** Is this UI intent, an accepted command, or a durable
domain outcome?
5. **Scope:** Is the subject a person, organization, project, instance, or
another bounded entity?
6. **Dimensions:** Which stable identifiers are needed for breakdowns? Keep
display copy separate from machine-readable keys.
7. **Cardinality and privacy:** Can any property contain secrets, raw errors,
concrete URLs, resource IDs used as breakdowns, or unbounded user input?
8. **Attribution:** Is an organization and time window sufficient, or does the
flow require a correlation ID propagated to its authoritative outcome?
9. **Consumer:** Which insight or dashboard uses the event, and how will its
query distinguish intent from conversion?
## Event ownership
Each event has one owning producer. Do not emit the same event name from the
frontend and backend: doing so mixes browser observations with domain outcomes
and double-counts web activity.
The frontend owns UI exposure, navigation, and intent that no server can
observe, such as a modal being viewed or a CTA being selected.
The application backend owns accepted commands and external sessions it
successfully creates. Emit properties from normalized or resolved service
state, not optional request inputs when they can differ.
The system that commits or reconciles a durable transition owns authoritative
outcomes. Subscription activation, payment, trial conversion, and similar
events normally belong to the license or webhook lifecycle rather than a
browser return page.
Name events after the fact their owner can prove. For example,
`Billing Checkout Session Created` is a backend fact; `Subscription Activated`
is a billing-lifecycle fact.
## Frontend events
Use `@app/lib/analytics` for new frontend product analytics. The event catalog
in `frontend/src/lib/analytics/events.ts` is the contract between application
code and PostHog dashboards: every event has one canonical name and a typed
property shape.
```ts
import { analytics, AnalyticsEvent } from "@app/lib/analytics";
analytics.captureForOrganization(AnalyticsEvent.PaywallViewed, orgId, {
paywallKey,
paywallText,
route,
isEnterpriseFeature
});
```
`captureForOrganization` adds `orgId` and the PostHog organization group. Do
not add those fields at call sites. Analytics initialization remains lazy; do
not instantiate the client at module scope.
To add a frontend event:
1. Add its canonical name to `AnalyticsEvent`.
2. Add its property contract to `OrganizationAnalyticsEventMap` or the
appropriate scope map when one exists.
3. Capture it through the scope-specific analytics method.
4. Update the PostHog insight or dashboard that consumes it.
Legacy frontend telemetry still uses `Telemetry` directly. Migrate it through
the shared API rather than copying that pattern into new code. Preserve
existing identity and feature-flag behavior during migration.
## Backend events
Define backend event names and discriminated property types in
`backend/src/services/telemetry/telemetry-types.ts`. Emit organization-scoped
events with `organizationId` so the telemetry service attaches the PostHog
organization group and refreshes its properties.
Fire an event only after the operation named by the event succeeds. Use the
resolved service result for normalized plans, cadence, provider type, and
other dimensions. When delivery is best-effort, follow the established
fire-and-forget pattern without changing the API response or error behavior.
## Properties and naming
Event names describe completed facts in title case. Use stable
machine-readable values for plans, products, capabilities, and other
breakdowns; do not use display labels as identifiers.
Properties must have bounded cardinality and must not contain secrets, raw
errors, arbitrary URLs, or unbounded user-entered content. Define lifecycle
terms such as viewed, clicked, created, succeeded, failed, and canceled so they
cannot overlap accidentally.
The one exception is the optional `comment` on
`Billing Cancellation Reason Submitted`. It is free text that a user writes
when they cancel, and it exists for churn analysis. It is capped at 500
characters, trimmed, and omitted when empty. Never use it as a breakdown, and
never put secrets, credentials, tokens, secret values, or other sensitive data
in it from code. The field shows a hint that tells users not to enter secrets or
credentials. Do not add free text to any other event without its own documented
exception here.
Every paywall has a required, lowercase dot-separated `paywallKey` owned by
its call site. Keep the key stable when copy or plan names change; use modal
text only as display context. Record the matched route ID, not a concrete URL
containing organization, project, or resource identifiers.
## Review an analytics change
Before opening or approving a pull request:
- Check the frontend and backend catalogs for a semantic duplicate.
- Trace every property to the executed state, not only the request payload.
- Verify shared-component callers supply stable identity at compile time.
- Check module-scope code and constructors for initialization side effects.
- Confirm retries, rerenders, and webhook redelivery cannot inflate counts.
- Run the saved PostHog queries and verify their breakdown fields match the
typed event contract.
- Keep pull request and ticket references out of durable repository guidance.