agentsclimarketplace

Event taxonomy

Skill alexpate/devtool-skills/skills/event-taxonomy

Establish one canonical event taxonomy and versioned schema registry for product analytics — object.verb naming, schema-as-code with CI validation, cross-source normalization, and forward-compatibility rules. Use when the user wants to name analytics events, design a tracking plan, clean up inconsistent event names, or unify analytics with webhook event types. Also use when they mention "instrumentation", "Segment spec", "Amplitude/Mixpanel/PostHog events", "usage metering", or are wiring metered billing to usage data, even if they never say "taxonomy".From its SKILL.md

Install
npx -y skills add alexpate/devtool-skills --skill event-taxonomy

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

9.6 KB, as published. Nobody here has run it

Event Taxonomy

Every developer tool ends up with two event streams, analytics events and outbound webhooks, and most teams let them drift into separate dialects until no query and no invoice can be trusted. The bar is Segment's tracking-plan discipline applied with Stripe's event-type rigor: one registry, one naming grammar, schemas reviewed like code. Renaming events after a year of data exists is a migration; naming them right costs an afternoon.

Before you start

Check for .agents/devtool-context.md and read it if present (the metered unit and the API surface determine which events matter most). If absent, ask:

  1. What are the 5 to 10 actions that matter: signups, API calls, the billable unit?
  2. Where do events land today (PostHog, Amplitude, Segment, a warehouse), and how many sources emit them (backend, web SDK, mobile, CLI)?
  3. Do outbound webhooks exist or are they planned? (If yes, the webhook type field and this taxonomy must be the same list.)

The rules, in order of consequence

Positions below are defaults; deviate only with a reason.

1. One grammar: object.verb, one registry

Event names are object.verb: invoice.paid, user.signed_up, image.resized. Lowercase, dot-separated, snake_case within segments. Verbs are past tense because events are facts about things that happened: image.resized, never image.resize (that's a command) or resizing_image (that's a state).

  • One canonical registry, zero team-local dialects. The moment growth tracks SignUp, backend tracks user.created, and mobile tracks signup_completed, every funnel query becomes archaeology. New events enter through the registry PR or they don't exist.
  • This is the same taxonomy your webhook type field uses. invoice.paid in the analytics warehouse and invoice.paid in the webhook envelope are the same event with the same name. Stripe's event types work exactly this way, and it means your docs, dashboard, and data team all speak one language. Coordinate with webhook-delivery; neither surface gets to fork the list.
  • Objects are your API resources (user, invoice, image, preset), not features or screens. pricing_page.viewed is fine for web analytics; checkout_flow.step_2 is a name you'll regret by the next redesign.

2. Versioned schemas: additive bumps, breaking mints

Every event has a schema, and every schema has a version.

  • Additive change (new optional property, new enum value) → bump the minor version of the existing schema. Consumers that ignore unknown fields (rule 4) don't notice.
  • Breaking change (removed/renamed field, changed type, changed meaning) → bump the schema's major version (image.resized 1-1 → 2-0) and keep honoring the old version through a deprecation window: the event name stays put; the version moves. Never mutate a schema in place under consumers' feet. Historical rows in the warehouse can't be re-emitted, so a mutated schema silently bifurcates every metric at the cutover date. This is Snowplow's self-describing-schema model (iglu:com.acme/image_resized/jsonschema/2-0-0), and it's right.

3. Normalize at ingestion, not in every consumer

Upstream sources report at different granularities: one payment platform reports per-card, another per-batch; the mobile SDK auto-batches, the web SDK doesn't. The wrong answer is letting every dashboard and billing job handle N dialects. Normalize into the canonical event at ingestion, with an explicit, checked-in mapping table per source:

// ingestion/mappings/acme-payments.ts — one file per upstream source
export const mapping: SourceMapping = {
  source: "acme_payments",
  emits: "charge.settled",           // canonical event
  from: "batch_settlement_report",   // upstream shape
  fanout: "one_row_per_charge",      // batch-level in → card-level out
  fields: {
    charge_id: "$.charges[*].ref",
    amount_minor: "$.charges[*].cents",
    settled_at: { path: "$.batch_date", transform: "date_to_iso8601_utc" },
  },
};

Consumers see exactly one shape per event. The mapping files are where per-source weirdness lives: reviewed, versioned, and testable, instead of smeared across every query.

4. Registry as code, enforced in CI

The registry is a directory of schema files in the repo, not a wiki page:

schemas/
  events/
    image.resized/1-1.json      # JSON Schema per event per version
    preset.created/1-0.json
  registry.json                  # the canonical index
  • PRs are the governance. A new event or version is a schema file plus a registry entry, reviewed like any code. This is Segment's tracking-plan discipline with the plan living in git. PostHog and Snowplow teams run the same pattern (Snowplow makes it structural via Iglu).
  • Generate tracking types from the schemas (json-schema-to-typescript or equivalent) so producers physically can't emit a property the schema doesn't know, the same move as Segment's Typewriter.
  • CI validates emitted events. Minimum bar: unit tests that run every track() call site's payload against its schema. Better: a staging-environment validator that samples the live stream and fails loudly on unknown events or schema violations, so drift is caught in days, not at quarter-end.

5. Forward-compatibility rules

Four rules, stated in the registry README and enforced in review:

  1. Consumers ignore unknown fields. Never fail, never warn-loop, on a property you don't recognize; that's what makes additive change safe.
  2. Producers never repurpose a field. If status meant delivery state, it never comes to mean payment state: mint a new field (or a new event version). A field's meaning is append-only.
  3. Enums grow; values never change meaning. Adding "webp" to format is a minor bump. Making "pending" mean something new is a lie to every historical row.
  4. Timestamps are ISO-8601 UTC everywhere (2026-08-10T12:00:00Z). No epochs in some events and locals in others; the first cross-source join will punish you.

6. Property conventions

  • snake_case properties, everywhere, matching the event-name grammar.
  • IDs are strings, even when they look numeric. JavaScript consumers corrupt 64-bit integers, and string IDs let you switch to prefixed IDs (img_8f3a, see api-keys) without a type change.
  • Every event carries the actor and the object id: actor_id (the user or API key that caused it) and <object>_id. Without both, you can't answer "who did this" or join to the resource table, the two queries every event exists to serve.
  • No PII in event properties by default. Emails, names, and IPs live in the user profile keyed by actor_id, not sprayed across the event stream. This is privacy hygiene (deletion requests touch one table, not every event) and it's leverage: a PII-free stream moves between vendors and into the warehouse without a compliance review per destination.

Worked example: an image-resizing API

Registry index (schemas/registry.json):

{
  "events": {
    "image.resized":   { "latest": "1-1", "owner": "api-team" },
    "preset.created":  { "latest": "1-0", "owner": "api-team" },
    "user.signed_up":  { "latest": "1-0", "owner": "growth" }
  }
}

One schema (schemas/events/image.resized/1-1.json):

{
  "$id": "events/image.resized/1-1",
  "type": "object",
  "required": ["image_id", "actor_id", "preset_id", "occurred_at"],
  "additionalProperties": false,
  "properties": {
    "image_id":     { "type": "string" },
    "actor_id":     { "type": "string" },
    "preset_id":    { "type": "string" },
    "output_format": { "enum": ["jpeg", "png", "webp", "avif"] },
    "bytes_out":    { "type": "integer" },
    "occurred_at":  { "type": "string", "format": "date-time" }
  }
}

(1-0 → 1-1 added "avif" to the enum and the optional bytes_out: additive, minor bump. Dropping preset_id would mint 2-0. Note occurred_at is the same field name the webhook envelope uses; one taxonomy, one vocabulary.)

One cross-source mapping: the mobile SDK reports resize batches; ingestion fans out one canonical image.resized per item, so billing (which meters image.resized; see billing-entitlements) and the web SDK's per-image events land in one indistinguishable stream.

Artifacts to produce

  1. The registry: schemas/ directory with registry.json and one JSON Schema per event/version, covering the user's actual top events.
  2. Naming audit: table of current event names → canonical names, with a migration note per rename (alias in the analytics tool vs. dual-emit window).
  3. Per-source mapping files for every upstream granularity mismatch found.
  4. Generated tracking types wired into the producer codebase.
  5. CI check that validates emitted events against the registry.

Related Skills

  • webhook-delivery: the webhook type field draws from this same registry; never let the two lists fork.
  • billing-entitlements: metered billing consumes usage events, and the canonical billable metric is defined here, not in the billing job.
  • devtool-context: create the context file first if it doesn't exist; the metered unit and API resources seed the registry.

Keep looking

Skills are one crate of 626,194. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.