jowy/feature

用于操作上线和A/B测试的功能标志API。

dev-master 2016-06-27 09:34 UTC

This package is not auto-updated.

Last update: 2024-09-14 17:59:00 UTC


README

Build Status Scrutinizer Code Quality Code Coverage

Etsy的功能标志API的分支版本,用于操作上线和A/B测试。

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

安装

composer require jowy/feature

基本用法

<?php

use Feature\Feature;

require 'vendor/autoload.php';

// World class must implement World interface
$world = new World();

// define your feature stanza here
$stanza['some_feature'] = ['enabled' => ['variant1' => 50, 'variant2' => 50];

// initialize feature, just do it once
Feature::create($world, $stanza);

if (Feature::isEnabled('some_feature')) {
    switch(Feature::variant('some_feature')) {
        case 'variant1':
            echo 'This is some_feature with variant1';
            break;
        case 'variant2':
            echo 'This is some_feature with variant2';
            break;
    }
}

echo 'Hello World';

配置菜谱

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

完全启用的功能

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

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

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

Bucketing值'user'会导致基于已登录用户ID进行分桶。目前,如果用户未登录,我们将回退到通过UAID进行分桶,但这存在问题,因为这意味着用户可以登录或注销以切换桶。(我们可能将此分桶方案的行为更改为简单地禁用未登录用户的特征。)

最后,bucketing值'random'会导致每个请求独立分桶,这意味着同一用户在不同请求中将处于不同的桶中。这通常用于应该没有用户可见效果的功能,但我们想逐步推出像从master到shards的切换或新的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' 为真,且请求包含指定了待讨论功能变体的 features 查询参数,则使用该变体。 features 参数的值是特征列表,其中每个特征可以是仅特征名称,表示应使用变体 'on' 启用该特征,或者特征名称、冒号和变体名称。例如,请求 features=foo,bar:x,baz:off 将开启功能 foo,开启功能 bar 并使用变体 x,以及关闭功能 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. 根据需要提升、清理或重新实验。

一些样式指南

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

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

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

第三,作为一个检查你正确使用特征API的方法,每次你有一个测试是调用Feature::isEnabled的if块时,确保删除检查并保留代码或者删除检查和代码都是合理的。在由isEnabled检查保护的块中,不应该有需要保存的代码片段,如果移除了该功能。