mcpField
A
analytics-events

analytics-events

github.com/metabase数据与数据库49.9k2.0k 次安装今天更新
代码仓库

关于 analytics-events

面向 Metabase 前端开发者的技能指南,说明如何用 Snowplow 和 trackSimpleEvent 添加类型安全的产品分析事件,并将事件定义与功能模块放在一起。

主要功能

  1. 01使用 trackSimpleEvent 上报简单事件
  2. 02按标准字段校验事件载荷
  3. 03在功能模块内组织追踪函数和类型
  4. 04遵循事件命名及隐私规范
  5. 05验证事件是否成功触发

适用场景

  1. 01为界面交互添加产品分析
  2. 02按结果和耗时记录操作
  3. 03在开发环境检查 Snowplow 事件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/metabase/metabase --skill analytics-events

安装后在 Claude Code 中输入/analytics-events确认已加载。

详细介绍

前端分析事件 Skill

这个 skill 帮助你添加产品分析(Snowplow)事件,以跟踪 Metabase 前端代码库中的用户交互。

快速参考

Metabase 中的分析事件使用 Snowplow,并带有类型化的事件 schema。简单事件在使用它们的地方声明——trackSimpleEvent 是泛型的,并在调用点验证 payload。

关键文件:

  • frontend/src/metabase/analytics/event.ts - 核心跟踪函数,trackSimpleEvent / trackSchemaEvent(从 metabase/analytics 导入)
  • frontend/src/metabase-types/analytics/event.ts - 仅包含共享的 SimpleEventSchema。不要在这里添加事件类型(见下文)
  • frontend/src/metabase-types/analytics/schema.ts - Schema 注册表(仅自定义/legacy schema)
  • 各功能自己的 analytics.ts 文件 - 你的跟踪函数和任何本地类型所在的位置

快速检查清单

添加新的分析事件时:

  • 选择一个事件名称(snake_case,过去式)
  • 向该功能的 analytics.ts 文件添加一个跟踪函数,调用 trackSimpleEvent()
  • 将任何字段联合类型(例如 "success" | "failure")作为本地类型保留在同一个文件中
  • 在交互点导入并调用该跟踪函数
  • 不要向 metabase-types/analytics/event.ts 或任何联合类型中添加事件类型

事件 Schema 类型

1. 简单事件(最常见)

使用 SimpleEventSchema 进行直接跟踪。它支持以下标准字段:

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
};

何时使用: 90% 的事件都符合这个 schema。用于点击、打开、关闭、创建、删除等。

trackSimpleEvent 是泛型的,并会对你传入的对象字面量强制执行这个 schema:

// 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);
}

这意味着缺少 event 或任何不在 SimpleEventSchema 中的字段都会在调用点产生编译错误。没有单独的事件类型需要声明,也没有 satisfies 子句需要添加——旧的 ValidateEvent<...> 辅助类型不再导出,也不属于工作流的一部分。

trackSchemaEvent 也是泛型的:它会把 schema 名称与 payload 类型关联起来,因此你无法在 simple_event schema 下发送 dashboard 事件。

2. 自定义 Schema(legacy,不再添加事件)

只有在非常特殊的情况下才考虑添加新的事件 schema。

示例: DashboardEventSchema、CleanupEventSchema、QuestionEventSchema

分步指南:添加一个简单事件

示例:跟踪用户在表格选择器中应用筛选器
第 1 步:创建跟踪函数

在你功能的 analytics.ts 文件中(例如 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",
  });
};
第 2 步:在组件中使用

在交互点导入并调用该跟踪函数:

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>
  );
}

使用 SimpleEventSchema 字段

下面的所有示例都位于功能自己的 analytics.ts 中——没有任何内容被集中注册。

示例:带 target_id 的事件
export const trackDataStudioLibraryCreated = (id: CollectionId) => {
  trackSimpleEvent({
    event: "data_studio_library_created",
    target_id: Number(id),
  });
};

// Usage
trackDataStudioLibraryCreated(newLibrary.id);
示例:带 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>
示例:带 event_detail 的事件

真实示例——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");
示例:带 result 和 duration 的事件

真实的版本请参见 frontend/src/metabase/archive/analytics.ts。

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",
  });
}

命名约定

事件名称(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
本地字段类型(PascalCase,以字段命名)

通常不再有 ...Event 类型需要命名。当你确实需要为某个字段定义联合类型时,请以它所服务的字段命名:

// Good
type MetricDimensionResult = "success" | "failure";     // -> result
export type MetadataEditEventDetail = "type_casting";   // -> event_detail
type NewButtonLocation = "app-bar" | "empty-collection"; // -> triggered_from
跟踪函数名称(camelCase,带 "track" 前缀)
// Good
trackDataStudioLibraryCreated
trackTablePickerFiltersApplied
trackMetabotChatOpened

// Bad
DataStudioLibraryCreated      // Missing "track" prefix
track_library_created         // Wrong case
logLibraryCreated             // Use "track" prefix

常见模式

模式 1:跨功能共享字段类型

当两个功能发送相同的事件但具有不同的 triggered_from 时,从拥有该字段的功能的 analytics.ts 中导出该字段联合类型并导入它——不要将任何内容提升到 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",
  });
}

这就是可扩展事件设计的要点:企业版和功能层级的类型保留在它们自己的模块中,而不是被导入到一个共享联合类型中。

模式 2:条件跟踪

根据用户操作跟踪不同的事件:

const handleSave = async () => {
  if (isNewItem) {
    await createItem(data);
    trackItemCreated(newItem.id);
  } else {
    await updateItem(id, data);
    trackItemUpdated(id);
  }
};

常见陷阱

不要:为简单事件添加自定义字段
// 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.
  });
};
不要:向 metabase-types/analytics/event.ts 添加事件类型

中心的 SimpleEvent 联合类型已被移除——它迫使功能层级类型被向下导入到共享代码中,导致模块边界违规。trackSimpleEvent 现在是泛型的,因此该类型除了重复之外别无用处。

// ❌ 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,
  });
};

仍有少数 ...Event 类型留在 metabase-types/analytics/event.ts 中。它们是重构前后合并的 PR 的遗留物——不要复制它们,也不要向其中添加内容。

不要:混淆事件名称格式
// 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
不要:跟踪 PII 或敏感数据
// 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
});
不要:忘记同时跟踪成功和失败
// 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" });
}

测试分析事件

在开发过程中,你可以验证事件是否正在触发:

  1. 检查浏览器控制台 - 在开发环境中当 SNOWPLOW_ENABLED=true 时,事件会被记录
  2. 使用 shouldLogAnalytics - 在 metabase/env 中设置以在控制台中查看所有分析
  3. 检查 Snowplow 调试器 - 用于 Snowplow 事件的浏览器扩展

控制台输出示例:

[SNOWPLOW EVENT | event sent:true], data_studio_table_picker_filters_applied

文件组织

将跟踪函数放在哪里:
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

在 embedding SDK 代码中,改用 trackSdkSimpleEvent (frontend/src/embedding-sdk-bundle/analytics/snowplow.ts)——主应用的 "sp" 跟踪器 未在客户的页面中初始化,因此 trackSimpleEvent 的 Snowplow 部分在那里是空操作。

真实示例

参考以下文件:

  • 简单事件 + 本地字段联合类型:frontend/src/metabase/metadata/pages/shared/analytics.ts
  • 复用另一个功能的字段类型:frontend/src/metabase/data-studio/data-model/analytics.ts
  • 结果 + 持续时间计时:frontend/src/metabase/archive/analytics.ts
  • 企业功能事件:enterprise/frontend/src/metabase-enterprise/google_drive/analytics.ts

工作流总结

  1. 确定要跟踪的用户交互
  2. 决定事件名称(snake_case,具有描述性)
  3. 创建跟踪函数,放在功能的 analytics.ts 中,调用 trackSimpleEvent()
  4. 添加本地字段联合类型,如果某个字段有一组固定值,则在同一文件中添加
  5. 导入并调用,在交互点处
  6. 测试事件是否正确触发

提示

  • 要具体 - filters_applied 比 action_performed 更好
  • 使用过去时 - library_created 而不是 create_library
  • 将相关事件分组 - 将某个功能的跟踪函数保留在其 analytics.ts 中
  • 跟踪有意义的操作 - 不是每次点击都需要跟踪
  • 考虑数据 - 你以后想要分析什么?
  • 保持一致性 - 遵循代码库中现有的命名模式
  • 记录上下文 - 使用 triggered_from 来跟踪操作发生的位置

评论

支持 Markdown