Help us learn about your current experience with the documentation. Take the survey.

内部事件跟踪快速入门

为了提供更高效、可扩展且统一的跟踪 API,GitLab 正在逐步弃用现有的 RedisHLL 和 Snowplow 跟踪方式。取而代之的是,我们正在实现新的 track_event(后端)和 trackEvent(前端)方法。 通过这种方式,我们可以在不关心底层实现的情况下,同时更新 RedisHLL 计数器并发送 Snowplow 事件。

要在您的代码中集成内部事件跟踪,您需要做三件事:

  1. 定义一个事件
  2. 定义一个或多个指标
  3. 触发事件

定义事件和指标

要创建事件和/或指标定义,请使用 gitlab 目录下的 internal_events 生成器:

scripts/internal_events/cli.rb

这个 CLI 工具将帮助您根据具体用例创建正确的定义文件,然后提供用于集成和测试的代码示例。

事件命名应遵循 <action>_<target_of_action>_<where/when> 的格式,有效示例包括 create_ci_buildclick_previous_blame_on_blob_page

触发事件

在后端和前端触发事件并更新指标的方式略有不同。请参考下面的相关部分。

后端跟踪

观看关于 使用内部事件进行后端集成 的视频

要触发事件,请从 Gitlab::InternalEventsTracking 模块调用 track_internal_event 方法,并传入所需的参数:

include Gitlab::InternalEventsTracking

track_internal_event(
  "create_ci_build",
  user: user,
  namespace: namespace,
  project: project
)

此方法会自动递增与事件 create_ci_build 相关的所有 RedisHLL 指标,并发送一个包含所有命名参数和标准上下文(仅限 SaaS)的相应 Snowplow 事件。 此外,触发事件的类名会保存在 Snowplow 事件的 category 属性中。

如果您定义了一个具有 unique 属性的指标(如 unique: project.id),则必须提供 project 参数。

建议尽可能多地填写 usernamespaceproject,因为这可以提高数据质量,并使将来定义指标更加容易。

如果提供了 project 但没有提供 namespace,则使用 project.namespace 作为事件的 namespace

在某些情况下,您可能需要手动指定 category 或完全不提供。为此,您可以直接调用 InternalEvents.track_event 方法,而不是使用模块。

当一个功能通过多个命名空间启用,并且需要跟踪启用该功能的原因时, 可以传递一个可选的 feature_enabled_by_namespace_ids 参数,其中包含命名空间 ID 数组。

track_internal_event(
  ...
  feature_enabled_by_namespace_ids: [namespace_one.id, namespace_two.id]
)

额外属性

在跟踪事件时可以传递额外属性。它们可用于保存与给定事件相关的额外数据。

跟踪类已经内置了三个属性:

  • label(字符串)
  • property(字符串)
  • value(数字)

这三个属性的任意命名和类型是由于数据提取过程的限制所致。 建议首先使用这些属性,即使它们的名称与您要跟踪的数据不匹配。您可以通过在事件的 YAML 定义中使用 description 属性来进一步描述实际跟踪的数据。示例请参见 create_ci_internal_pipeline.yml

additional_properties:
  label:
    description: 管道来源,例如推送、计划任务等。
  property:
    description: 配置来源,例如仓库、auto_devops 等。

通过在 #track_event 调用中包含 additional_properties 哈希来传递额外属性:

track_internal_event(
  "create_ci_build",
  user: user,
  additional_properties: {
    label: source, # label 跟踪管道的来源
    property: config_source # property 跟踪配置的来源
  }
)

如果需要传递超过三个内置的额外属性,可以使用带有自定义键的 additional_properties 哈希:

track_internal_event(
  "code_suggestion_accepted",
  user: user,
  additional_properties: {
    # 内置属性
    label: editor_name,
    property: suggestion_type,
    value: suggestion_shown_duration,
    # 您的自定义属性
    lang: 'ruby',
    custom_key: 'custom_value'
  }
)

仅在添加内置属性的基础上添加自定义属性。额外属性只能具有字符串或数字值。

确保额外属性不包含任何敏感信息。更多信息请参阅 数据分类标准

Controller 和 API 助手

有一个 ProductAnalyticsTracking 助手模块供控制器使用,您可以通过调用 #track_internal_event 来跟踪特定控制器操作的内部事件:

class Projects::PipelinesController < Projects::ApplicationController
  include ProductAnalyticsTracking

  track_internal_event :charts, name: 'visit_charts_on_ci_cd_pipelines', conditions: -> { should_track_ci_cd_pipelines? }

  def charts
    ...
  end

  private

  def should_track_ci_cd_pipelines?
    params[:chart].blank? || params[:chart] == 'pipelines'
  end
end

您需要在控制器主体中添加这两个方法,以便助手可以获取事件的当前项目和命名空间:

  private

  def tracking_namespace_source
    project.namespace
  end

  def tracking_project_source
    project
  end

此外,还有一个 API 助手:

track_event(
  event_name,
  user: current_user,
  namespace_id: namespace_id,
  project_id: project_id
)

批处理

当同时发出多个事件时,使用 with_batched_redis_writes 将它们批量处理到单个 Redis 调用中。

Gitlab::InternalEvents.with_batched_redis_writes do
  incr.times { Gitlab::InternalEvents.track_event(event) }
end

请注意,只有对总计数器的更新会被批处理。如果定义了 n 个唯一指标和 m 个总计数器指标,将导致 incr * n + m 次 Redis 写入。

后端测试

在测试触发内部事件或递增指标的代码时,可以在块参数上使用 trigger_internal_eventsincrement_usage_metrics 匹配器。

 expect { subject }
  .to trigger_internal_events('web_ide_viewed')
  .with(user: user, project: project, namespace: namespace)
  .and increment_usage_metrics('counts.web_views')

trigger_internal_events 匹配器接受与 receive 匹配器相同的链式方法(#once#at_most 等)。默认情况下,它期望提供的事件只被触发一次。

链式方法 #with 接受以下参数:

  • user - 用户对象
  • project - 项目对象
  • namespace - 命名空间对象。如果未提供,将设置为 project.namespace
  • additional_properties - 哈希。要随事件发送的额外属性。例如:{ label: 'scheduled', value: 20 }
  • category - 字符串。如果未提供,将设置为触发事件的对象的类名

increment_usage_metrics 匹配器接受与 change 匹配器相同的链式方法(#by#from#to 等)。默认情况下,它期望提供的指标递增一。

expect { subject }
  .to trigger_internal_events('web_ide_viewed')
  .with(user: user, project: project, namespace: namespace)
  .exactly(3).times

这两个匹配器都可以与其他作用于块的匹配器(如 change 匹配器)组合使用。

expect { subject }
  .to trigger_internal_events('mr_created')
    .with(user: user, project: project, category: category, additional_properties: { label: label } )
  .and increment_usage_metrics('counts.deployments')
    .at_least(:once)
  .and change { mr.notes.count }.by(1)

调试提示:如果您的测试因指标未按预期递增而失败, 您可能需要应用 :clean_gitlab_redis_shared_state 特性来清除示例之间的 Redis 缓存。

要测试事件未被触发,可以使用 not_trigger_internal_events 匹配器。它不接受消息链。

expect { subject }.to trigger_internal_events('mr_created')
    .with(user: user, project: project, namespace: namespace)
  .and increment_usage_metrics('counts.deployments')
  .and not_trigger_internal_events('pipeline_started')

或者您可以使用 not_to 语法:

expect { subject }.not_to trigger_internal_events('mr_created', 'member_role_created')

trigger_internal_events 匹配器也可用于测试 Haml with data attributes

前端跟踪

任何前端跟踪调用都会自动从当前页面的上下文中传递 user.idnamespace.idproject.id 的值。

Vue 组件

在 Vue 组件中,可以使用 Vue mixin 进行跟踪。

要实现 Vue 组件跟踪:

  1. 导入 InternalEvents 库并调用 mixin 方法:

    import { InternalEvents } from '~/tracking';
    const trackingMixin = InternalEvents.mixin();
  2. 在组件中使用该 mixin:

    export default {
      mixins: [trackingMixin],
    
      data() {
        return {
          expanded: false,
        };
      },
    };
  3. 调用 trackEvent 方法。跟踪选项可以作为第二个参数传递:

    this.trackEvent('click_previous_blame_on_blob_page');

    或者在模板中使用 trackEvent 方法:

    <template>
      <div>
        <button data-testid="toggle" @click="toggle">Toggle</button>
    
        <div v-if="expanded">
          <p>Hello world!</p>
          <button @click="trackEvent('click_previous_blame_on_blob_page')">Track another event</button>
        </div>
      </div>
    </template>

原生 JavaScript

对于从任意前端 JavaScript 代码直接跟踪事件,提供了原生 JavaScript 模块。这可以在无法使用 Mixin 的组件上下文之外使用。

import { InternalEvents } from '~/tracking';
InternalEvents.trackEvent('click_previous_blame_on_blob_page');

data-event 属性

此属性确保如果我们想要为按钮跟踪 GitLab 内部事件,不需要在 Click 处理程序中编写 JavaScript 代码。相反,我们只需添加一个带有事件值的 data-event-tracking 属性,它就应该可以工作。这也可以与 HAML 视图一起使用。

  <gl-button
    data-event-tracking="click_previous_blame_on_blob_page"
  >
   Click Me
  </gl-button>

Haml

= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle',  data: { event_tracking: 'click_previous_blame_on_blob_page' }}) do

渲染时的内部事件

有时我们希望在组件渲染或加载时发送内部事件。在这些情况下,我们可以添加 data-event-tracking-load="true" 属性:

= render Pajamas::ButtonComponent.new(button_options: { data: { event_tracking_load: 'true', event_tracking: 'click_previous_blame_on_blob_page' } }) do
        = _("New project")

额外属性

您可以包含额外属性与事件一起保存额外数据。包含时,您必须在 additional_properties 字段中定义每个额外属性。可以发送三个内置的额外属性,键为 label(字符串)、property(字符串)和 value(数字),以及自定义额外属性,如果内置属性不够用的话。

不要将页面 URL 或页面路径作为额外属性传递,因为我们已经为每个事件跟踪了匿名化的页面 URL。 从 window.location 获取 URL 不会对项目和命名空间信息进行匿名化处理,如文档所述

对于 Vue Mixin:

   this.trackEvent('click_view_runners_button', {
    label: 'group_runner_form',
    property: dynamicPropertyVar,
    value: 20
   });

对于原生 JavaScript:

   InternalEvents.trackEvent('click_view_runners_button', {
    label: 'group_runner_form',
    property: dynamicPropertyVar,
    value: 20
   });

对于 data-event 属性:

  <gl-button
    data-event-tracking="click_view_runners_button"
    data-event-label="group_runner_form"
    :data-event-property=dynamicPropertyVar
    data-event-additional='{"key1": "value1", "key2": "value2"}'
  >
   Click Me
  </gl-button>

对于 Haml:

= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle',  data: { event_tracking: 'action', event_label: 'group_runner_form', event_property: dynamic_property_var, event_value: 2, event_additional: '{"key1": "value1", "key2": "value2"}' }}) do

前端测试

JavaScript/Vue

如果您在任何代码中使用 trackEvent 方法,无论是在原生 JavaScript 中还是在 Vue 组件中,都可以使用 useMockInternalEventsTracking 助手方法来断言 trackEvent 是否被调用。

例如,如果我们需要测试下面的 Vue 组件,

<script>
import { GlButton } from '@gitlab/ui';
import { InternalEvents } from '~/tracking';
import { __ } from '~/locale';

export default {
  components: {
    GlButton,
  },
  mixins: [InternalEvents.mixin()],
  methods: {
    handleButtonClick() {
      // 一些应用逻辑
      // 当某些事件发生时触发跟踪调用
      this.trackEvent('click_view_runners_button', {
        label: 'group_runner_form',
        property: 'property_value',
        value: 3,
      });
    },
  },
  i18n: {
    button1: __('Sample Button'),
  },
};
</script>
<template>
  <div style="display: flex; height: 90vh; align-items: center; justify-content: center">
    <gl-button class="sample-button" @click="handleButtonClick">
      {{ $options.i18n.button1 }}
    </gl-button>
  </div>
</template>

以下是该组件的测试用例。

import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DeleteApplication from '~/admin/applications/components/delete_application.vue';
import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';

describe('DeleteApplication', () => {
  /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
  let wrapper;

  const createComponent = () => {
    wrapper = shallowMountExtended(DeleteApplication);
  };

  beforeEach(() => {
    createComponent();
  });

  describe('sample button 1', () => {
    const { bindInternalEventDocument } = useMockInternalEventsTracking();
    it('should call trackEvent method when clicked on sample button', async () => {
      const { trackEventSpy } = bindInternalEventDocument(wrapper.element);

      await wrapper.find('.sample-button').vm.$emit('click');

      expect(trackEventSpy).toHaveBeenCalledWith(
        'click_view_runners_button',
        {
          label: 'group_runner_form',
          property: 'property_value',
          value: 3,
        },
        undefined,
      );
    });
  });
});

如果您在 Vue/View 模板中使用跟踪属性,如下所示,

<script>
import { GlButton } from '@gitlab/ui';
import { InternalEvents } from '~/tracking';
import { __ } from '~/locale';

export default {
  components: {
    GlButton,
  },
  mixins: [InternalEvents.mixin()],
  i18n: {
    button1: __('Sample Button'),
  },
};
</script>
<template>
  <div style="display: flex; height: 90vh; align-items: center; justify-content: center">
    <gl-button
      class="sample-button"
      data-event-tracking="click_view_runners_button"
      data-event-label="group_runner_form"
    >
      {{ $options.i18n.button1 }}
    </gl-button>
  </div>
</template>

以下是该组件的测试用例。

import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DeleteApplication from '~/admin/applications/components/delete_application.vue';
import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';

describe('DeleteApplication', () => {
  /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
  let wrapper;

  const createComponent = () => {
    wrapper = shallowMountExtended(DeleteApplication);
  };

  beforeEach(() => {
    createComponent();
  });

  describe('sample button', () => {
    const { bindInternalEventDocument } = useMockInternalEventsTracking();
    it('should call trackEvent method when clicked on sample button', () => {
      const { triggerEvent, trackEventSpy } = bindInternalEventDocument(wrapper.element);
      triggerEvent('.sample-button');
      expect(trackEventSpy).toHaveBeenCalledWith('click_view_runners_button', {
        label: 'group_runner_form',
      });
    });
  });
});

带数据属性的 Haml

如果您在 Haml 层使用 data attributes 来跟踪内部事件, 可以使用 trigger_internal_events matcher 来断言预期的属性是否存在。

例如,如果您需要测试下面的 Haml,

%div{ data: { testid: '_testid_', event_tracking: 'some_event', event_label: 'some_label' } }

您可以在任何与 have_css 匹配器兼容的渲染 HTML 上调用断言。 使用 :on_click:on_load 链式方法来指示您期望事件何时触发。

以下是该 haml 的测试用例。

  it 'assigns the tracking items' do
    render

    expect(rendered).to trigger_internal_events('some_event').on_click
      .with(additional_properties: { label: 'some_label' })
  end
  • 渲染的 HTML 是一个 Capybara::Node::SimpleViewComponent
  it 'assigns the tracking items' do
    render_inline(component)

    expect(page.find_by_testid('_testid_'))
      .to trigger_internal_events('some_event').on_click
      .with(additional_properties: { label: 'some_label' })
  end
  • 渲染的 HTML 是一个 Nokogiri::HTML4::DocumentFragmentViewComponent
  it 'assigns the tracking items' do
    expect(render_inline(component))
      .to trigger_internal_events('some_event').on_click
      .with(additional_properties: { label: 'some_label' })
  end

或者您可以使用 not_to 语法:

  it 'assigns the tracking items' do
    render_inline(component)

    expect(page).not_to trigger_internal_events
  end

当否定时,匹配器不接受额外的链式方法或参数。 这断言没有使用任何跟踪属性。

使用内部事件 API

您还可以使用我们的 API 来跟踪连接到 GitLab 实例的其他系统的事件。 更多信息请参阅 Usage Data API documentation

其他系统上的内部事件

除了 GitLab 代码库,我们还在以下系统中使用内部事件。

  1. AI gateway
  2. Switchboard