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. All events must be defined in TypeScript types before use.
Key Files:
frontend/src/metabase-types/analytics/event.ts- Event type definitionsfrontend/src/metabase-types/analytics/schema.ts- Schema registryfrontend/src/metabase/lib/analytics.ts- Core tracking functions- Feature-specific
analytics.tsfiles - Tracking function wrappers
Quick Checklist
When adding a new analytics event:
- Define event type in
frontend/src/metabase-types/analytics/event.ts - Add event to appropriate union type (e.g.,
DataStudioEvent,SimpleEvent) - Create tracking function in feature's
analytics.tsfile - Import and call tracking function at the interaction point
- Use
trackSimpleEvent()for basic events (most common)
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.
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: Define Event Types
Add event type definitions to frontend/src/metabase-types/analytics/event.ts:
export type DataStudioTablePickerFiltersAppliedEvent = ValidateEvent<{ event: "data_studio_table_picker_filters_applied"; }>; export type DataStudioTablePickerFiltersClearedEvent = ValidateEvent<{ event: "data_studio_table_picker_filters_cleared"; }>;
Step 2: Add to Union Type
Find or create the appropriate union type and add your events:
export type DataStudioEvent = | DataStudioLibraryCreatedEvent | DataStudioTablePublishedEvent | DataStudioGlossaryCreatedEvent | DataStudioGlossaryEditedEvent | DataStudioGlossaryDeletedEvent | DataStudioTablePickerFiltersAppliedEvent // <- Add here | DataStudioTablePickerFiltersClearedEvent; // <- Add here
Step 3: 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/lib/analytics"; export const trackDataStudioTablePickerFiltersApplied = () => { trackSimpleEvent({ event: "data_studio_table_picker_filters_applied", }); }; export const trackDataStudioTablePickerFiltersCleared = () => { trackSimpleEvent({ event: "data_studio_table_picker_filters_cleared", }); };
Step 4: 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
Example: Event with target_id
// Type definition export type DataStudioLibraryCreatedEvent = ValidateEvent<{ event: "data_studio_library_created"; target_id: number | null; }>; // Tracking function export const trackDataStudioLibraryCreated = (id: CollectionId) => { trackSimpleEvent({ event: "data_studio_library_created", target_id: Number(id), }); }; // Usage trackDataStudioLibraryCreated(newLibrary.id);
Example: Event with triggered_from
// Type definition export type NewButtonClickedEvent = ValidateEvent<{ event: "new_button_clicked"; triggered_from: "app-bar" | "empty-collection"; }>; // Tracking function export const trackNewButtonClicked = (location: "app-bar" | "empty-collection") => { trackSimpleEvent({ event: "new_button_clicked", triggered_from: location, }); }; // Usage <Button onClick={() => { trackNewButtonClicked("app-bar"); handleCreate(); }}> New </Button>
Example: Event with event_detail
// Type definition export type MetadataEditEvent = ValidateEvent<{ event: "metadata_edited"; event_detail: "type_casting" | "semantic_type_change" | "visibility_change"; triggered_from: "admin" | "data_studio"; }>; // Tracking function export const trackMetadataChange = ( detail: "type_casting" | "semantic_type_change" | "visibility_change", location: "admin" | "data_studio" ) => { trackSimpleEvent({ event: "metadata_edited", event_detail: detail, triggered_from: location, }); }; // Usage trackMetadataChange("semantic_type_change", "data_studio");
Example: Event with result and duration
// Type definition export type MoveToTrashEvent = ValidateEvent<{ event: "moved-to-trash"; target_id: number | null; triggered_from: "collection" | "detail_page" | "cleanup_modal"; duration_ms: number | null; result: "success" | "failure"; event_detail: "question" | "model" | "metric" | "dashboard"; }>; // Tracking function 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
Event Type Names (PascalCase with "Event" suffix)
// Good DataStudioLibraryCreatedEvent TablePickerFiltersAppliedEvent MetabotChatOpenedEvent // Bad dataStudioLibraryCreated // Wrong case DataStudioLibraryCreated // Missing "Event" suffix
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: Feature-Specific Union Types
Group related events together:
export type DataStudioEvent = | DataStudioLibraryCreatedEvent | DataStudioTablePublishedEvent | DataStudioGlossaryCreatedEvent; export type MetabotEvent = | MetabotChatOpenedEvent | MetabotRequestSentEvent | MetabotFixQueryClickedEvent; // Then add to SimpleEvent union export type SimpleEvent = | /* other events */ | DataStudioEvent | MetabotEvent | /* more events */;
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 SimpleEvent
// WRONG - SimpleEvent doesn't support custom fields 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: Forget to add event to union type
// Define the event export type NewFeatureClickedEvent = ValidateEvent<{ event: "new_feature_clicked"; }>; // β WRONG - Forgot to add to SimpleEvent union // Event won't be recognized by TypeScript // β RIGHT - Add to appropriate union export type SimpleEvent = | /* other events */ | NewFeatureClickedEvent;
Don't: Mix up event name formats
// WRONG event: "dataStudioLibraryCreated" // camelCase event: "data-studio-library-created" // kebab-case event: "Data_Studio_Library_Created" // Mixed case // RIGHT event: "data_studio_library_created" // snake_case
Don't: Track PII or sensitive data
// WRONG - Don't track user emails, names, or sensitive data trackSimpleEvent({ event: "user_logged_in", event_detail: user.email, // β PII }); // RIGHT - Track non-sensitive identifiers only trackSimpleEvent({ event: "user_logged_in", target_id: user.id, // β Just the ID });
Don't: Forget to track both success and failure
// WRONG - Only tracking success try { await saveData(); trackDataSaved(); } catch (error) { // β No tracking for failure case } // RIGHT - Track both outcomes try { await saveData(); trackDataSaved({ result: "success" }); } catch (error) { trackDataSaved({ result: "failure" }); }
Testing Analytics Events
While developing, you can verify events are firing:
- Check browser console - When
SNOWPLOW_ENABLED=truein dev, events are logged - Use shouldLogAnalytics - Set in
metabase/envto see all analytics in console - Check Snowplow debugger - Browser extension for Snowplow events
Example console output:
[SNOWPLOW EVENT | event sent:true], data_studio_table_picker_filters_applied
File Organization
Where to put tracking functions:
Feature-specific analytics functions:
frontend/src/metabase/{feature}/analytics.ts
enterprise/frontend/src/metabase-enterprise/{feature}/analytics.ts
Event type definitions (all in one place):
frontend/src/metabase-types/analytics/event.ts
Core tracking utilities:
frontend/src/metabase/lib/analytics.ts
Real-World Examples
See these files for reference:
- Simple events:
enterprise/frontend/src/metabase-enterprise/data-studio/analytics.ts - Events with variants:
frontend/src/metabase/dashboard/analytics.ts - Complex events:
frontend/src/metabase/query_builder/analytics.js - Event type examples:
frontend/src/metabase-types/analytics/event.ts
Workflow Summary
- Identify the user interaction to track
- Decide on event name (snake_case, descriptive)
- Define event type in
event.tsusingValidateEvent - Add to union type (create feature union if needed)
- Create tracking function in feature's
analytics.ts - Import and call at the interaction point
- Test that events fire correctly
Tips
- Be specific -
filters_appliedis better thanaction_performed - Use past tense -
library_creatednotcreate_library - Group related events - Create feature-specific event union types
- Track meaningful actions - Not every click needs tracking
- Consider the data - What would you want to analyze later?
- Stay consistent - Follow existing naming patterns in the codebase
- Document context - Use
triggered_fromto track where the action happened