analytics-events

Add product analytics events to track user interactions in the Metabase frontend

16 stars

Best use case

analytics-events is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Add product analytics events to track user interactions in the Metabase frontend

Teams using analytics-events should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/analytics-events/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/data-ai/analytics-events/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/analytics-events/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How analytics-events Compares

Feature / Agentanalytics-eventsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Add product analytics events to track user interactions in the Metabase frontend

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# 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 definitions
- `frontend/src/metabase-types/analytics/schema.ts` - Schema registry
- `frontend/src/metabase/lib/analytics.ts` - Core tracking functions
- Feature-specific `analytics.ts` files - 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.ts` file
- [ ] 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:

```typescript
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`:

```typescript
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:

```typescript
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`):

```typescript
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:

```typescript
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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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)

```typescript
// 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)

```typescript
// Good
DataStudioLibraryCreatedEvent
TablePickerFiltersAppliedEvent
MetabotChatOpenedEvent

// Bad
dataStudioLibraryCreated      // Wrong case
DataStudioLibraryCreated      // Missing "Event" suffix
```

### Tracking Function Names (camelCase with "track" prefix)

```typescript
// 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:

```typescript
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:

```typescript
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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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:

```
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

1. **Identify the user interaction** to track
2. **Decide on event name** (snake_case, descriptive)
3. **Define event type** in `event.ts` using `ValidateEvent`
4. **Add to union type** (create feature union if needed)
5. **Create tracking function** in feature's `analytics.ts`
6. **Import and call** at the interaction point
7. **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** - 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_from` to track where the action happened

Related Skills

analytics-scoping

16
from diegosouzapw/awesome-omni-skill

Define the scope of analytics efforts by identifying relevant metrics, data sources, and analysis approaches. Use when framing pilot analysis questions, selecting KPIs, or aligning data feeds to business objectives and stakeholder needs.

analytics-metrics

16
from diegosouzapw/awesome-omni-skill

Build data visualization and analytics dashboards. Use when creating charts, KPI displays, metrics dashboards, or data visualization components. Triggers on analytics, dashboard, charts, metrics, KPI, data visualization, Recharts.

Analytics Learning

16
from diegosouzapw/awesome-omni-skill

Process YouTube analytics to extract actionable insights

analytics-flow

16
from diegosouzapw/awesome-omni-skill

Analytics and data analysis workflow skill

analytics-design

16
from diegosouzapw/awesome-omni-skill

Design data analysis from purpose clarification to visualization. Use when analyzing data, exploring BigQuery schemas, building queries, or creating Looker Studio reports.

advanced-analytics

16
from diegosouzapw/awesome-omni-skill

Advanced analytics including machine learning, predictive modeling, and big data techniques

simple-analytics-automation

16
from diegosouzapw/awesome-omni-skill

Automate Simple Analytics tasks via Rube MCP (Composio). Always search tools first for current schemas.

rosette-text-analytics-automation

16
from diegosouzapw/awesome-omni-skill

Automate Rosette Text Analytics tasks via Rube MCP (Composio). Always search tools first for current schemas.

Rankscale GEO Analytics

16
from diegosouzapw/awesome-omni-skill

Fetch and interpret Rankscale GEO (Generative Engine Optimization) analytics. Pulls brand visibility score, citation rate, sentiment, and top AI search terms.

performance-analytics

16
from diegosouzapw/awesome-omni-skill

Analyze marketing performance with key metrics, trend analysis, and optimization recommendations. Use when building performance reports, reviewing campaign results, analyzing channel metrics (email, social, paid, SEO), or identifying what's working and what needs improvement.

events-webinars

16
from diegosouzapw/awesome-omni-skill

Use this skill when a VP of Events or field marketing leader needs to plan and execute the full events program — including webinars, technology workshops, solution workshops, global speaking engagements, conferences, and community events — capturing leads, filling the sales calendar post-event, and building market presence with investors, enterprise buyers, and practitioners.

analytics

16
from diegosouzapw/awesome-omni-skill

Activate for marketing analytics, KPI tracking, reporting dashboards, attribution analysis, and performance optimization. Use when analyzing campaign data, creating reports, or measuring marketing ROI.