Non*_*-da 56 testing django ab-testing
我们刚刚开始为基于Django的项目进行A/B测试.我是否可以获得有关此A/B测试的最佳实践或有用见解的一些信息.
理想情况下,每个新的测试页面都将使用单个参数进行区分(就像Gmail一样).mysite.com/?ui=2应该给出不同的页面.因此,对于每个视图,我需要编写一个装饰器来根据'ui'参数值加载不同的模板.而且我不想在装饰器中硬编码任何模板名称.那么urls.py url模式将如何?
jb.*_*jb. 94
在深入研究代码之前,退一步并抽象A/B测试正在尝试做的事情是有用的.我们究竟需要进行什么测试?
考虑到这一点,让我们考虑实施.
目标
当我们考虑网络上的目标时,我们通常意味着用户到达某个页面或者他们完成了特定的操作,例如成功注册为用户或进入结帐页面.
在Django中,我们可以通过几种方式对其进行建模 - 可能在视图中天真地,在达到目标时调用函数:
def checkout(request):
a_b_goal_complete(request)
...
Run Code Online (Sandbox Code Playgroud)
但这没有用,因为我们必须在我们需要的任何地方添加代码 - 如果我们使用任何可插拔的应用程序,我们宁愿不编辑他们的代码来添加我们的A/B测试.
如何在不直接编辑视图代码的情况下引入A/B目标?中间件怎么样?
class ABMiddleware:
def process_request(self, request):
if a_b_goal_conditions_met(request):
a_b_goal_complete(request)
Run Code Online (Sandbox Code Playgroud)
这样我们就可以在网站的任何地方跟踪A/B目标.
我们怎么知道目标的条件得到满足?为了便于实现,我建议我们知道当用户到达特定的URL路径时,目标已满足条件.作为奖励,我们可以测量这一点,而不会让我们的手弄脏视图.回到我们注册用户的示例,我们可以说当用户到达URL路径时已达到此目标:
/注册完成
所以我们定义a_b_goal_conditions_met
:
a_b_goal_conditions_met(request):
return request.path == "/registration/complete":
Run Code Online (Sandbox Code Playgroud)
路径
在考虑Django中的Paths时,很自然地会跳到使用不同模板的想法.是否有另一种方式还有待探索.在A/B测试中,您在两个页面之间做出微小差异并测量结果.因此,最佳做法是定义单个基本路径模板,目标的所有路径都应该从该模板扩展.
应该如何呈现这些模板?装饰器可能是一个好的开始 - 在Django中最好的做法是在template_name
视图中包含一个参数,装饰器可以在运行时更改此参数.
@a_b
def registration(request, extra_context=None, template_name="reg/reg.html"):
...
Run Code Online (Sandbox Code Playgroud)
您可以看到这个装饰器要么内省包装函数,要么修改template_name
参数或从某个地方查找正确的模板(如模型).如果我们不想将装饰器添加到每个函数中,我们可以将其作为ABMiddleware的一部分来实现:
class ABMiddleware:
...
def process_view(self, request, view_func, view_args, view_kwargs):
if should_do_a_b_test(...) and "template_name" in view_kwargs:
# Modify the template name to one of our Path templates
view_kwargs["template_name"] = get_a_b_path_for_view(view_func)
response = view_func(view_args, view_kwargs)
return response
Run Code Online (Sandbox Code Playgroud)
我们还需要添加一些方法来跟踪哪些视图有A/B测试运行等.
用于沿路径发送观看者的系统
理论上这很容易,但是有很多不同的实现,所以不清楚哪一个是最好的.我们知道,一个好的系统应该分为用户均匀地沿着小路 - 一些散列法必须使用 - 也许你可以使用内存缓存计数器由路径的数目除以模 - 也许有更好的方法.
用于记录测试结果的系统
我们需要记录多少用户去了什么路径 - 当用户到达目标(我们需要能够说他们就什么路径到满足目标的条件),我们也需要访问这些信息 - 我们将使用某种模型来记录数据,并使用Django Sessions或Cookies来保留路径信息,直到用户满足目标条件.
闭幕思考
我已经给了很多的伪码实现在Django A/B测试 - 上面是决不是一个完整的解决方案,但对为在Django A/B测试可重复使用的框架,一个良好的开端.
作为参考,你可能想看看Paul Mar在GitHub上的七分钟A/B - 这是上面的ROR版本! http://github.com/paulmars/seven_minute_abs/tree/master
更新
关于Google网站优化工具的进一步反思和调查,很明显上述逻辑存在漏洞.通过使用不同的模板来表示路径,您可以破坏视图上的所有缓存(或者如果缓存视图,它将始终提供相同的路径!).相反,使用Paths,我会窃取GWO术语并使用Combinations
- 这是模板更改的一个特定部分 - 例如,更改<h1>
站点的标记.
该解决方案将涉及模板标记,这些模板标记将呈现为JavaScript.当页面在浏览器中加载时,JavaScript向您的服务器发出请求,该请求将获取其中一个可能的组合.
这样,您可以在保留缓存的同时每页测试多个组合!
更新
仍然存在模板切换的空间 - 例如,您引入了一个全新的主页,并希望在旧主页上测试它的性能 - 您仍然希望使用模板切换技术.需要记住的是,您必须找到一些方法来切换页面的X个缓存版本.为此,您需要覆盖标准缓存中间件,以查看它们是否是在请求的URL上运行的A/B测试.然后它可以选择正确的缓存版本来显示!
更新
使用上面提到的想法,我实现了一个可插拔的应用程序,用于基本的A/B测试Django.你可以把它从Github上拿下来:
http://github.com/johnboxall/django-ab/tree/master
如果您使用像suggsted(?ui=2
)那样的GET参数,那么您根本不必触摸urls.py.你的装饰师可以检查request.GET['ui']
并找到它需要的东西.
为了避免硬编码模板名称,也许你可以从视图函数中包装返回值?您可以返回一个元组(template_name, context)
并让装饰器破坏模板名称,而不是返回render_to_response的输出.这样的事怎么样?警告:我还没有测试过这段代码
def ab_test(view):
def wrapped_view(request, *args, **kwargs):
template_name, context = view(request, *args, **kwargs)
if 'ui' in request.GET:
template_name = '%s_%s' % (template_name, request.GET['ui'])
# ie, 'folder/template.html' becomes 'folder/template.html_2'
return render_to_response(template_name, context)
return wrapped_view
Run Code Online (Sandbox Code Playgroud)
这是一个非常基本的例子,但我希望它可以得到这个想法.您可以修改有关响应的其他几个内容,例如向模板上下文添加信息.您可以使用这些上下文变量与您的网站分析集成,例如Google Analytics.
作为奖励,如果你决定停止使用GET参数并转移到基于cookie等的东西,你可以在将来重构这个装饰器.
更新如果您已经编写了大量视图,并且您不想全部修改它们,那么您可以编写自己的版本render_to_response
.
def render_to_response(template_list, dictionary, context_instance, mimetype):
return (template_list, dictionary, context_instance, mimetype)
def ab_test(view):
from django.shortcuts import render_to_response as old_render_to_response
def wrapped_view(request, *args, **kwargs):
template_name, context, context_instance, mimetype = view(request, *args, **kwargs)
if 'ui' in request.GET:
template_name = '%s_%s' % (template_name, request.GET['ui'])
# ie, 'folder/template.html' becomes 'folder/template.html_2'
return old_render_to_response(template_name, context, context_instance=context_instance, mimetype=mimetype)
return wrapped_view
@ab_test
def my_legacy_view(request, param):
return render_to_response('mytemplate.html', {'param': param})
Run Code Online (Sandbox Code Playgroud)