| name | product-analytics |
| description | Design a mobile product-analytics event taxonomy with consistent naming, governance, and a registry. Use when introducing new events or cleaning up an existing Amplitude / Mixpanel / Segment / Firebase Analytics setup. |
Product Analytics Taxonomy
Instructions
Product analytics only pays off if events are consistently named, typed, and owned. Treat the event taxonomy as a schema: reviewed, versioned, and enforced.
1. Event Naming Convention
<object>_<action> in snake_case, past tense.
- Good:
checkout_started, payment_submitted, paywall_viewed, onboarding_step_completed.
- Bad:
Click Checkout Button, paymentFlow, clicked_button_42.
Rules:
- Objects come from a fixed domain list (e.g.
checkout, paywall, onboarding, feed, profile).
- Actions come from a fixed verb list (
viewed, started, completed, submitted, failed, dismissed, tapped).
- No event for "user did something general" -- always tie to an object.
2. Standard Properties
Every event carries a standard envelope:
| Property | Type | Notes |
|---|
event_id | uuid | generated client-side, for idempotency |
event_time | iso-ts | wall clock; server-side also stamps ingest time |
app_version | string | 4.12.0+4120 |
platform | enum | ios / android / flutter-ios / ... |
os_version | string | major.minor only |
device_class | enum | low / mid / high |
locale | string | en-US |
session_id | string | rotated on sign-out / new cold start |
user_id_hash | string | optional, only after auth |
experiment_buckets | map | { "paywall_v2": "variant_a" } |
consent_state | enum | granted / denied / not_requested |
Analytics SDKs (Amplitude, Mixpanel, Segment, Firebase Analytics) provide hooks to set these as user / super properties.
3. Event-Specific Properties
- Primitives only (string, number, boolean). No nested objects.
- Enums must be documented; a new enum value requires a registry update.
*_id fields must be hashed if they can identify a person; raw ids only for non-PII objects (product id, article id).
- Include
source (home, search, deeplink) so funnels can differentiate entry points.
Example:
{
"event": "checkout_started",
"properties": {
"cart_value_cents": 2599,
"currency": "USD",
"item_count": 3,
"source": "cart",
"shipping_method": "standard"
}
}
4. Kotlin Wrapper
object Analytics {
fun track(event: AnalyticsEvent) {
if (Consent.current == Consent.Denied) return
val envelope = StandardEnvelope.current()
mixpanel.track(event.name, JSONObject(envelope + event.properties + redactedEnum(event)))
}
}
sealed class AnalyticsEvent(val name: String, val properties: Map<String, Any?>) {
class CheckoutStarted(cartValueCents: Int, currency: String, itemCount: Int, source: Source)
: AnalyticsEvent("checkout_started", mapOf(
"cart_value_cents" to cartValueCents,
"currency" to currency,
"item_count" to itemCount,
"source" to source.wire
))
}
Sealed classes prevent typos; the registry equals the set of subclasses.
5. Swift Wrapper
enum AnalyticsEvent {
case checkoutStarted(cartValueCents: Int, currency: String, itemCount: Int, source: Source)
var name: String {
switch self {
case .checkoutStarted: return "checkout_started"
}
}
var properties: [String: Any] {
switch self {
case let .checkoutStarted(v, c, n, s):
return ["cart_value_cents": v, "currency": c, "item_count": n, "source": s.rawValue]
}
}
}
6. Dart Wrapper (Flutter)
sealed class AnalyticsEvent {
String get name;
Map<String, Object?> get properties;
}
class CheckoutStarted extends AnalyticsEvent {
CheckoutStarted({required this.cartValueCents, required this.currency,
required this.itemCount, required this.source});
final int cartValueCents;
final String currency;
final int itemCount;
final Source source;
@override String get name => 'checkout_started';
@override Map<String, Object?> get properties => {
'cart_value_cents': cartValueCents,
'currency': currency,
'item_count': itemCount,
'source': source.wire,
};
}
7. TypeScript Wrapper (RN)
export type AnalyticsEvent =
| { name: 'checkout_started'; properties: { cart_value_cents: number; currency: string; item_count: number; source: Source } }
| { name: 'payment_submitted'; properties: { method: PaymentMethod; outcome: Outcome } };
export function track(event: AnalyticsEvent): void {
if (consent.current === 'denied') return;
analytics.track(event.name, { ...standardEnvelope(), ...event.properties });
}
8. Governance
- Single source of truth: a
analytics-registry.yaml in the repo lists every event, every property, allowed enum values, owner team, and PII status.
- Pull request gate: CI check fails if a new event lacks a registry entry or a tracking plan link.
- Retirement policy: events deprecated must stay defined in the registry for one release cycle, then be removed from both client and data warehouse views.
- Review: data / product owns the taxonomy; engineering owns the implementation.
9. Anti-Patterns
button_clicked with a button_name property. Create separate events; it's five events, not one.
- Logging raw API payloads as properties. Always extract the specific fields you need.
- Emitting
user_signed_in multiple times per session (guard with session state).
- Setting user id before consent is granted.
10. Validation
- Unit test each wrapper: event names match the registry, enum values are exhaustive.
- A lint script parses
analytics-registry.yaml and the source, failing if a wrapper emits an unregistered property.
- Schema validation on ingest (see
event-schemas).
Checklist