shapeways/feature

Shapeways 对 Etsy 特性库的分支。分支目的是使其兼容 Composer。见:https://github.com/etsy/feature

dev-master 2015-03-01 21:02 UTC

This package is not auto-updated.

Last update: 2024-09-18 06:28:04 UTC


README

Shapeways 对 Etsy 的特性标记 API 分支,用于运营提升和 A/B 测试。此分支还包括通过 Composer 安装的能力。

特性 API 是我们选择性地启用和禁用功能的方式,可以非常细粒度地启用功能,并为运营提升和 A/B 测试启用特定比例的用户。一个特性可以是完全启用、完全禁用,或者介于两者之间,并可以包含多个相关变体。

对于未完全启用或禁用的特性,我们记录每次检查特性是否启用的情况,并将结果(包括所选变体)包含在触发的事件中。

API 的两个主要入口点是

Feature::isEnabled('my_feature')

my_feature 启用时返回 true,对于多变体特性

Feature::variant('my_feature')

返回应使用的特定变体的名称。

这些方法的单个参数是测试特性的名称。

对于单变体特性,使用 Feature::isEnabled 的典型用法如下

if (Feature::isEnabled('my_feature')) {
    // do stuff
}

对于多变体特性,在由 Feature::isEnabled 检查保护的代码块内,我们可以使用类似以下的方式确定每个变体应运行的适当代码

if (Feature::isEnabled('my_feature')) {

    switch (Feature::variant('my_feature')) {
      case 'foo':
          // do stuff appropriate for the foo variant
          break;
      case 'bar':
          // do stuff appropriate for the bar variant
          break;
    }
}

请求未启用的特性的变体是错误的(并将被记录为错误),因此对变体的调用应该总是由 Feature::isEnabled 检查来保护。

API 还提供了另外一对不太常用的方法

Feature::isEnabledFor('my_feature', $user)

Feature::variantFor('my_feature', $user)

Feature::isEnabledBucketingBy('my_feature', $bucketingID)

Feature::variantBucketingBy('my_feature', $bucketingID)

这些方法仅用于支持一些非常特定的用例:当我们想要基于不是发起请求的用户而是其他用户来启用或禁用特性时,或者当我们想要基于与用户完全不同的东西来对执行进行分桶时。前者在 Etsy 的典型用例是,如果我们想改变我们处理列表的方式,而不是只为一些用户启用特性,而是为所有看到这些列表的用户启用,但我们想为所有用户启用它,只为一些列表,那么我们可以使用 isEnabledForvariantFor,并传递表示列表所有者的用户对象。这将允许我们为特定的列表所有者启用特性。`bucketingBy` 方法有类似的目的,除了在没有相关用户或我们不想总是将相同的用户放入相同的桶中的情况下。因此,如果我们想为显示的所有列表中的 10% 启用某个特性,独立于发起请求的用户和列表所有者,我们可以使用 isEnabledBucketingBy 并以列表 ID 作为分桶 ID。

通常情况下,您更可能想要使用简单的 isEnabledvariant 方法。

对于 Smarty 模板,由于静态方法无法直接调用,Tpl.php 中配置了 $feature 对象,它暴露与特性 API 相同的四个方法,但作为实例方法,例如

{% if $feature->isEnabled("my_feature") %}

配置说明

有一些常见的配置,因此在解释特性配置段落的完整语法之前,这里有一些更常见的案例以及编写配置的最简洁方式。

完全启用的功能

$server_config['foo'] = 'on';

完全禁用的功能

$server_config['foo'] = 'off';

为所有人启用了获胜变体的功能

$server_config['foo'] = 'blue_background';

仅管理员可用的功能

$server_config['foo'] = array('admin' => 'on');

单一变体功能已逐步推广至1%的用户。

$server_config['foo'] = array('enabled' => 1);

多变体功能已逐步推广至每个变体1%的用户。

$server_config['foo'] = array(
   'enabled' => array(
       'blue_background'   => 1,
       'orange_background' => 1,
       'pink_background'   => 1,
   ),
);

仅针对单个特定用户启用的功能。

$server_config['foo'] = array('users' => 'fred');

针对少数特定用户启用的功能。

$server_config['foo'] = array(
   'users' => array('fred', 'barney', 'wilma', 'betty'),
);

针对特定群体启用的功能

$server_config['foo'] = array('groups' => 1234);

针对10%的普通用户和所有管理员启用

$server_config['foo'] = array(
   'enabled' => 10,
   'admin' => 'on',
);

功能已逐步推广至1%的请求,随机分配而非按用户分配

$server_config['foo'] = array(
   'enabled' => 1,
   'bucketing' => 'random',
);

50/50 A/B测试中的单一变体功能

$server_config['foo'] = array('enabled' => 50);

A/B测试中的多变体功能,20%的用户看到每个变体(剩余40%在对照组)

$server_config['foo'] = array(
   'enabled' => array(
       'blue_background'   => 20,
       'orange_background' => 20,
       'pink_background'   => 20,
   ),
);

仅通过将 ?features=foo 添加到URL中启用的新功能

$server_config['foo'] = array('enabled' => 0);

这是一种有点好笑的边缘情况。它也可以写成

$server_config['foo'] = array();

因为缺少 'enabled' 将默认为0。

配置细节

每个功能的配置段控制功能何时启用以及启用时应该使用哪个变体。

在不考虑一会儿将要解释的几个缩写的情况下,功能配置段的价值是一个包含多个特殊键的数组,其中最重要的键是 'enabled'

在完整形式中,'enabled' 属性的值是字符串 'off',表示功能完全禁用,任何其他字符串表示所有请求都启用了命名变体,或者是一个键为变体名称、值为应看到每个变体的请求百分比的数组。

作为支持只有一个变体的功能的常见情况,'enabled' 也可以指定为从0到100的百分比,这等价于指定一个包含变体名称 'on' 和给定百分比的数组。

功能配置段的下一个四个最重要的属性指定了特殊类别用户应看到的特定变体:'admin''internal''users''groups'

'admin''internal' 属性,如果存在,应命名一个应显示给所有管理员用户或所有内部请求的变体。对于单变体功能,这个名称几乎总是 'on'。技术上,您也可以指定 'off' 来关闭管理员用户或内部请求的功能,否则这些功能将是启用的。但这会很奇怪。对于多变体功能,可以是 'enabled' 数组中提到的任何变体。

'users''groups' 变体提供从变体名称到用户列表或数字组ID的映射。在完全指定的情况下,值将是一个键为变体名称、值为用户名列表或组ID列表的数组,根据需要。作为缩写,如果用户名或组ID列表只有一个元素,它可以只指定名称或ID。进一步缩写,在单变体功能的配置中,'users''groups' 属性的值可以简单地是分配给 'on' 变体的值。所以使用这两个缩写,这些是等效的

$server_config['foo'] => array('users' => array('on' => array('fred')));

$server_config['foo'] => array('users' => 'fred');

如果没有 'enabled' 值或变体的百分比是0,则这些四个属性都没有任何效果,因为在这种情况下,功能被认为是完全启用或禁用。然而,如果没有提供 'enabled' 值或变体的百分比是0,它们可以启用功能的一个变体。

另一方面,当指定数组 'enabled' 的值时,为了帮助检测拼写错误,在 'admin''internal''users''groups' 属性中使用的变体名称也必须是 'enabled' 数组中的键。因此,如果通过 'enabled' 指定了任何变体,则它们都应该如此,即使它们的百分比设置为 0。

剩下的两个功能配置属性是 'bucketing''public_url_override'。Bucketing 指定当功能仅对用户的百分比启用时,如何对用户进行分组。默认值 'uaid' 通过 UAID 饼干进行分组,这意味着用户将处于相同的组,无论他们是否已登录。

bucketing 值 'user' 导致基于已登录用户的 id 进行分组。目前,如果用户未登录,我们将回退到通过 UAID 进行分组,但这是有问题的,因为它意味着用户可以在登录或注销时切换组。(我们可能改变此分组方案的行为,使其仅对未登录的用户禁用功能。)

最后,bucketing 值 'random' 导致每个请求独立分组,这意味着同一用户在不同请求中将处于不同的组。这通常用于应该没有用户可见效果的功能,但我们想逐步增加像从 master 切换到碎片或新版本的 jquery 这样的功能。

'public_url_override' 属性允许所有请求(而不仅仅是管理员和内部请求)通过 features 查询参数打开功能并选择变体。如果存在,它的值几乎总是为 true,因为省略时默认为 false。

最后,还有两个简写

首先,只包含键 'enabled' 和字符串值的配置段可以替换为简单的字符串。所以

$server_config['foo'] = array('enabled' => 'on');
$server_config['bar'] = array('enabled' => 'off');
$server_config['baz'] = array('enabled' => 'some_variant');

可以简单地写成

$server_config['foo'] = 'on';
$server_config['bar'] = 'off';
$server_config['baz'] = 'some_variant';

其次,如果功能配置完全缺失,则相当于指定为 'off'。这允许暗色更改包含在尚未添加到 production.php 中的功能之前检查功能代码。

操作员注意:完全删除功能配置、将其设置为字符串 'off' 或将 'enabled' 设置为 'off' 都会完全禁用功能,确保受 Feature::isEnabled 保护的代码永远不会运行。在紧急情况下关闭现有功能的最好方法是设置 'enabled''off'。为了促进这一点,我们应尽可能将 'enabled' 值放在一行上。因此

$server_config['foo'] = array(
   'enabled' => array('foo' => 10, 'bar' => 10),
);

而不是

$server_config['foo'] = array(
   'enabled' => array(
       'foo' => 10,
       'bar' => 10
    ),
);

这样,凌晨 3 点眼睛模糊的初级操作员可以这样做

$server_config['foo'] = array(
   'enabled' => 'off', // array('foo' => 10, 'bar' => 10),
);

而不是这样做,这会破坏配置文件

$server_config['foo'] = array(
   'enabled' => 'off', // array(
       'foo' => 10,
       'bar' => 10
    ),
);

但是,请注意,删除 'enabled' 属性主要会关闭功能,而不会完全禁用它,因为它仍然可以通过 'admin' 属性等启用。

优先级

启用功能的各种机制的优先级如下。

  • 如果 'enabled' 是字符串(变体名称或 'off'),则功能对所有请求完全打开或关闭。

  • 否则,如果请求来自管理员用户或内部请求,或者如果 'public_url_override' 为 true 且请求包含指定特定功能的变体的 features 查询参数,则使用该变体。features 参数的值是逗号分隔的功能列表,其中每个功能要么是功能的名称,表示应使用变体 'on' 启用该功能,要么是功能的名称、冒号和变体名称。例如,带有 features=foo,bar:x,baz:off 的请求将打开功能 foo,使用变体 x 打开功能 bar,并关闭功能 baz

  • 否则,如果请求来自在'users'属性中指定的用户,则将启用指定的变体。

  • 否则,如果请求来自在'groups'属性中指定的组成员,则将启用指定的变体。(当用户是分配了不同变体的多个组成员时,其行为是未定义的。小心鼻涕鬼。)

  • 否则,如果请求来自管理员,则将启用'admin'变体。

  • 否则,如果请求是内部请求,则将启用'internal'变体。

  • 否则,将请求分桶,并选择一个变体,以确保正确比例的分桶请求将看到每个变体。

错误

有几种方式可能会误用功能API或错误配置功能,这些可能会被检测并记录下来。(其中一些目前可能不会被检测,但将来可能会。)

  1. 为单变体功能调用Feature::variant

  2. 在不经过Feature::isEnabled检查的情况下调用Feature::variant

  3. 在多变体功能中将'on'作为变体名称包括在内。

  4. 'enabled'设置为小于0或大于100的数值。

  5. 将变体的百分比值在'enabled'中设置为小于0或大于100的值。

  6. 'enabled'设置为使得变体百分比总和大于100。

  7. 'enabled'设置为非数字、非字符串、非数组值。

  8. 'enabled'是数组时,将'users''groups'属性设置为包含不是'enabled'中的键的键的数组。

  9. 'enabled'是数组时,将'admin''internal'属性设置为不是'enabled'中的键的值。

功能的生命周期

功能API的设计着眼于使其更容易地将功能通过可预测的生命周期进行推送,其中功能可以轻松创建、增加、A/B测试,然后清理,无论是通过提升为功能标志,还是删除配置和相关功能检查但保留代码,或者完全删除代码。

功能的基本生命周期可能看起来是这样的

  1. 开发人员编写了一些代码,并用Feature::isEnabled检查进行保护。为了在开发中测试该功能,他们将在development.php中添加功能的配置,为特定用户或管理员打开它,或将'enabled'设置为0,以便他们可以通过URL查询参数进行测试。

  2. 在某个时候,开发人员将向production.php添加一个配置段。最初这可能只是一个占位符,完全禁用功能,或者为管理员等打开它。

  3. 一旦功能完成,production.php配置将更改,以启用一小部分用户的功能进行操作测试。对于单变体功能,这意味着将'enabled'设置为一个小数值;对于多变体功能,这意味着将'enabled'设置为指定每个变体的一个小百分比的数组。

  4. 在增长期间,暴露于功能的用户比例可能会上下移动,直到开发人员和运维人员确信代码完全就绪。如果在任何时候出现严重问题,可以通过将enabled设置为'off'来完全禁用新代码。

  5. 如果功能要成为A/B实验的一部分,那么开发人员将(与数据团队合作)确定将功能暴露给的最佳用户比例以及实验需要运行多长时间以收集良好的实验数据。要启动实验,将生产配置更改为为适当比例的用户启用功能或其变体。在此之后,应保持百分比不变,直到实验完成。

在此阶段可能发生以下几种情况:如果实验显示有明确的赢家,我们可能只想保留代码,可能将其置于顶级功能标志的控制之下,以便运维人员可以出于运营原因禁用该功能。或者我们可能想删除与该功能相关的所有代码。或者我们可能想基于从这次实验中学到的东西运行另一个实验。以下是这些情况下的处理方式

将功能作为网站永久的一部分,而不创建顶级功能标志

  1. 将功能配置的值更改为获胜变体的名称(对于单变量功能为'on')。

  2. 删除实现其他变体的任何代码,并移除对Feature::variant的调用以及任何相关条件逻辑(例如,基于变体名称的开关)。

  3. 移除Feature::isEnabled检查,但保留它们保护的代码。

  4. 移除功能配置。

将功能置于完整功能标志的控制之下。(即对于通常会被启用但希望保留通过简单的配置更改关闭能力的情况)

  1. 将功能配置的值更改为获胜变体的名称(对于单变量功能为'on')。

  2. 删除实现其他变体的任何代码,并移除对Feature::variant的调用以及任何相关条件逻辑(例如,基于变体名称的开关)。

  3. 添加一个新的配置,以feature_前缀命名,并将其值设置为'on'

  4. 将旧标志名的所有Feature::isEnabled检查更改为新功能标志。

  5. 移除旧配置。

要完全删除功能

  1. 将功能配置的值更改为'off'

  2. 删除所有由Feature::isEnabled检查保护的代码,然后删除检查。

  3. 移除功能配置。

基于相同的代码运行新的实验

  1. 将功能配置的启用值设置为'off'

  2. 创建一个新的功能配置,具有类似名称,但后缀为_vN,其中N是2(如果是第二次实验),3(如果是第三次)。将其设置为'off'

  3. 将旧功能的所有Feature::isEnabled检查更改为新功能。

  4. 删除旧配置。

  5. 实现新实验所需的变化,根据需要删除旧变体并添加新变体。

  6. 按正常方式逐步推广并A/B测试新功能。

  7. 根据需要提升、清理或重新进行实验。

一些风格指南

为了更容易地推动功能通过此生命周期,有一些编码指南需要遵守。

首先,Feature方法(isEnabledvariantisEnabledForvariantFor)的功能名称参数始终应该是字符串字面量。这将使查找检查特定功能的所有位置变得更加容易。如果你发现自己正在运行时创建功能名称并对其进行检查,那么你很可能在滥用Feature系统。在这种情况下,你很可能根本不想使用Feature API,而是简单地使用一些普通的配置数据来驱动你的代码。

其次,Feature方法的结果不应被缓存,例如通过一次调用Feature::isEnabled并将结果存储在某个控制器的实例变量中。Feature机制已经缓存了其计算结果的输出,因此简单地调用Feature::isEnabledFeature::variant应该已经足够快。这又将有助于找到依赖于特定功能的依赖位置。

第三,作为正确使用Feature API的检查,每当你有if块,其测试是调用Feature::isEnabled时,请确保移除检查并保留代码或删除检查和代码一起都是合理的。在由isEnabled检查保护的块内不应该有需要被抢救的代码片段。