cafemedia / feature
基于 Etsy 的 PSR-4 兼容的 Feature Flags 库
Requires
- php: >=5.6
This package is not auto-updated.
Last update: 2024-09-24 18:38:00 UTC
README
需要 PHP 5.6 及以上版本。
安装
composer require cafemedia/feature
使用
$config = [ 'testFeature' => [ 'description' => 'this is the description of the test feature', 'enabled' => [ 'variant1' => 100, //100% chance this variable will be chosen 'variant2' => 0 ], ] ]; $feature = (new Feature($config))->addUser([ 'user-uaid' => 'unique identifier', //required 'user-id' => 'logged in user ID', // if applicable 'user-name' => 'logged in user name' // if applicable ]); $feature->isEnabled('testFeature'); // true $feature->variant('variant1'); // true $feature->variant('description'); // 'this is the description of the test feature'
待办事项
文档!!!!删除 Etsy 的存档文档,并用新的文档替换。增加更多测试。添加更多分桶方案。
以下所有内容均为存档,仅供参考。
这是一个存档项目
Feature 已经不再积极维护,并且不再与 Etsy 内部使用的版本同步。
Feature API
Etsy 的 Feature 标志 API 用于操作加速和 A/B 测试。
Feature 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->('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 的前一个典型用例是,如果我们想改变处理列表的方式,而不是仅对某些用户启用功能,而是对所有用户看到的列表启用功能,但我们想对所有用户启用功能,但只对部分列表启用,那么我们可以使用 isEnabledFor
和 variantFor
,并传入表示列表所有者的用户对象。这将使我们能够为特定的列表所有者启用功能。`bucketingBy` 方法具有类似的作用,除非没有相关的用户或我们不想总是将同一用户放入相同的桶中。因此,如果我们想为显示的所有列表的 10% 启用某个功能,独立于请求用户和所有者用户,我们可以使用 isEnabledBucketingBy
,并将列表 ID 作为分桶 ID。
一般来说,您更有可能想使用简单的 isEnabled
和 variant
方法。
对于Smarty模板,由于静态方法无法直接调用,在Tpl.php中已经创建了一个对象$feature
,它暴露了与Feature API相同的四个方法,但作为实例方法,例如
{% if $feature->isEnabled("my_feature") %}
配置指南
存在许多常见的配置,因此在解释功能配置子句的完整语法之前,这里列出了一些更常见的案例以及编写配置的最简洁方式。
完全启用的功能
$server_config['foo'] = ['enabled' => 100];
完全禁用的功能
$server_config['foo'] = ['enabled' => 0];
为所有人启用获胜变体的功能
$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 饼干进行分桶,这意味着用户将处于相同的桶中,无论他们是否已登录。
分桶值 'user'
导致基于已登录用户 ID 进行分桶。目前,如果用户未登录,我们会回退到通过 UAID 进行分桶,但这存在问题,因为这意味着用户可以通过登录或注销来切换桶。 (我们可能会改变这种分桶方案的行为,使其仅针对未登录的用户禁用功能。)
最后,分桶值 '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或错误配置功能的方式可能会被检测并记录下来。(其中一些目前尚未检测到,但将来可能会。)
-
为单个变体功能调用
$feature->variant
。 -
在未被
$feature->isEnabled
检查保护的代码中调用$feature->variant
。 -
在多变体功能中将
'on'
作为变体名称包括在内。 -
将
'enabled'
设置为小于0或大于100的数值。 -
将变体的百分比值设置为小于0或大于100的值。
-
将
'enabled'
设置为使得变体百分比的和大于100。 -
将
'enabled'
设置为非数字、非字符串、非数组值。 -
当
'enabled'
是一个数组时,将'users'
或'groups'
属性设置为包含'enabled'
中不是键的键的数组。 -
当
'enabled'
是一个数组时,将'admin'
或'internal'
属性设置为不是'enabled'
中的键的值。
功能的生命周期
功能API的设计旨在使其更容易地通过一个可预测的生命周期来推动功能的发展,其中功能可以轻松创建、逐步推出、进行A/B测试,然后通过将其提升为完全功能的功能标志、删除配置和相关功能检查但保留代码或完全删除代码来进行清理。
功能的基本生命周期可能看起来像这样
-
开发者编写了一些受
Feature::isEnabled
检查保护的代码。为了在开发中测试功能,他们将在development.php
中为功能添加配置,为特定用户或管理员启用它,或将'enabled'
设置为0,以便他们可以通过URL查询参数进行测试。 -
在某个时候,开发者将为
production.php
添加一个配置段。最初这可能只是一个占位符,完全禁用功能,或者它可能为管理员等启用它。 -
一旦功能完成,
production.php
配置将更改为启用少量用户的功能进行操作烟雾测试。对于单个变体功能,这意味着将'enabled'
设置为一个小数值;对于多变体功能,这意味着将'enabled'
设置为一个数组,该数组指定每个变体的一个小百分比。 -
在逐步推出期间,暴露于功能中的用户百分比可能会上下移动,直到开发人员和运维人员确信代码已经完全准备好。如果在任何点上出现严重问题,可以通过将启用设置为
'off'
来完全禁用新代码。 -
如果该功能将作为A/B实验的一部分,那么开发人员将与数据团队合作,确定向哪些用户展示该功能以及实验需要运行多长时间才能收集到良好的实验数据。为了启动实验,生产配置将更改以启用相应百分比的用户的功能或其变体。在此之后,直到实验完成,百分比应保持不变。
此时可能会发生几件事情:如果实验显示出明确的赢家,我们可能只想保留代码,可能将其置于顶级功能标志的控制之下,以便运维人员可以根据运营原因禁用该功能。或者我们可能想要丢弃与该功能相关的所有代码。或者我们可能想根据我们从这次实验中学到的经验运行另一个实验。以下是在这些情况下会发生的事情:
在不创建顶级功能标志的情况下,将功能作为网站永久部分
-
将功能配置的值更改为获胜变体的名称(对于单变体功能为
'on'
)。 -
删除实现其他变体的任何代码,并删除对
Feature::variant
和任何相关条件逻辑(例如基于变体名称的开关)的调用。 -
删除对
$feature->isEnabled
的检查,但保留受其保护的代码。 -
删除功能配置。
为了通过完整的功能标志控制功能。即对于通常会被启用但希望保留通过简单配置更改关闭的能力的东西。
-
将功能配置的值更改为获胜变体的名称(对于单变体功能为
'on'
)。 -
删除实现其他变体的任何代码,并删除对
$feature->variant
和任何相关条件逻辑(例如基于变体名称的开关)的调用。 -
添加一个以
feature_
前缀命名的新的配置,并将其值设置为'on'
。 -
将旧标志名的所有
Feature::isEnabled
检查更改为新功能标志。 -
删除旧配置。
要完全删除功能
-
将功能配置的值更改为
'off'
。 -
删除所有由
Feature::isEnabled
检查保护的代码,然后删除检查。 -
删除功能配置。
根据相同代码运行新实验
-
将功能配置的启用值设置为
'off'
。 -
创建一个新的功能配置,名称类似,但以_vN结尾,其中N是2(如果是第二个实验),3(如果是第三个实验)。将其设置为
'off'
。 -
将旧功能的所有
Feature::isEnabled
检查更改为新功能。 -
删除旧配置。
-
实现新实验所需的变化,根据需要删除旧变体并添加新变体。
-
正常进行增长和A/B测试新功能。
-
根据适当的情况进行推广、清理或重新实验。
一些风格指南
为了使推动功能通过此生命周期更容易,有一些编码指南需要遵守。
首先,Feature方法(isEnabled
、variant
、isEnabledFor
和variantFor
)的功能名称参数应始终为字符串字面量。这将使找到检查特定功能的所有位置变得更容易。如果您发现自己正在运行时创建功能名称然后进行检查,那么您可能正在滥用Feature系统。在这种情况下,您可能并不真的想使用Feature API,而是用一些普通的旧配置数据驱动代码。
其次,特征方法的结果不应被缓存,例如通过调用一次Feature::isEnabled
并将结果存储在某些控制器的实例变量中。特征机制已经缓存了它所做计算的结果,因此只需在需要时简单地调用Feature::isEnabled
或Feature::variant
就足够快了。这又将有助于找到依赖于特定特征的位置。
第三,为了检查你是否正确使用Feature API,每当你有条件为Feature::isEnabled
调用的if块时,确保移除检查并保留代码或删除检查和代码都将是合理的。在由isEnabled检查保护的块内,不应该有需要保留的代码片段,以防该特征被删除。