Frontend Analytics Events

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

You say
Install this skill Read the source first Free Written by metabase · unverified publisher
Context cost
3.7k tokensestimated from the bundle, loaded when it triggers
Bundle
1 file · 14.9 kBtext throughout, nothing executable
Licence
AGPL-3.0free to use
Last change
no release on file
Servers it uses
Noneruns standalone

What it does

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

Installed, it changes the agent in these ways.

What this skill changes about the agent is not written down here yet. The listing was collected from its source, and the description is in its own SKILL.md.

Guardrail

Constrains what the agent is allowed to do.

analytics

The skill itself

This is the whole product. A skill is instructions the model reads, so there is nothing behind the listing you cannot see first — the front matter loads with every session, and the body below it loads when the skill triggers.

SKILL.md14.9 kB · 494 lines
--- name: analytics-events description: Add product analytics events to track user interactions in the Metabase frontend allowed-tools: Read, Write, Edit, Grep, Glob ---
7# Frontend Analytics Events Skill
8
9This skill helps you add product analytics (Snowplow) events to track user interactions in the Metabase frontend codebase.
10
11## Quick Reference
12
13Analytics 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.
14
15**Key Files:**
16- frontend/src/metabase/analytics/event.ts - Core tracking functions, trackSimpleEvent / trackSchemaEvent (import from metabase/analytics)
17- frontend/src/metabase-types/analytics/event.ts - The shared SimpleEventSchema only. **Do not add event types here** (see below)
18- frontend/src/metabase-types/analytics/schema.ts - Schema registry (custom/legacy schemas only)
19- Feature-specific analytics.ts files - Where your tracking functions and any local types live
20
21## Quick Checklist
22
23When adding a new analytics event:
24
25- [ ] Pick an event name (snake_case, past tense)
26- [ ] Add a tracking function to the feature's analytics.ts file, calling trackSimpleEvent()
27- [ ] Keep any field unions (e.g. "success" | "failure") as local types in that same file
28- [ ] Import and call the tracking function at the interaction point
29- [ ] Do **not** add an event type to metabase-types/analytics/event.ts or to any union
30
31## Event Schema Types
32
33### 1. Simple Events (Most Common)
34
35Use SimpleEventSchema for straightforward tracking. It supports these standard fields:
36
37```typescript
38type SimpleEventSchema = {
39 event: string; // Required: Event name (snake_case)
40 target_id?: number | null; // Optional: ID of affected entity
41 triggered_from?: string | null; // Optional: UI location/context
42 duration_ms?: number | null; // Optional: Duration in milliseconds
43 result?: string | null; // Optional: Outcome (e.g., "success", "failure")
44 event_detail?: string | null; // Optional: Additional detail/variant
45};
46```
47
48**When to use:** 90% of events fit this schema. Use for clicks, opens, closes, creates, deletes, etc.
49
50trackSimpleEvent is generic and enforces this schema on the object literal you pass it:
51
52```typescript
53// frontend/src/metabase/analytics/event.ts
54export function trackSimpleEvent<
55 T extends SimpleEventSchema &
56 Record<Exclude<keyof T, keyof SimpleEventSchema>, never>,
57>(event: T) {
58 trackSchemaEvent("simple_event", event);
59}
60```
61
62That means a missing event or any field outside SimpleEventSchema is a compile error at the call
63site. There is no separate event type to declare and no satisfies clause to add — the old
64ValidateEvent<...> helper is no longer exported and is not part of the workflow.
65
66trackSchemaEvent is generic too: it correlates the schema name with the payload type, so you can't
67send a dashboard event under the simple_event schema.
68
69### 2. Custom Schemas (legacy, no events are being added)
70
71Consider adding new event schema only in very special cases.
72
73**Examples:** DashboardEventSchema, CleanupEventSchema, QuestionEventSchema
74
75## Step-by-Step: Adding a Simple Event
76
77### Example: Track when a user applies filters in a table picker
78
79#### Step 1: Create Tracking Functions
80
81In your feature's analytics.ts file (e.g., enterprise/frontend/src/metabase-enterprise/data-studio/analytics.ts):
82
83```typescript
84import { trackSimpleEvent } from "metabase/analytics";
85
86export const trackDataStudioTablePickerFiltersApplied = () => {
87 trackSimpleEvent({
88 event: "data_studio_table_picker_filters_applied",
89 });
90};
91
92export const trackDataStudioTablePickerFiltersCleared = () => {
93 trackSimpleEvent({
94 event: "data_studio_table_picker_filters_cleared",
95 });
96};
97```
98
99#### Step 2: Use in Components
100
101Import and call the tracking function at the interaction point:
102
103```typescript
104import {
105 trackDataStudioTablePickerFiltersApplied,
106 trackDataStudioTablePickerFiltersCleared,
107} from "metabase-enterprise/data-studio/analytics";
108
109function FilterPopover({ filters, onSubmit }) {
110 const handleReset = () => {
111 trackDataStudioTablePickerFiltersCleared(); // <- Track here
112 onSubmit(emptyFilters);
113 };
114
115 return (
116 <form
117 onSubmit={(event) => {
118 event.preventDefault();
119 trackDataStudioTablePickerFiltersApplied(); // <- Track here
120 onSubmit(form);
121 }}
122 >
123 {/* form content */}
124 </form>
125 );
126}
127```
128
129## Using SimpleEventSchema Fields
130
131All examples below live in the feature's own analytics.ts — nothing is registered centrally.
132
133### Example: Event with target_id
134
135```typescript
136export const trackDataStudioLibraryCreated = (id: CollectionId) => {
137 trackSimpleEvent({
138 event: "data_studio_library_created",
139 target_id: Number(id),
140 });
141};
142
143// Usage
144trackDataStudioLibraryCreated(newLibrary.id);
145```
146
147### Example: Event with triggered_from
148
149```typescript
150// Local union, exported only if another feature needs to pass the same value
151export type NewButtonLocation = "app-bar" | "empty-collection";
152
153export const trackNewButtonClicked = (location: NewButtonLocation) => {
154 trackSimpleEvent({
155 event: "new_button_clicked",
156 triggered_from: location,
157 });
158};
159
160// Usage
161<Button onClick={() => {
162 trackNewButtonClicked("app-bar");
163 handleCreate();
164}}>
165 New
166</Button>
167```
168
169### Example: Event with event_detail
170
171Real example — frontend/src/metabase/metadata/pages/shared/analytics.ts:
172
173```typescript
174export type MetadataEditEventDetail =
175 | "type_casting"
176 | "semantic_type_change"
177 | "visibility_change";
178
179export const trackMetadataChange = (detail: MetadataEditEventDetail) => {
180 trackSimpleEvent({
181 event: "metadata_edited",
182 event_detail: detail,
183 triggered_from: "admin",
184 });
185};
186
187// Usage
188trackMetadataChange("semantic_type_change");
189```
190
191### Example: Event with result and duration
192
193See frontend/src/metabase/archive/analytics.ts for the real version of this.
194
195```typescript
196export const trackMoveToTrash = (params: {
197 targetId: number | null;
198 triggeredFrom: "collection" | "detail_page" | "cleanup_modal";
199 durationMs: number | null;
200 result: "success" | "failure";
201 itemType: "question" | "model" | "metric" | "dashboard";
202}) => {
203 trackSimpleEvent({
204 event: "moved-to-trash",
205 target_id: params.targetId,
206 triggered_from: params.triggeredFrom,
207 duration_ms: params.durationMs,
208 result: params.result,
209 event_detail: params.itemType,
210 });
211};
212
213// Usage with timing
214const startTime = Date.now();
215try {
216 await moveToTrash(item);
217 trackMoveToTrash({
218 targetId: item.id,
219 triggeredFrom: "collection",
220 durationMs: Date.now() - startTime,
221 result: "success",
222 itemType: "question",
223 });
224} catch (error) {
225 trackMoveToTrash({
226 targetId: item.id,
227 triggeredFrom: "collection",
228 durationMs: Date.now() - startTime,
229 result: "failure",
230 itemType: "question",
231 });
232}
233```
234
235## Naming Conventions
236
237### Event Names (snake_case)
238
239```typescript
240// Good
241"data_studio_library_created"
242"table_picker_filters_applied"
243"metabot_chat_opened"
244
245// Bad
246"DataStudioLibraryCreated" // Wrong case
247"tablePickerFiltersApplied" // Wrong case
248"filters-applied" // Use underscore, not hyphen
249```
250
251### Local Field Types (PascalCase, named after the field)
252
253There is usually no ...Event type to name anymore. When you do need a union for a field, name it
254after the field it feeds:
255
256```typescript
257// Good
258type MetricDimensionResult = "success" | "failure"; // -> result
259export type MetadataEditEventDetail = "type_casting"; // -> event_detail
260type NewButtonLocation = "app-bar" | "empty-collection"; // -> triggered_from
261```
262
263### Tracking Function Names (camelCase with "track" prefix)
264
265```typescript
266// Good
267trackDataStudioLibraryCreated
268trackTablePickerFiltersApplied
269trackMetabotChatOpened
270
271// Bad
272DataStudioLibraryCreated // Missing "track" prefix
273track_library_created // Wrong case
274logLibraryCreated // Use "track" prefix
275```
276
277## Common Patterns
278
279### Pattern 1: Sharing Field Types Across Features
280
281When two features send the same event with a different triggered_from, export the field union from
282the owning feature's analytics.ts and import it — don't hoist anything into metabase-types:
283
284```typescript
285// frontend/src/metabase/data-studio/data-model/analytics.ts
286import { trackSimpleEvent } from "metabase/analytics";
287import type { MetadataEditEventDetail } from "metabase/metadata/pages/shared/analytics";
288
289export function trackMetadataChange(detail: MetadataEditEventDetail) {
290 trackSimpleEvent({
291 event: "metadata_edited",
292 event_detail: detail,
293 triggered_from: "data_studio",
294 });
295}
296```
297
298This is the point of the extensible-events design: enterprise and feature-tier types stay in their
299own module instead of being imported down into a shared union.
300
301### Pattern 2: Conditional Tracking
302
303Track different events based on user action:
304
305```typescript
306const handleSave = async () => {
307 if (isNewItem) {
308 await createItem(data);
309 trackItemCreated(newItem.id);
310 } else {
311 await updateItem(id, data);
312 trackItemUpdated(id);
313 }
314};
315```
316
317## Common Pitfalls
318
319### Don't: Add custom fields to a simple event
320
321```typescript
322// WRONG - SimpleEventSchema doesn't support custom fields (this is a compile error)
323export const trackFiltersApplied = (filters: FilterState) => {
324 trackSimpleEvent({
325 event: "filters_applied",
326 data_layer: filters.dataLayer, // ❌ Not in SimpleEventSchema
327 data_source: filters.dataSource, // ❌ Not in SimpleEventSchema
328 with_owner: filters.hasOwner, // ❌ Not in SimpleEventSchema
329 });
330};
331
332// RIGHT - Use only standard SimpleEventSchema fields
333export const trackFiltersApplied = () => {
334 trackSimpleEvent({
335 event: "filters_applied",
336 });
337};
338
339// Or use event_detail for a single variant
340export const trackFilterApplied = (filterType: string) => {
341 trackSimpleEvent({
342 event: "filter_applied",
343 event_detail: filterType, // ✓ "data_layer", "data_source", etc.
344 });
345};
346```
347
348### Don't: Add event types to metabase-types/analytics/event.ts
349
350The central SimpleEvent union was removed — it forced feature-tier types to be imported down into
351shared code, causing module-boundary violations. trackSimpleEvent is generic now, so the type adds
352nothing but duplication.
353
354```typescript
355// ❌ WRONG - central declaration + re-import for a satisfies clause
356// frontend/src/metabase-types/analytics/event.ts
357export type NewFeatureClickedEvent = ValidateEvent<{
358 event: "new_feature_clicked";
359 target_id: number;
360}>;
361
362// frontend/src/metabase/my-feature/analytics.ts
363import type { NewFeatureClickedEvent } from "metabase-types/analytics";
364
365export const trackNewFeatureClicked = (id: number) => {
366 trackSimpleEvent({
367 event: "new_feature_clicked",
368 target_id: id,
369 } satisfies NewFeatureClickedEvent);
370};
371
372// ✓ RIGHT - the object literal is already checked by the generic
373// frontend/src/metabase/my-feature/analytics.ts
374export const trackNewFeatureClicked = (id: number) => {
375 trackSimpleEvent({
376 event: "new_feature_clicked",
377 target_id: id,
378 });
379};
380```
381
382A few ...Event types still sit in metabase-types/analytics/event.ts. They are leftovers from PRs
383that landed around the refactor — don't copy them, and don't add to them.
384
385### Don't: Mix up event name formats
386
387```typescript
388// WRONG
389event: "dataStudioLibraryCreated" // camelCase
390event: "data-studio-library-created" // kebab-case
391event: "Data_Studio_Library_Created" // Mixed case
392
393// RIGHT
394event: "data_studio_library_created" // snake_case
395```
396
397### Don't: Track PII or sensitive data
398
399```typescript
400// WRONG - Don't track user emails, names, or sensitive data
401trackSimpleEvent({
402 event: "user_logged_in",
403 event_detail: user.email, // ❌ PII
404});
405
406// RIGHT - Track non-sensitive identifiers only
407trackSimpleEvent({
408 event: "user_logged_in",
409 target_id: user.id, // ✓ Just the ID
410});
411```
412
413### Don't: Forget to track both success and failure
414
415```typescript
416// WRONG - Only tracking success
417try {
418 await saveData();
419 trackDataSaved();
420} catch (error) {
421 // ❌ No tracking for failure case
422}
423
424// RIGHT - Track both outcomes
425try {
426 await saveData();
427 trackDataSaved({ result: "success" });
428} catch (error) {
429 trackDataSaved({ result: "failure" });
430}
431```
432
433## Testing Analytics Events
434
435While developing, you can verify events are firing:
436
4371. **Check browser console** - When SNOWPLOW_ENABLED=true in dev, events are logged
4382. **Use shouldLogAnalytics** - Set in metabase/env to see all analytics in console
4393. **Check Snowplow debugger** - Browser extension for Snowplow events
440
441Example console output:
442
443```
444[SNOWPLOW EVENT | event sent:true], data_studio_table_picker_filters_applied
445```
446
447## File Organization
448
449### Where to put tracking functions:
450
451```
452Tracking functions AND their local field types (this is where new events live):
453frontend/src/metabase/{feature}/analytics.ts
454enterprise/frontend/src/metabase-enterprise/{feature}/analytics.ts
455
456Core tracking utilities:
457frontend/src/metabase/analytics/ (import from metabase/analytics)
458
459Shared SimpleEventSchema only — nothing new goes here:
460frontend/src/metabase-types/analytics/event.ts
461```
462
463In embedding SDK code, use trackSdkSimpleEvent
464(frontend/src/embedding-sdk-bundle/analytics/snowplow.ts) instead — the main-app "sp" tracker
465isn't initialized in the customer's page, so trackSimpleEvent's Snowplow leg is a no-op there.
466
467## Real-World Examples
468
469See these files for reference:
470
471- **Simple events + local field union**: frontend/src/metabase/metadata/pages/shared/analytics.ts
472- **Reusing another feature's field type**: frontend/src/metabase/data-studio/data-model/analytics.ts
473- **Result + duration timing**: frontend/src/metabase/archive/analytics.ts
474- **Enterprise feature events**: enterprise/frontend/src/metabase-enterprise/google_drive/analytics.ts
475
476## Workflow Summary
477
4781. **Identify the user interaction** to track
4792. **Decide on event name** (snake_case, descriptive)
4803. **Create tracking function** in feature's analytics.ts, calling trackSimpleEvent()
4814. **Add local field unions** in that same file if a field has a fixed set of values
4825. **Import and call** at the interaction point
4836. **Test** that events fire correctly
484
485## Tips
486
487- **Be specific** - filters_applied is better than action_performed
488- **Use past tense** - library_created not create_library
489- **Group related events** - Keep a feature's tracking functions together in its analytics.ts
490- **Track meaningful actions** - Not every click needs tracking
491- **Consider the data** - What would you want to analyze later?
492- **Stay consistent** - Follow existing naming patterns in the codebase
493- **Document context** - Use triggered_from to track where the action happened
494
In the file
SKILL.md1,762 words
Files1
LicenceAGPL-3.0
Why you can read it

Nothing in a skill executes. The client loads the text and the model follows it, so a skill can be audited the way a runbook is — by reading it.

What it costs in context

Skills are not billed by the call. They are paid for in context: every token the instructions occupy is a token your code, your diff and your conversation cannot use. Here is what this one takes and when it takes it.

≈40
always loaded
The name and description, so the model knows the skill exists and when to reach for it.
3,685
on trigger
The instruction body, read only when the skill fires.
1.9%
of a 200k window
Ten skills this size would take about 19% of the window before you open a file.
050k100k150k200k context window

3.7k tokens, estimated from the bundle at four bytes to the token, held for the rest of the session once it triggers. Middling. Fine to keep on in a project where you use it weekly, worth unloading in one where you never do.

Servers bill, skills cost

A server charges by the month. A skill charges once per session, in context, and then keeps charging it for as long as the session lives.

Before and after

The same question, put to the same model twice: once as it comes, and once with these instructions loaded.

No worked example has been published for this skill yet.

Adoption
Installsnone yet
Ratingno reviews yet

The procedure it runs

The procedure has not been published here. It is in the skill’s own SKILL.md, which its author has not sent to the marketplace yet.

Prose, not code

These steps are written for a model to follow, not executed by a runtime. It can still be told to skip one, and it will say so when it does.

Servers it uses

None. This skill calls no MCP servers at all.

Everything it needs is in the instructions, so it works in a project with nothing connected — the model reads the file and changes how it works with what it can already reach.

It asks the agent to write files, using whatever file access your client already has. It never touches the network.

What it asks for
Writes filesyes
Network accessno

Read from the allowed-tools line of this skill’s own SKILL.md. A skill grants no permissions of its own — it can only ask for tools your client already has.

What it will not do

Every skill is narrow, and the useful ones say where they stop. These are the jobs this one is the wrong tool for.

What this skill is not for has not been published here. Nothing is implied by that: it is a section the author has not filled in.

What is in the bundle

1 file, 14.9 kB on disk. A bundle is text throughout: the instructions the model reads, plus the templates it fills in.

  • SKILL.md14.9 kB
What is not in it

No dependencies and nothing executable: a skill is text the agent reads, so the bundle is 1 file you can review in full before installing. The AGPL-3.0 licence covers the templates and examples as well as the instructions.

Install

Installing copies the bundle into your project. Nothing runs at install time — the files sit on disk until the model reads them.

# Frontend Analytics Events · 3.7k tokens when loaded npx mcprush@latest skill add metabase/frontend-analytics-events

Writes to .claude/skills/frontend-analytics-events/ in the current project. Add --global to put it in your home directory instead, for every project.

Which clients pick it up on their own

A skill is a folder of text. A client with a skills folder reads it without being told; everywhere else the same text works, it is just handed to the model rather than found.

Claude Code.claude/skills/
Claude Desktop
ChatGPT
Cursor.cursor/skills/
VS Code.github/skills/
Codex CLI.agents/skills/
Gemini CLI.gemini/skills/
Grok.grok/skills/
Zed.agents/skills/
Windsurf.windsurf/skills/
Agent SDK.claude/skills/
HTTP / API
This release
Versionnot versioned
Publishedno release date on file
PriceFree
Referencemetabase/frontend-analytics-events

Versions

Its author publishes no version number, so there is nothing here to pin to: what you install is the folder as it stands today. Instructions change more often than APIs do — a skill can be rewritten entirely without anything it depends on moving.

v
  • No earlier releases have been published to the marketplace.
Pinning

Nothing to pin to: this skill carries no version number of its own. What you install is what the folder holds on the day you install it.

Reviews

no reviews yet · no installs yet

Nobody has reviewed this skill yet. The rating is the mean of the reviews written here, so there is none until somebody writes the first.

Who can post

Only accounts that have had the skill installed for fourteen days, so a review is written after living with it rather than after reading it. Publishers may reply once.

Publisher
Servers0
Claim this skill