船舶越快,风险越来越少。获得优化的推出,为开发人员提供免费功能标志。 建立免费账户

优化开发人员



在指南针上,我们正试图通过技术重新定义房地产行业。我们的工具由15k +房地产代理使用,我们的网络应用程序每天都被数百万用户使用。在产品和工程部门拥有超过300人,我们需要对每个功能发布进行仔细和高效。
为了释放满足用户需求的功能,我们需要能够打开和关闭功能,指定受众,测试不同的变体,并使用当前的应用程序轻松集成。

问题

Currently, Compass uses an internal service called “Experiments API” to roll out new features. It’s a simple service; it will provide either true or false given a user ID and an experiment name. Today, it makes decisions by:

  • 具有随机铲除用户的指定百分比
  • 强制启用/禁用通过电子邮件登录用户

随着我们的业务增长,我们开始看到以下需要:

  • 推出特征时更复杂的规则:可能是区域或用户角色
  • 一种更稳定的服务:由于其底层基础设施,当前的实验服务不可靠,并且其运行时事件正在以不可接受的速度发生
  • 测试功能变化的能力:我们需要通过与真正的用户验证我们的假设,“从现实中学习”。通过实验服务,所有数据分析都是按需完成的,我们需要更好的方法来分析功能。

谢天谢地,今天我们可以聘请第三方服务来处理我们的功能测试和卷展览,因此我们的工程师可以专注于最重要的是:特色本身! -

经过仔细考虑,我们决定优化,更具体地说 优化完整的堆栈 产品,由于其强大的功能推出和测试能力。它还需要比我们当前的内部实验服务更少的维护工作。

该项目

好的,现在这个问题是“我们如何开始使用它?”。

我们组建了一支小组,调查指南针的需求以及如何有优化的完整堆栈产品可以帮助这些用例。很快我们了解到,我们必须与以下“广泛”目标建立我们的内部整合:

  1. 主要任务:每一支愿意使用优化的团队将能够轻松实现
  2. 二级使命:退休自制实验API

并且非常重要的是,我们应该尽可能少,同时尽可能少的未来维护工作。

战略

为了实现我们的目标,我们有原则,我们将根据以下:

原则1:我们将尽可能多的决定。

我们看到我们需要早日了解,并且需要做出很多影响我们公司几乎所有团队的决策多年;这意味着我们根本不能依赖我们的第一个想法!早期,每次决定通常都在小白板课程中进行头脑风暴。我们在需要我们认为时,我们拥有许多规划课程。此外,当我们开始制作代码时,我们在同一台计算机上做了一切,在我们后来学到的是被称为 MOB编程。这种做法被证明是非常有价值的,我们在开始时对我们的代码质量非常有信心!

有趣的是,我们相信这并没有推迟我们的交货,相反 - 我们使我们同步的想法并以非常一致的速度前进。

原则2:通过引入太多的抽象,不要成为瓶颈

我们生产的一切都需要容易理解和易于使用。我们不想在其他团队中对其他团队进行大量认知负荷时,每当他们需要优化时都需要。

为实现这一目标,我们将尽量介绍尽可能少的额外代码。更多代码意味着在这些抽象需要更改时,将来将来出现更多的错误,更多的抽象和更多维护工作。我们计划花费大量时间与Opty用户了解他们的用例,因此我们可以确定集成所需的共享模块。

换句话说,我们希望通过提取创建抽象,而不是尝试预测未来的需求。以下将描述更多。

工作

在本节中,我们将探讨我们与指南针架构集成优化的完整堆栈的旅程。每个部分都将定义立即挑战,我们的解决方案以及来自所提出的解决方案的任何挑战。

挑战:每个项目都应该能够优化使用

解决方案: 在指南针中,有三种类型的客户可以优化地呼叫;有后端微服务,Web客户端和移动客户端。对于服务和Web客户端,优化提供 适用于多种语言的客户。客户的工作得很好:他们不断调查您的优化配置(称为数据文件),而且没有方法取决于外部呼叫,这是所有本地和快速的。优化还提供非常好的移动应用程序客户,我们决定直接使用我们的IOS和Android应用程序。

 

每个微服务和应用程序都会运行优化的客户端。

新问题: 我们的大多数后端服务都是通过GO写的,而且没有去客户! [去SDK. 公开可用01/10/2020。]

挑战:优化作为服务实现

解决方案: 下列的 优化的推荐,我们决定实现将运行优化客户端的内部服务。我们与Java一起去了,因为优化的团队告诉我们,这是他们最稳定和成熟的客户。此外,这是他们自己为他们的功能使用的客户,我们了解 如果制造商也是用户,对产品质量的影响!! [优化代理人 可用于将完整堆栈部署为微服务–公开可用03/30/2020。

在指南针,我们的所有后端服务都通过彼此通信 grpc.,我们有一个名为APIV3的服务,将所有GRPC服务暴露为HTTP端点(卡梅隆瓦尔德在QCON 2018谈论了我们的建筑,检查出来)。在此架构中,服务可以非常高效且快速,并且可以从后端和前端到达。此时,我们开始相信每个项目都将使用此服务,只有少数人需要实际实现客户自身以性能原因。

“优化作为服务”策略

“优化作为服务”策略

新问题:

  1. 定义一个好的界面:与服务交互的每个项目都将通过界面进行。一个糟糕的界面可以使每个其他项目更糟糕,并且介绍一个项目在这么多不同的地方使用了一旦项目使用。
    (退房 这本书由John Ousterhout 这解释了为什么良好的接口是创建良好软件设计的关键)
  2. 让服务生产:我们希望尽早运行持续的集成,我们也希望尽早处理Devops问题。这些问题通常涉及不同团队的优先事项,因此请求帮助和沟通早期是关键。
    (早期生产的好处是 在这个博客文章中概述了)

挑战:定义服务的界面

解决方案: 在我们的原则之后,我们决定这项服务将是一个优化的客户的“细包装”。对于每个客户端方法,我们将公开单个RPC /端点,具有相同的名称和相同的参数。在HTTP界面中,我们甚至创建了一个惯例:所有请求将是一个帖子,其中URL中所需的参数,身体中可选。
例如, 此呼叫来自客户端检查功能是否已启用功能

optimizelyClientInstance.isFeatureEnabled(
'price_filter', // feature key
'some_user_id', // id
{ some_attribute: 'some_value' }, // attributes
);

会转化为:

POST /opty/is_feature_enabled/price_filter/some_user_id
"{ "attributes": { "some_attribute": "some_value" } }"

这一实施只需要几行代码,正如我们所致的原则。我们不需要创建任何抽象,这意味着我们将对我们的用户施加小的认知负荷。如果有疑问,用户将能够直接咨询优化文档的奖金。

这个决定的唯一“骗局”是我们根本没有隐藏我们正在使用的所有事实,这可能导致一个 供应商锁定 问题。由于以下原因,我们决定承担风险:

  1. 无论如何,人们都会与仪表板进行互动,所以我们已经使用它已经明确了。我们没有计划抽象仪表板!
  2. 人们将希望优化最大限度地使用,以及我们在其顶部创建的任何抽象都需要考虑每个功能。这意味着我们的抽象至少与优化一样复杂,这绝对不与我们的原则同步。
  3. 如果我们决定改变供应商,我们将大概会产生一个大的破坏变革迁移。

所以“薄包装”抽象是!

现在我们在途中提供服务,一些未来的用户开始询问我们“我们应该为我们的功能推广和实验使用什么是Userid?”。事实证明,这不是一个简单的回答问题!

新问题: 我们应该发送哪些用户标识?

挑战:识别我们的用户

解决方案: most of our users will be using Optimizely to roll out features. That means the isFeatureEnabled method is going to be used a lot. Analyzing its arguments, it needs a featureKey and a userId.

该功能键易于识别,只要我们在仪表板中定义的任何东西就会。用户ID怎么样?我们经历了一些可能性:

  1. 数据库中的用户ID: 如果我们在页面或服务中,我们可以保证拥有一个(即,身份验证的路线)。这也可以保证相同的用户在不同的设备中看到相同的变体。但是,我们也想推出功能 登出 用户,所以我们需要使用不同的ID。
  2. 细分的匿名ID: we use 事件跟踪分部,它生成并为访问我们网站的每个用户生成并设置匿名ID。如果用户登录,此ID不会更改,这很好,因为我们不希望在用户验证后以不同方式切换功能。问题是一旦用户从网站/应用程序手动注销,那么段才会擦除匿名ID,我们最终会使用每个用户的多个匿名ID。
  3. 创建每个设备唯一的新ID: 创建一个新的ID可能是有问题的,因为我们必须更新许多应用程序和页面,但是这个拥有我们想要的所有属性,有些数据团队也表现出兴趣。
摘要三个ID可能性

摘要三个ID可能性

唯一的设备ID是获胜者,我们决定创建一个新的ID。在内部民意调查结束后,它被命名 高地ID(只有一个,永远存在一个,有趣的讨厌参考游戏 - )。如果它尚不存在,我们会在页面视图上生成此ID,并将其沿API请求作为自定义标题传递。

功能卷展栏看起来非常好!下一步将是支持 功能测试, which is when we actually compare two different variants of a feature with our users. Feature Tests in Optimizely are implemented in a similar way to Feature Rollouts, but we also need to send “success” events to Optimizely via the track method. Optimizely will then track how many impressions each variant has, and statistically, compare how many of those impressions generated these “success” events.

One reason why we chose Optimizely was because they offered Segment integration. Since we use it, we had the option to set up Optimizely as an event destination, so we won’t need to manually call Optimizely’s track method. This is great, it’对我们的用户来说甚至更少摩擦!但没有什么是容易的,对吧?事实证明,该段’S集成只有工作 如果我们使用段’用户ID或匿名ID.

新问题: 与细分集成。

挑战:细分和优化集成

解决方案: 软件开发中的所有内容都是一个权衡,所以经过仔细考虑,我们决定使用分部的匿名ID是一种可接受的代价来使用细分分部的集成。我们需要讽刺意味,“杀死”高地人ID。当我们仍然没有很多用户时,最好打破变化(但是这仍然是一个痛苦!)。

Next we needed to integrate Segment with Optimizely on the browser side, and it turned out it was also not that simple: Segment in the browser expects the page to be running a version of Optimizely client, and its integration is simply calling the track method in that client. None of our pages or JS apps have Optimizely’在他们身上运行的客户端,我们不想在团队中施加它。

经过经历后 段的源代码 and talk to Segment’s developer support, we came up with an interesting strategy to mitigate this: create a stub of the Optimizely client in the frontend that has the track method implemented as a fetch call to our service. It works like a charm, is easy to set up, and only adds a couple of kilobytes to our bundles!


// all our packages are prefixed @uc,
// and "opty" is Optimizely's nickname in our codebase
import {initOptyTrackInBrowser} from '@uc/opty';

// call this when running in the browser:
initOptyTrackInBrowser(window);

// this will create a "fake" version of the Optimizely client
// that Segment will call for every event triggered!
//
// window.optimizelyClientInstance = {
// track: (...args) = > {
// fetchCallToOptyService(transformArgs(args));
// }
// }

此时,我们已经与一些项目密切合作。显然,QA和测试的一个共同关注。用户想要运行应用程序并快速查看正在呈现的不同变体。最适合团队工作的策略是使用查询参数来切换功能和覆盖变量值,并在我们决定将其提取到共享节点中间件模块中的第三次之后实现。

新问题:我们应该如何测试或通过优化的应用程序进行测试?

挑战:启用测试和QA

在我们使用的指南针 koa. 对于我们的Web应用程序,我们在我们的应用中共享Koa中间字。我们的优化中间件最终结束:从我们的优化服务中获取功能和变量数据,并通过查询参数启用QA和测试。这是中间件中的示例:


import {
loadOpty,
VariableTypes,
FEATURE_ONLY
} from '@uc/opty';
// Initialize your Koa app...

// Setup the middleware:
// Pass a `features` dictionary. As values, pass either a dictionary of the
// desired Variables, or the `FEATURE_ONLY` value if you don't want
// to fetch any variables.
app.use(loadOpty({
features: {
'my_feature': {
'my_integer_variable': VariableTypes.INTEGER,
'my_string_variable': VariableTypes.STRING,
},
'my_other_feature': FEATURE_ONLY,
}
}))

// Now ctx.state.opty is populated:
app.use((ctx, next) => {
console.log(ctx.state.opty);
return next();
})
 
// Output:
// {
// features: {
// 'my_feature': true,
// 'my_other_feature': false
// },
// variables: {
// 'my_feature': {
// 'my_integer_variable': 123,
// 'my_string_variable': 'Hello World!'
// }
// }
// }

 

And the most important part: the middleware will override any feature or variable by adding a query parameter to the requested URL! For instance, we could override the my_integer_variable value by pointing our browser to www.mywebsite.com/some_route?opty_my_integer_variable=456.

什么顺利

  1. 交货时间。 四个月后,我们所有的项目都能够使用优化的方式推出和测试功能,并且在这篇文章写的时间上,它已经在三个不同的项目中用于生产!
  2. 一起学习。 我们为我们生产的代码的质量感到自豪,以及它带给不同的团队的好处。我们相信这是我们所有人计划,工作,大多数时间学习,并在需要时与其他专家联系的直接结果。
  3. 帮助其他团队。 请记住,我们谈到试图尽早获得生产的服务吗?好吧,我们最终陷入了很多障碍,不得不与许多不同的团队交谈。为了总结我们的服务进程和优化的调查结果,我们添加了以下项目以改善开发人员体验:
    一种。 我们项目的结果之一也是部署到生产的“样板”服务,CI和运行时监控设置,而其他团队则可以在通过同样的挑战时参考它。
    湾 托管每周办公时间和培训课程以回答任何问题,进行代码审查,对开发人员进行对编程进行优化集成。
    C。  创建了一个松弛频道,使开发人员更容易实现优化问题。
  4. 早期迭代界面。 即使我们在思考界面的思考时,事情也不会在第一次尝试时进展顺利。打破接口的变化是昂贵的,但我们很高兴能够尽早找到这些问题并更容易迭代它们。我们将此归因于我们选择的事实确切地接近几个项目,并在拥有多个用户之前找到问题。
  5. 没有技术债务。 产生的代码没有我们说“嗯,我们应该在我们有时间的时候重构这个 -

什么可能更好

  1. 更多规划和研究第三方集成。我们认为通过查看文档,可以轻松集成和段。事实证明,这不是,这可能导致我们项目中大部分延迟和早期破坏变化的问题。
  2. 与后端服务的更多集成。 由于我们是在我们的新Kubernetes集群上运行的第一个Java服务之一,因此有许多未知数;这为与我们的服务集成的后端服务创造了一些困难。虽然我们发布了FrontEnd应用程序的软件包和中间件,但后端呼叫者没有得到任何有用的模块来使用。

结论

我们认为该项目成功:发生了很大的意想不到的问题,但我们能够及时处理它们。我们的主要目标是实现的,我们对旧实验API具有明确的弃用策略,这是我们的二级使命。我们还期待将原则应用于指南针的不同项目。

如果您有兴趣加入指南针并解决新的挑战, 我们正在招聘!

优化X.