managing-analytics-events
GitHub管理产品分析事件的规范,定义事件目录、命名规则及分类体系。指导开发者如何正确添加、修改或移除前端、服务端及CLI中的分析事件,确保数据合规与可追溯。
Trigger Scenarios
Install
npx skills add NangoHQ/nango --skill managing-analytics-events -g -y
SKILL.md
Frontmatter
{
"name": "managing-analytics-events",
"description": "Use when adding, renaming, changing or removing a product analytics event from the webapp, the server or the CLI - covers the catalogue, the naming rules, properties, where an event fires from, and what a rename means for existing dashboards"
}
Managing Analytics Events
analyticsEventCatalogue in packages/types/lib/analytics/catalogue.ts is the source of truth for every
product analytics event, whether the webapp, the server or the CLI sends it. Each entry records the
event's surface, the insight it serves, when it fires, and its properties as a zod schema. The senders take
their types from those schemas, so an event that isn't in the catalogue doesn't compile. Nothing is
validated at runtime.
The allowed categories, objects and actions, each with its meaning, are the interfaces in
packages/types/lib/analytics/taxonomy.ts. A name built from anything else fails npm run ts-build.
catalogue.unit.test.ts checks that property names are snake_case and that booleans start with is_ or
has_. The other rules below are yours and the reviewer's to check.
No insight, no event. If you can't name the chart or funnel step an event feeds, don't add it. The questions we want answered are on the product insights page. The reasoning behind the rules below is on the taxonomy page.
Workflow
-
Name the insight. Find the question on the product insights page that the event helps answer. In the entry's
insight, say what this event lets you see within that question: "How often accounts open their invoices from Billing & Usage", not the whole Billing & Usage question. Two events with the sameinsightare probably one event. If no question fits, write the sentence anyway, and ask whether a question belongs on the page. -
Look for an existing event first. If a new event would carry the same properties as an existing one, differ only by a label and be charted next to it, add the label as a property instead. CLI commands are one
functions:command_startwithcommand. Usage page changes are onebilling:usage_updatewithchange.Page views come from
$pageview, filtered by path. Add a_viewevent only for UI that isn't its own page, such as a panel, modal or tab, or when the view needs properties a page view can't carry. -
Choose where it fires.
You need Source Counts and current state, such as accounts per plan Our database. Not an event Changes over time and funnel steps, such as a plan change A server event Interaction, such as clicks, views and abandoned flows A web event Never move a web event to the server to get around ad blockers. Analytics is not for monitoring errors: don't send an event just to report an API error. If an insight will compare this event with another, fire both from the same surface.
-
Name it
category:object_action. Lowercase,snake_case, present tense, built only from the values intaxonomy.ts. The surface is a property, never part of the name. A UI element object ends in_button,_link,_tab,_modalor_page.To add a value, add it with its meaning to the interface in
taxonomy.ts. Use the word the product already uses, and check that no existing value means the same thing:integration, neverprovider. -
Define its properties.
object_adjective,snake_case:integration_id,run_duration_ms.- Booleans start with
is_orhas_. Dates end in_dateor_timestamp. - For a change, the old value takes a
previous_prefix and the new value the plain name:previous_planandplan. - Values are mostly strings, numbers or booleans. Arrays and objects are the exception, below.
- A
completeevent always carriesis_success. Whenis_successis false and the failure has an error code, adderror_codewith it, such asresource_capped. Never send the error message. - A
create,updateordeletethat also reports failed attempts carriesis_success. - An HTTP call carries
http_statuswhen the response provides one. A session-scoped event carriesagent_session_id. - Don't list
surfaceoris_production. Every sender addssurface, and the server addsis_productionwhen it knows the environment. - An array or object is allowed only when an insight can't be answered without it and it has a fixed maximum size. Say why in a comment on the property, and send primitive summaries next to it, such as a count or the top value.
-
Add the entry.
'playground:run_complete': { surface: 'web', insight: 'How often Playground runs succeed, and how long they take', fires: 'When a Playground run returns a result or an error', properties: z.object({ function_type: z.string(), integration: z.string(), is_success: z.boolean(), run_state: z.string(), run_duration_ms: z.number() }) },firesis one sentence. The code shows where it fires. An event with no properties usesnone. For a string typed as a union defined elsewhere, such asUsageMetric, usestringOf<UsageMetric>(), notz.custom, so the test still sees a string.The catalogue lives in
@nangohq/types, so it can only import types from there. When a property's type lives in one service, don't move it into@nangohq/typesby default:- If the values exist mainly for tracking, list them with
z.enum([...])in the catalogue, and have the service take the type fromAnalyticsEventProperties<'category:object_action'>['property']. - If nothing depends on the narrow type, use
z.string(). - Move the type into
@nangohq/typesonly when it is small and other services can use it too.
- If the values exist mainly for tracking, list them with
-
Send it.
- Web:
track()frompackages/webapp/src/utils/analytics.tsx. - Server:
productTracking.track()from@nangohq/shared. The request's tracking context adds the account, the logged-in user when the request has a session, andis_productionwhen it knows the environment. Requests without a session, such as secret-key calls and webhooks, send as the account. - CLI: the CLI posts to
/cli/telemetry, and the server relays the event withproductTracking.trackAnonymous(). Released CLIs keep sending what they sent when they shipped, so the endpoint has to keep accepting old bodies.
- Web:
-
Run
npm run ts-buildandnpx vitest run packages/types/lib/analytics.
Personal data
- A person is identified by user id, with
emailandnameas person properties. Nothing else personal. - The account name is the
companygroup'sname, never an event property. - Free text from a user or an agent can contain personal data. Send it only when an insight needs the text.
- Send a low-cardinality value, such as a filter's dimension, rather than the value someone typed.
Changing an event
| Change | What to do |
|---|---|
| Add an optional property | Add it to the entry with .optional() |
| Change what a property holds, or its type | Add a property with a new name. Never retype one |
| Revamp the flow an event belongs to | Version the category, such as onboarding_v2:connection_create |
| Stop tracking something | Delete the entry and its calls. Never reuse the name |
| Rename an event | See the gotcha below |
Gotchas
- Renaming starts the event's history again under the new name. Dashboards, saved insights and alerts that read the old name go quiet after the deploy. Say in the PR description which events were renamed, so whoever owns those dashboards and alerts makes them read both names before the deploy and drops the old name after it.
- The analytics tool's own events keep their names.
$pageview,$mcp_tool_calland other$events stay out of the catalogue, but a property we add to one still follows the property rules. - Most insights count accounts, not people. Aggregate by the
companygroup. CLI events have no group, because the telemetry call carries no key. - Don't divide a web count by a server count. Ad blockers drop web events but never server ones, so a ratio across surfaces, such as clicks per server outcome, is skewed. When an insight compares two steps, send both from the same surface. In a funnel, only the first step may be a web event: blocked people then never enter it, but a blocked web step later on counts them as not converting.
Review Checklist
- The entry names a real insight, and no existing event already answers it
- The name uses only values from
taxonomy.ts, and any new value is the product's own word - Properties follow the naming rules, and
completeevents carryis_success - It fires from the right surface: server for business facts, web for interaction
- No personal data beyond what the insight needs
- For a rename or removal, the PR description names the events, so dashboards and alerts get updated
-
npm run ts-buildand the catalogue test pass
Version History
- c934467 Current 2026-10-19 20:37


