New to Claude Skills? Learn how to install them →

metabase on GitHub

Frontend Analytics Events

Free

Track user interactions in Metabase with ease.

Get this skill

Free · Opens the source repo

What Frontend Analytics Events does

The Frontend Analytics Events skill is designed to integrate product analytics into the Metabase frontend, enabling developers to track user interactions effectively. Utilizing Snowplow for analytics, this skill provides a structured approach to event tracking through typed event schemas. By following the guidelines, developers can seamlessly add tracking for various user interactions, such as clicks and form submissions, ensuring that every important action is captured for analysis.

The core functionality revolves around the trackSimpleEvent method, which validates the payload at the call site. This minimizes errors and enforces a consistent structure for event data. Developers simply need to define their events in the relevant feature's analytics.ts file and call the tracking function at the point of interaction, making it straightforward to implement and maintain.

This skill is particularly beneficial for teams working on the Metabase codebase who need to gather insights into user behavior without overcomplicating the analytics setup. It streamlines the process of adding new events, ensuring that developers can focus on building features while still capturing valuable data.

Overall, the Frontend Analytics Events skill is a practical tool for any Metabase developer looking to enhance their application's analytics capabilities without introducing unnecessary complexity.

When to use it

Use this skill when you need to implement user interaction tracking in the Metabase frontend, particularly for new features or changes.

When not to use it

This skill is not suitable for tracking events outside of the Metabase frontend or for applications that do not utilize Snowplow for analytics.

What you can build with it

Tracking Button Clicks

Implement tracking for button clicks in your Metabase features to monitor user engagement.

Capturing Form Submissions

Use the skill to track when users submit forms, helping to analyze user behavior and feature effectiveness.

Monitoring Filter Applications

Track when users apply filters in data tables to understand how they interact with data visualizations.

How to install Frontend Analytics Events

View source

1. Install with the skills CLI

npx skills add metabase/metabase/analytics-events --agent claude-code

2. Or install it manually

Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.

Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs

Inside SKILL.md

Written by metabase

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 usedtrackSimpleEvent is generic and validates the payload at the call site.

Key Files:

  • frontend/src/metabase/analytics/event.ts - Core tracking functions, trackSimpleEvent / trackSchemaEvent (import from metabase/analytics)
  • frontend/src/metabase-types/analytics/event.ts - The shared SimpleEventSchema only. Do not add event types here (see below)
  • frontend/src/metabase-types/analytics/schema.ts - Schema registry (custom/legacy schemas only)
  • Feature-specific analytics.ts files - 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.ts file, calling trackSimpleEvent()
  • 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.ts or 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/analytics/event.ts

The central SimpleEvent union was removed — it forced feature-tier types to be imported down into shared code, causing module-boundary violations. trackSimpleEvent is generic now, so the type adds nothing but duplication.

// ❌ WRONG - central declaration + re-import for a `satisfies` clause
// frontend/src/metabase-types/analytics/event.ts
export type NewFeatureClickedEvent = ValidateEvent<{
  event: "new_feature_clicked";
  target_id: number;
}>;

// frontend/src/metabase/my-feature/analytics.ts
import type { NewFeatureClickedEvent } from "metabase-types/analytics";

export const trackNewFeatureClicked = (id: number) => {
  trackSimpleEvent({
    event: "new_feature_clicked",
    target_id: id,
  } satisfies NewFeatureClickedEvent);
};

// ✓ RIGHT - the object literal is already checked by the generic
// frontend/src/metabase/my-feature/analytics.ts
export const trackNewFeatureClicked = (id: number) => {
  trackSimpleEvent({
    event: "new_feature_clicked",
    target_id: id,
  });
};

A few ...Event types still sit in metabase-types/analytics/event.ts. They are leftovers from PRs that landed around the refactor — don't copy them, and don't add to them.

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:

  1. Check browser console - When SNOWPLOW_ENABLED=true in dev, events are logged
  2. Use shouldLogAnalytics - Set in metabase/env to see all analytics in console
  3. 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:

Tracking functions AND their local field types (this is where new events live):
frontend/src/metabase/{feature}/analytics.ts
enterprise/frontend/src/metabase-enterprise/{feature}/analytics.ts

Core tracking utilities:
frontend/src/metabase/analytics/ (import from `metabase/analytics`)

Shared SimpleEventSchema only — nothing new goes here:
frontend/src/metabase-types/analytics/event.ts

In embedding SDK code, use trackSdkSimpleEvent (frontend/src/embedding-sdk-bundle/analytics/snowplow.ts) instead — the main-app "sp" tracker isn't initialized in the customer's page, so trackSimpleEvent's Snowplow leg is a no-op there.

Real-World Examples

See these files for reference:

  • Simple events + local field union: frontend/src/metabase/metadata/pages/shared/analytics.ts
  • Reusing another feature's field type: frontend/src/metabase/data-studio/data-model/analytics.ts
  • Result + duration timing: frontend/src/metabase/archive/analytics.ts
  • Enterprise feature events: enterprise/frontend/src/metabase-enterprise/google_drive/analytics.ts

Workflow Summary

  1. Identify the user interaction to track
  2. Decide on event name (snake_case, descriptive)
  3. Create tracking function in feature's analytics.ts, calling trackSimpleEvent()
  4. Add local field unions in that same file if a field has a fixed set of values
  5. Import and call at the interaction point
  6. Test that events fire correctly

Tips

  • Be specific - filters_applied is better than action_performed
  • Use past tense - library_created not create_library
  • Group related events - Keep a feature's tracking functions together in its analytics.ts
  • 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_from to track where the action happened

Frequently asked questions about Frontend Analytics Events

Similar skills