analytics-events
Adds Snowplow tracking events to the Metabase frontend.
Install
mkdir -p .claude/skills/analytics-events && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/3422" && unzip -o skill.zip -d .claude/skills/analytics-events && rm skill.zipInstalls to .claude/skills/analytics-events
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Add product analytics events to track user interactions in the Metabase frontendKey capabilities
- →Registers new Snowplow event schemas
- →Injects tracking calls into existing interaction logic
- →Validates event data against defined TypeScript union types
- →Wraps feature-specific logic with standard tracking functions
How it works
It guides the creation of TypeScript types and registers tracking wrappers by editing specific Metabase frontend files.
Inputs & outputs
When to use analytics-events
- →Define a new user interaction event in TypeScript
- →Add tracking calls to a specific frontend feature
- →Update Snowplow event schemas for existing features
About this skill
Frontend Analytics Events Skill
This skill helps you add product analytics (Snowplow) events to track user interactions in the Metabase frontend codebase.
Quick Reference
Analytics events in Metabase use Snowplow with typed event schemas. Simple events are declared where they are used — trackSimpleEvent is generic and validates the payload at the call site.
Key Files:
frontend/src/metabase/analytics/event.ts- Core tracking functions,trackSimpleEvent/trackSchemaEvent(import frommetabase/analytics)frontend/src/metabase-types/analytics/event.ts- The sharedSimpleEventSchemaonly. Do not add event types here (see below)frontend/src/metabase-types/analytics/schema.ts- Schema registry (custom/legacy schemas only)- Feature-specific
analytics.tsfiles - Where your tracking functions and any local types live
Quick Checklist
When adding a new analytics event:
- Pick an event name (snake_case, past tense)
- Add a tracking function to the feature's
analytics.tsfile, callingtrackSimpleEvent() - Keep any field unions (e.g.
"success" | "failure") as local types in that same file - Import and call the tracking function at the interaction point
- Do not add an event type to
metabase-types/analytics/event.tsor to any union
Event Schema Types
1. Simple Events (Most Common)
Use SimpleEventSchema for straightforward tracking. It supports these standard fields:
type SimpleEventSchema = {
event: string; // Required: Event name (snake_case)
target_id?: number | null; // Optional: ID of affected entity
triggered_from?: string | null; // Optional: UI location/context
duration_ms?: number | null; // Optional: Duration in milliseconds
result?: string | null; // Optional: Outcome (e.g., "success", "failure")
event_detail?: string | null; // Optional: Additional detail/variant
};
When to use: 90% of events fit this schema. Use for clicks, opens, closes, creates, deletes, etc.
trackSimpleEvent is generic and enforces this schema on the object literal you pass it:
// frontend/src/metabase/analytics/event.ts
export function trackSimpleEvent<
T extends SimpleEventSchema &
Record<Exclude<keyof T, keyof SimpleEventSchema>, never>,
>(event: T) {
trackSchemaEvent("simple_event", event);
}
That means a missing event or any field outside SimpleEventSchema is a compile error at the call
site. There is no separate event type to declare and no satisfies clause to add — the old
ValidateEvent<...> helper is no longer exported and is not part of the workflow.
trackSchemaEvent is generic too: it correlates the schema name with the payload type, so you can't
send a dashboard event under the simple_event schema.
2. Custom Schemas (legacy, no events are being added)
Consider adding new event schema only in very special cases.
Examples: DashboardEventSchema, CleanupEventSchema, QuestionEventSchema
Step-by-Step: Adding a Simple Event
Example: Track when a user applies filters in a table picker
Step 1: Create Tracking Functions
In your feature's analytics.ts file (e.g., enterprise/frontend/src/metabase-enterprise/data-studio/analytics.ts):
import { trackSimpleEvent } from "metabase/analytics";
export const trackDataStudioTablePickerFiltersApplied = () => {
trackSimpleEvent({
event: "data_studio_table_picker_filters_applied",
});
};
export const trackDataStudioTablePickerFiltersCleared = () => {
trackSimpleEvent({
event: "data_studio_table_picker_filters_cleared",
});
};
Step 2: Use in Components
Import and call the tracking function at the interaction point:
import {
trackDataStudioTablePickerFiltersApplied,
trackDataStudioTablePickerFiltersCleared,
} from "metabase-enterprise/data-studio/analytics";
function FilterPopover({ filters, onSubmit }) {
const handleReset = () => {
trackDataStudioTablePickerFiltersCleared(); // <- Track here
onSubmit(emptyFilters);
};
return (
<form
onSubmit={(event) => {
event.preventDefault();
trackDataStudioTablePickerFiltersApplied(); // <- Track here
onSubmit(form);
}}
>
{/* form content */}
</form>
);
}
Using SimpleEventSchema Fields
All examples below live in the feature's own analytics.ts — nothing is registered centrally.
Example: Event with target_id
export const trackDataStudioLibraryCreated = (id: CollectionId) => {
trackSimpleEvent({
event: "data_studio_library_created",
target_id: Number(id),
});
};
// Usage
trackDataStudioLibraryCreated(newLibrary.id);
Example: Event with triggered_from
// Local union, exported only if another feature needs to pass the same value
export type NewButtonLocation = "app-bar" | "empty-collection";
export const trackNewButtonClicked = (location: NewButtonLocation) => {
trackSimpleEvent({
event: "new_button_clicked",
triggered_from: location,
});
};
// Usage
<Button onClick={() => {
trackNewButtonClicked("app-bar");
handleCreate();
}}>
New
</Button>
Example: Event with event_detail
Real example — frontend/src/metabase/metadata/pages/shared/analytics.ts:
export type MetadataEditEventDetail =
| "type_casting"
| "semantic_type_change"
| "visibility_change";
export const trackMetadataChange = (detail: MetadataEditEventDetail) => {
trackSimpleEvent({
event: "metadata_edited",
event_detail: detail,
triggered_from: "admin",
});
};
// Usage
trackMetadataChange("semantic_type_change");
Example: Event with result and duration
See frontend/src/metabase/archive/analytics.ts for the real version of this.
export const trackMoveToTrash = (params: {
targetId: number | null;
triggeredFrom: "collection" | "detail_page" | "cleanup_modal";
durationMs: number | null;
result: "success" | "failure";
itemType: "question" | "model" | "metric" | "dashboard";
}) => {
trackSimpleEvent({
event: "moved-to-trash",
target_id: params.targetId,
triggered_from: params.triggeredFrom,
duration_ms: params.durationMs,
result: params.result,
event_detail: params.itemType,
});
};
// Usage with timing
const startTime = Date.now();
try {
await moveToTrash(item);
trackMoveToTrash({
targetId: item.id,
triggeredFrom: "collection",
durationMs: Date.now() - startTime,
result: "success",
itemType: "question",
});
} catch (error) {
trackMoveToTrash({
targetId: item.id,
triggeredFrom: "collection",
durationMs: Date.now() - startTime,
result: "failure",
itemType: "question",
});
}
Naming Conventions
Event Names (snake_case)
// Good
"data_studio_library_created"
"table_picker_filters_applied"
"metabot_chat_opened"
// Bad
"DataStudioLibraryCreated" // Wrong case
"tablePickerFiltersApplied" // Wrong case
"filters-applied" // Use underscore, not hyphen
Local Field Types (PascalCase, named after the field)
There is usually no ...Event type to name anymore. When you do need a union for a field, name it
after the field it feeds:
// Good
type MetricDimensionResult = "success" | "failure"; // -> result
export type MetadataEditEventDetail = "type_casting"; // -> event_detail
type NewButtonLocation = "app-bar" | "empty-collection"; // -> triggered_from
Tracking Function Names (camelCase with "track" prefix)
// Good
trackDataStudioLibraryCreated
trackTablePickerFiltersApplied
trackMetabotChatOpened
// Bad
DataStudioLibraryCreated // Missing "track" prefix
track_library_created // Wrong case
logLibraryCreated // Use "track" prefix
Common Patterns
Pattern 1: Sharing Field Types Across Features
When two features send the same event with a different triggered_from, export the field union from
the owning feature's analytics.ts and import it — don't hoist anything into metabase-types:
// frontend/src/metabase/data-studio/data-model/analytics.ts
import { trackSimpleEvent } from "metabase/analytics";
import type { MetadataEditEventDetail } from "metabase/metadata/pages/shared/analytics";
export function trackMetadataChange(detail: MetadataEditEventDetail) {
trackSimpleEvent({
event: "metadata_edited",
event_detail: detail,
triggered_from: "data_studio",
});
}
This is the point of the extensible-events design: enterprise and feature-tier types stay in their own module instead of being imported down into a shared union.
Pattern 2: Conditional Tracking
Track different events based on user action:
const handleSave = async () => {
if (isNewItem) {
await createItem(data);
trackItemCreated(newItem.id);
} else {
await updateItem(id, data);
trackItemUpdated(id);
}
};
Common Pitfalls
Don't: Add custom fields to a simple event
// WRONG - SimpleEventSchema doesn't support custom fields (this is a compile error)
export const trackFiltersApplied = (filters: FilterState) => {
trackSimpleEvent({
event: "filters_applied",
data_layer: filters.dataLayer, // ❌ Not in SimpleEventSchema
data_source: filters.dataSource, // ❌ Not in SimpleEventSchema
with_owner: filters.hasOwner, // ❌ Not in SimpleEventSchema
});
};
// RIGHT - Use only standard SimpleEventSchema fields
export const trackFiltersApplied = () => {
trackSimpleEvent({
event: "filters_applied",
});
};
// Or use event_detail for a single variant
export const trackFilterApplied = (filterType: string) => {
trackSimpleEvent({
event: "filter_applied",
event_detail: filterType, // ✓ "data_layer", "data_source", etc.
});
};
Don't: Add event types to `metabase-types/analyt
Content truncated.
When not to use it
- →Adding custom event schemas (only legacy use-case)
- →Tracking events that require backend-only data aggregation
Prerequisites
Limitations
- →Restricted to Metabase frontend architecture
- →Requires explicit TypeScript definition for every new event type
How it compares
It enforces type safety for analytics events rather than manually writing arbitrary JSON objects to the tracking layer.
Compared to similar skills
analytics-events side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| analytics-events (this skill) | 1 | 3mo | No flags | Intermediate |
| reviewing-nextjs-16-patterns | 11 | 8mo | Review | Intermediate |
| angular-best-practices | 21 | 3mo | No flags | Advanced |
| react-best-practices | 22 | 3mo | No flags | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by metabase
View all by metabase →You might also like
reviewing-nextjs-16-patterns
djankies
Review code for Next.js 16 compliance - security patterns, caching, breaking changes. Use when reviewing Next.js code, preparing for migration, or auditing for violations.
angular-best-practices
sickn33
Angular performance optimization and best practices guide. Use when writing, reviewing, or refactoring Angular code for optimal performance, bundle size, and rendering efficiency.
react-best-practices
redpanda-data
Client-side React performance optimization patterns.
rerender-memo
TheOrcDev
Extract expensive work into memoized components with React.memo. Apply when components perform expensive computations that can be skipped when props haven't changed.
codex-code-review
tyrchen
Perform comprehensive code reviews using OpenAI Codex CLI. This skill should be used when users request code reviews, want to analyze diffs/PRs, need security audits, performance analysis, or want automated code quality feedback. Supports reviewing staged changes, specific files, entire directories, or git diffs.
react-component-performance
Dimillian
Analyze and optimize React component performance issues (slow renders, re-render thrash, laggy lists, expensive computations). Use when asked to profile or improve a React component, reduce re-renders, or speed up UI updates in React apps.