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

实施 A/B/n 实验

实施实验

示例

首先,像往常一样使用 bin/feature-flag 命令生成一个功能标志,确保将类型设置为 experiment。为了文档说明,我们将我们的功能标志(和实验)命名为 pill_color

bin/feature-flag pill_color -t experiment

生成所需的功能标志后,您就可以立即在代码中实现实验。基本的实验实现可以是:

experiment(:pill_color, actor: current_user) do |e|
  e.control { 'control' }
  e.variant(:red) { 'red' }
  e.variant(:blue) { 'blue' }
end

当代码执行时,实验会运行,一个变体会被分配,并且(如果在控制器或视图中)客户端层中会出现一个 window.gl.experiments.pill_color 对象,其中包含以下详细信息:

  • 分配的变体。
  • 客户端跟踪事件的上下文键。

此外,当实验运行时,会为实验 :assignment 跟踪一个事件。我们稍后会详细介绍事件、跟踪和客户端层。

在本地开发中,您可以使用功能标志界面使实验处于激活状态。您还可以通过提供相关实验来启用功能标志,以针对特定情况:

# 为所有人启用
Feature.enable(:pill_color)

# 获取 `experiment` 方法 -- 在控制器、视图和邮件程序中已可用
include Gitlab::Experiment::Dsl
# 仅对第一个用户启用
Feature.enable(:pill_color, experiment(:pill_color, actor: User.first))

要在环境中部署您的实验功能标志,请使用 ChatOps 运行以下命令(在 GitLab 开发中的功能标志 文档中有更详细的介绍)。此命令创建了一个场景,其中遇到实验的人中有一半会被分配到 control,25% 会被分配到 red 变体,25% 会被分配到 blue 变体:

/chatops run feature set pill_color 50 --actors

为了在这个例子中获得更均匀的分布,将命令更改为设置为 66% 而不是 50。

要立即停止运行实验,请使用 /chatops run feature set pill_color false 命令。

我们强烈建议在使用 ChatOps 命令时使用 --actors 标志,因为其他任何操作都可能由于变体分配的缓存处理方式而产生奇怪的行为。

我们也可以在带有 HTML 包装的 HAML 文件中实现这个实验:

#cta-interface
  - experiment(:pill_color, actor: current_user) do |e|
    - e.control do
      .pill-button control
    - e.variant(:red) do
      .pill-button.red red
    - e.variant(:blue) do
      .pill-button.blue blue

上下文的重要性

在我们之前的示例实验中,我们的上下文(这是一个重要术语)是一个设置为 { actor: current_user } 的哈希。上下文必须根据您希望运行实验的方式进行唯一设置,并且应该在较低层次上理解。

建议您使用其中一些上下文来简化报告:

  • { actor: current_user }: 分配一个变体,并对每个进入实验的用户(如果 current_user 为 nil 则为 “client”)保持"粘性"。
  • { project: project }: 分配一个变体,并对正在查看的项目保持"粘性"。如果您的实验在查看项目时比特定用户查看任何项目时更有用,请考虑这种方法。
  • { group: group }: 与项目示例类似,但适用于更广泛的项目和用户范围。
  • { actor: current_user, project: project }: 分配一个变体,并对正在查看给定项目的用户保持"粘性"。这为 current_user 查看的每个项目创建了不同的变体分配可能性。请理解,如果这样的实验在应用程序的高流量部分运行,可能会创建很大的缓存大小。
  • { wday: Time.current.wday }: 根据当前星期几分配变体。在这个例子中,它会在周五始终分配一个变体,在周六可能会分配不同的变体。

上下文对于您如何定义和报告实验至关重要。这通常也是您选择实施实验方式的最重要方面,因此请仔细考虑,并在需要时与更广泛的团队讨论。同时,请注意您选择的上下文会影响我们的缓存大小。

在上述示例之后,我们可以总结一般情况:给定一个特定的、一致的上下文,我们可以提供一致的经验并为该经验跟踪事件。 为了更深入地了解实现细节:从提供的上下文中生成一个上下文键。使用此上下文键来:

  • 确定分配的变体。
  • 识别针对该上下文键跟踪的事件。

我们可以将其视为我们已呈现的经验,这既由上下文键决定,也由上下文键跟踪。上下文键用于跟踪我们为该上下文键呈现的交互和结果。这些概念最初可能有些抽象且难以理解,但这种方法使我们能够将实验视为比用户行为更广泛的东西。

如果 current_user 为 nil,使用 actor: 会使用 cookies。但是,如果您不需要 cookies——这意味着暴露的功能只对认证用户可见——{ user: current_user } 会同样有效。

变体分配的缓存是通过使用此上下文完成的,因此在定义实验时要考虑对缓存大小的影响。如果您使用 { time: Time.current },每次运行实验都会增加缓存大小。不仅如此,您的实验不会是"粘性的",事件也无法解析。

高级实验

有两种方法可以实现实验:

  1. 之前描述的基本实验风格。
  2. 提供实验类的更高级风格。

高级风格通过命名约定处理,并且类似于您在 Rails 中期望的工作方式。

要生成可以覆盖 ApplicationExperiment 中默认值的自定义实验类,请使用 Rails 生成器:

rails generate gitlab:experiment pill_color control red blue

这会在 app/experiments/pill_color_experiment.rb 中生成一个实验类,其中包含我们提供给生成器的 behaviors。以下是迁移我们之前的示例后该类的示例:

class PillColorExperiment < ApplicationExperiment
  control { 'control' }
  variant(:red) { 'red' }
  variant(:blue) { 'blue' }
end

我们现在可以通过显式调用 run 来简化运行实验的位置,而不是最初提供的块:

experiment(:pill_color, actor: current_user).run

我们在实验类中定义的 behaviors 代表默认实现。但是,您仍然可以使用块语法来覆盖这些 behaviors,因此以下内容也是有效的:

experiment(:pill_color, actor: current_user) do |e|
  e.control { '<strong>control</strong>' }
end

当向 experiment 方法传递块时,它会被隐式调用,就像已经调用了 run 一样。

分段规则

您可以使用运行时分段规则,例如将上下文分段到特定变体。segment 方法是一个回调(如 before_action),因此允许提供块或方法名。

在这个例子中,任何名为 'Richard' 的用户都会被分配到 red 变体,任何超过 2 周的账户都会被分配到 blue 变体:

class PillColorExperiment < ApplicationExperiment
  # ...已注册的行为

  segment(variant: :red) { context.actor.first_name == 'Richard' }
  segment :old_account?, variant: :blue

  private

  def old_account?
    context.actor.created_at < 2.weeks.ago
  end
end

当实验运行时,分段规则按照定义的顺序执行。产生真实结果的第一条分段规则会分配变体。

在我们的例子中,任何名为 'Richard' 的用户,无论账户年龄如何,都会被分配到 red 变体。如果您想要相反的逻辑,请颠倒顺序。

定义分段规则时请注意:在产生真实结果后,剩余的分段规则会被跳过以实现最佳性能。

排除规则

排除规则类似于分段规则,但旨在确定是否应该将上下文视为我们应该包含在实验中并跟踪事件的内容。排除意味着我们不关心与给定上下文相关的事件。

这些例子排除了所有名为 'Richard' 的用户,以及任何超过 2 周的账户。他们不仅被给予控制行为——这可能什么都不是——而且在这些情况下也不会跟踪任何事件。

class PillColorExperiment < ApplicationExperiment
  # ...已注册的行为

  exclude :old_account?, ->{ context.actor.first_name == 'Richard' }

  private

  def old_account?
    context.actor.created_at < 2.weeks.ago
  end
end

您可能还需要通过调用 should_track? 在自定义跟踪逻辑中检查排除:

class PillColorExperiment < ApplicationExperiment
  # ...已注册的行为

  def expensive_tracking_logic
    return unless should_track?

    track(:my_event, value: expensive_method_call)
  end
end

跟踪事件

实验最重要的方面之一是收集数据并报告。您可以使用 track 方法在实验实现中跟踪事件。如果您在调用实验之间提供相同的上下文,就可以一致地跟踪实验的事件。如果您不理解上下文,现在应该阅读关于上下文的内容。

我们可以假设我们在一个或几个地方运行实验,但在许多地方跟踪事件。跟踪调用保持不变,使用您在使用 snowplow 跟踪事件时通常使用的参数。在 Ruby 中跟踪事件的最简单例子是:

experiment(:pill_color, actor: current_user).track(:clicked)

当您使用到目前为止的任何示例运行实验时,默认会自动跟踪一个 :assignment 事件。从实验跟踪的所有事件都会向事件添加一个特殊的实验上下文。这通常可以被数据团队使用,以创建给定实验上事件之间的连接。

如果我们的用户没有遇到过实验(意味着实验运行的地方),而我们为他们跟踪了一个事件,他们会被分配一个变体,如果他们后来遇到实验,他们会看到那个变体,届时会为他们跟踪一个 :assignment 事件。

GitLab 试图在跟踪方面对客户保持敏感和尊重,因此我们的实验库允许我们实现实验而不跟踪识别 ID。但是,根据实验报告要求,这并不总是可能的。您可能会被要求偶尔在实验中跟踪特定记录 ID。这种方法很大程度上取决于创建实现的 PM 和工程师。目前此处不提供任何建议。

客户端层中的实验

在请求生命周期中运行的任何实验都会出现在 window.gl.experiments 中,并匹配此模式,以便在解析客户端层中的实验时使用。

鉴于我们已经为实验定义了一个类,并为其定义了变体,我们可以通过几种方式发布该实验。

第一种方式是通过运行实验。假设实验已经运行,它会出现在客户端层中,无需做任何特殊处理。

第二种方式不运行实验,并且仅在实验必须仅出现在客户端层时使用。为此,我们可以 .publish 实验。这不会运行任何逻辑,但会在客户端层中显示实验细节,以便在那里使用。

一个例子是在控制器的 before_action 中发布实验。假设我们已经定义了 PillColorExperiment 类,如上所述,我们可以通过发布而不是运行它来将其呈现给客户端:

before_action -> { experiment(:pill_color).publish }, only: [:show]

然后您可以在 JavaScript 控制台中看到这个呈现:

window.gl.experiments // => { pill_color: { excluded: false, experiment: "pill_color", key: "ca63ac02", variant: "candidate" } }

在 Vue 中使用实验

使用 gitlab-experiment 组件,您可以定义与推送到 window.gl.experiments 的变体名称匹配的插槽。

我们可以利用 Vue 组件中的命名插槽,这些插槽与 中定义的行为匹配:

<script>
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';

export default {
  components: { GitlabExperiment }
}
</script>

<template>
  <gitlab-experiment name="pill_color">
    <template #control>
      <button class="bg-default">Click default button</button>
    </template>

    <template #red>
      <button class="bg-red">Click red button</button>
    </template>

    <template #blue>
      <button class="bg-blue">Click blue button</button>
    </template>
  </gitlab-experiment>
</template>

window.gl.experiments 对象中没有给定实验名称的实验数据时,如果存在,则使用 control 插槽。