Rails 微信用户访问统计及渠道推广力统计

yue · 2015年11月22日 · 最后由 martin91 回复于 2015年11月23日 · 3426 次阅读

背景和需求

加入访问统计模块,完成用户行为的分析,是面向 C 端用户必不可少的功能。现在市面上也有不少现成的访问监控统计工具,如百度统计和 Google 分析,都提供了很不错的功能。不过短板在于,百度统计定制化程度低,展示数据不够直接,不能直接满足市场运维人员的查看分析需要。谷歌问题是被墙,访问不方便。因此有必要内部开发这样的模块。分析下来,原理和实现都不困难,获得的效果也很显著。

需求如下:

  • 针对不同的功能,分别统计该功能下的推广页(首页)PV,UV,新用户 UV,以及其他跳转页面的访问情况
  • 统计不同功能的用户转发,分享,取消转发情况
  • 便于给新功能添加访问统计模块
  • 提供可视化界面来实时查看各功能下的用户访问情况

设计和技术选型

按照上面提出的需求,可以转化为下面的用例 (User Story):

  • 记录用户的每次访问,需要知道用户访问是什么功能,访问时间,上游链接,访问参数
  • 记录用户对自有公众号的关注情况(仅针对微信用户)
  • 实现统计的算法和统计数据的展示

由于功能都限制在微信浏览器下使用,因此借助 微信授权机制 来创建系统用户。在这里需要定义用户模型:visitor,这个模型目前仅保存了用户的微信 openid,后期有需要可以添加更多属性。

那么如何保存用户的访问记录呢?可以使用 impressionist。它把访问数据存到一个叫做 impression 的模型中,属性包括了:controller_name,action_name(这两个属性可以用来确定用户访问的是哪个功能),request_hash,session_hash,ip_address,message,referrer,params(访问的参数)。满足了目前我们保存数据的需求。不过它也有短板,在后面会提到。

界面的展示借助了 active admin ,定制化程度高,上手快,view DSL 够用。下图是最后出来的效果。统计了整体的访问情况和最近一周的数据。

实现 controller 层逻辑

考虑到这是一个通用功能,是属于 before_action 的行为,所以把逻辑提出到一个 controller concern 中。

# 在控制器中调用
class FeatureController < ApplicationController
  include Concerns::ImpressionConcern
  before_action :checkin_wechat_visitor # 识别当前微信用户
  before_action :trace_wechat_visitor, only: [:index] # 仅记录 index 的访问
end

下面是在 concern 中的逻辑,主要是实现微信用户识别和记录访问请求。

# controllers/concern/impression_concern.rb
def checkin_wechat_visitor # 需要通过微信用户的 openid 来识别用户
  if !(session.has_key?(:openid) && session.has_key?(:subscribe))
    redirect_to wechat_authorize_url(authentication_check_subscription_url) and return
  end
  checkin
end

def trace_wechat_visitor # 记录用户访问情况
  impressionist(visitor, impression_message) and return if visitor
end

private
  def checkin
    @visitor = Visitor.find_or_create_by(openid: session[:openid])
    @subscribe = session[:subscribe]
  end

实现 active admin 逻辑

首先使用 active admin Arbre 中的 panel 和 table_for 来绘制统计表格。在这里 FeatureStatistician 是实现统计算法的模块。通过调用 calculate 方法返回过去 7 的各个指标下的访问情况。再传递到 table_for 中实现遍历输出。当有其他的功能模块需要统计的时候,就是添加一个新的 panel 和对应的 Statistician 就行。

# models/admin/dashboard.rb
panel "速查统计" do
  statistician = Utilities::Admin::FeatureStatistician.new
  stat = statistician.calculate
  table_for stat do
    column "日期" do |item| 
      item[:captain].is_a?(String) ? item[:captain] : item[:captain].strftime("%Y-%-m-%d")
    end
    column "PV" do |item| item[:page_view] end
    column "UV" do |item| item[:wechat_visitor] end
    column "新用户(未关注)" do |item| item[:unsubscribe_visitor] end
    column "点击收藏" do |item| item[:click_collect_count] end
    column "查看收藏夹" do |item| item[:click_collection_count] end
    column "转发朋友" do |item| item[:friend_share_count] end
    column "转发朋友圈" do |item| item[:circle_share_count] end
    column "取消转发" do |item| item[:share_cancel_count] end
  end
end

实现 Statistician 逻辑

Statistician 中的逻辑很直接,就是提取做数据库的聚合。比如说计算过去 7 天的 PV:

# lib/utilities/admin/feature_statistician.rb
def pv_in_last_week
  Impression.where(controller_name: 'feature', action_name: 'index')
    .where("created_at::date >= ?", 7.day.ago.to_date)
    .group("to_char(created_at, 'YYYY-MM-dd')")
    .select("to_char(created_at, 'YYYY-MM-dd') as date, count(id) as count")
    .map{|i| [i.date, i.count] }
end

设计渠道推广力统计功能(省略实现)

功能会有不同的渠道合作伙伴进来推广,针对不同的推广方式,需要查看此方式带来的用户访问量。建模是定义了 channel 和 advertisement 两个表格。上面的 UML 图就修改为下图。后期传播出去的 url 都会带上推广方式标示,这样就能识别进入系统是通过哪个渠道。

impression 的问题

Impression 增加了 params 字段,可以保存请求携带的参数。在统计过程中往往需要匹配参数。可是 gem 把 params 定义为 text 类型,然后仅是存了序列化的 ActiveSupport::HashWithIndifferentAccess,这样就不能借助 sql 自带的查找方法来做聚合,只能通过 ruby 逻辑。

PostgreSQL 提供了 hstore 机制,可以在 hash 中做查找。若 impressionist 能针对这个做改进会更好。

Impressionist

看了一下 impressionist 的实现,感觉略粗糙,其实完全可以抛弃这个 gem,没提供什么(有用的)功能。

默认created_at上居然没加索引,感觉这个 gem 作者设计的时候就没有考虑要通过时间去过滤(比如最近 7 天)这种场景。自己加的话 pg 需要用 Expression Indexes ,mysql 的话可能要再冗余出一个 date 列才能满足即在 datetime 又在 date 上过滤的需求了...

再加上你提到的 params 存成 text 很快就会让这个功能变得不可用,因为这种场景下,impression 表行数增长的是非常快的,如果要全表查出来用 Ruby 过滤死的会非常惨,存成 jsonb 再加索引是不错的选择。

impression 里用了很多 counter cache 去做预聚合,其实对于多维分析(时间、渠道)场景也是没什么用处的...多维分析还得去在查询上优化。

Google Analytics Or Piwik

这种需求似乎可以用 GA 的 custom dimension 或 event tracking + custom report 来实现,不知百度统计有没有对应的。如果考虑 GA 被墙,自己搭piwik也不错,但略重。

Fluentd + Elastic

最近尝试通过 Fluentd + Elastic 来反爬,原理就是通过找出某段时间内访问频次高的 IP,再结合这个 IP 的轨迹和 UA 判断这个 IP 是谷歌爬虫还是野爬虫。Fluentd 支持实时发送 log 到 elastic,也可以异步从 log file 里提取数据再批量 import 到 elastic。

当然,这个架构对多维分析场景仍然适用。

看不懂,没有头,没有尾。

#1 楼 @hooopo 看了你的分析更确定 impression 这个 gem 是鸡肋。等到数据量大了,查找和聚合性能都是问题。如果要修改 gem,也不是那么直接。不过目前暂时够用。多场景的问题会再去研究。

#2 楼 @u1442016572 本着交流学习的目的发的文章。如果能指出文中哪里不清晰,技术使用不当等问题,那肯定更有建设性。

先收藏再看,太晚了,得先睡觉了,明天再拜读

#5 楼 @martin91 马婷同学太刻苦了。

终于拜读完了,条理比较清晰,不过觉得有几点或许可以成为建议。

  1. 统计功能其实最容易遇到的就是性能问题,我看你实现统计的代码是每次都从数据库查过去七天的记录,然后再 group by,这块可以建议用一个定时任务每天 0 点之后去统计前一天的数据,最后再把统计结果写到数据库,后边业务上只需要查统计好后的 PV UV 数据就好了,这种方式适合需求比较固定的场景,比如每天都想看过去的 PV, UV, 而且这些数据本来就是静态数据,适合先统计好,原始数据仅作统计结果校验。另一个不建议每次查看的时候去查原始数据的原因是,当你的数据量开始多了之后(几百万甚至几千万),这样的查询可能会明显慢下来。
  2. 看你的描述,如果 Impression 只是用 varchar 存 hash 的话,这样必然没法索引,那这样 impression 其实没太大必要,毕竟这些东西我自己也可以写一个 concern,然后专门提取这些信息然后一股脑塞进数据库就行了。但是这样的话,以后如果需要做一些数据分析的时候就会非常痛苦了,比如哪天运营找你说:“嘿,帮我看看昨天有多少 PV 或者 UV 是从 xx 推广过来的,还有这些里边有多少转化成为新用户”,然后可能过多几天,运营又会找你说:“帮我看看昨天有多少来自广州的用户”。这些场景下,可能结合一些索引工具会更好,比如 Elastic Search、Solr 这些,不过这样又会增加复杂度。或许像你说的 Postgresql 会是更好的选择,又或者 NoSQL 数据库,anyway,只是一个设想,我自己也没实践过。
  3. 你的那些 PV UV 数据或许用图表展示会更直观生动。

哈哈,说得一般,求轻拍。

#7 楼 @martin91

  • 第一点建议提得正好。等到数据量大了会这样采用。之前也这样处理过数据。不过发现其实没有需求。
  • 原始数据还是要存的。结合你和 hooopo 的建议后期再优化处理。表示没有用过 Elastic Search、Solr。不知道学习曲线如何。
  • 图表是有计划的,考虑是 highchart。

我给你的回复打全五分好评。

#8 楼 @yue 第二点提到的两个索引工具请先忽略,太重,而且都是 Java 系,安装运行略麻烦,可能也不一定适合,我主要是用在一些复杂搜索以及索引,没仔细研究过是否符合你的需求。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号