All Files
(78.67%
covered at
7.07
hits/line)
10 files in total.
286 relevant lines.
225 lines covered and
61 lines missed
-
1
require 'redis'
-
1
require 'browser'
-
1
require 'geoip'
-
-
1
require 'redis_analytics'
-
-
1
require 'redis_analytics/version'
-
1
require 'redis_analytics/time_ext'
-
1
require 'redis_analytics/filter'
-
1
require 'redis_analytics/configuration'
-
1
require 'redis_analytics/metrics'
-
1
require 'redis_analytics/visit'
-
1
require 'redis_analytics/helpers'
-
1
require 'redis_analytics/tracker'
-
1
require 'redis_analytics/engine'
-
-
1
module RedisAnalytics
-
1
extend Configuration
-
end
-
1
module RedisAnalytics
-
1
module Configuration
-
# Redis connection instance
-
1
attr_accessor :redis_connection
-
-
# Redis namespace for keys
-
1
attr_writer :redis_namespace
-
-
# Name of the cookie which tracks first visitors
-
1
attr_writer :first_visit_cookie_name
-
-
# Name of the cookie which tracks current visits
-
1
attr_writer :current_visit_cookie_name
-
-
# Minutes the visit should timeout after (if no hit is received)
-
1
attr_writer :visit_timeout
-
-
# Endpoint for dashboard
-
1
attr_writer :dashboard_endpoint
-
-
# Endpoint for api
-
1
attr_writer :api_endpoint
-
-
1
attr_writer :path_filters
-
-
1
attr_writer :filters
-
-
# Path to the Geo IP Database file
-
1
attr_writer :geo_ip_data_path
-
-
# Redis namespace for keys
-
1
def redis_namespace
-
6
@redis_namespace ||= 'ra'
-
end
-
-
# Minutes the visit should timeout after (if no hit is received)
-
1
def visit_timeout
-
6
@visit_timeout ||= 30 # minutes
-
end
-
-
# Name of the cookie which tracks first visitors (unknown visitors)
-
1
def first_visit_cookie_name
-
22
@first_visit_cookie_name ||= '_rucn'
-
end
-
-
# Name of the cookie which tracks visits
-
1
def current_visit_cookie_name
-
29
@current_visit_cookie_name ||= '_vcn'
-
end
-
-
1
def filters
-
6
@filters ||= []
-
end
-
-
1
def path_filters
-
6
@path_filters ||= []
-
end
-
-
1
def add_filter(&proc)
-
1
filters << RedisAnalytics::Filter.new(proc)
-
end
-
-
1
def add_path_filter(path)
-
1
path_filters << RedisAnalytics::PathFilter.new(path)
-
end
-
-
1
def geo_ip_data_path
-
3
@geo_ip_data_path = ::File.expand_path(::File.join(::File.dirname(__FILE__),'..','..')) + "/bin/GeoIP.dat"
-
end
-
-
1
def visitor_recency_slices
-
2
@visitor_recency_slices ||= [1, 7, 30]
-
end
-
-
1
def default_range
-
3
@default_range = :day
-
end
-
-
1
def redis_key_timestamps # [format, expire in seconds or nil]
-
40
['%Y', '%Y_%m', '%Y_%m_%d', '%Y_%m_%d_%H', '%Y_%m_%d_%H_%M']
-
end
-
-
1
def time_range_formats
-
2
[[:year, :month, "%b"], [:week, :day, "%a"], [:day, :hour, "%l%P"]]
-
end
-
-
1
def configure
-
6
yield self
-
end
-
-
1
def api_endpoint
-
@api_endpoint || dashboard_endpoint + '/api'
-
end
-
-
1
def dashboard_endpoint
-
@dashboard_endpoint || '/redis_analytics'
-
end
-
-
end
-
end
-
-
1
require 'rails'
-
1
require 'jquery-rails'
-
-
1
module RedisAnalytics
-
1
module Dashboard
-
1
class Engine < ::Rails::Engine
-
1
isolate_namespace RedisAnalytics
-
-
1
initializer "redis_analytics.middleware" do |app|
-
app.config.app_middleware.use "RedisAnalytics::Tracker"
-
end
-
-
1
initializer "redis_analytics.view_helpers" do |app|
-
ActionController::Base.send :include, RedisAnalytics::Helpers
-
end
-
-
1
initializer "redis_analytics.precompile.hook", group: :all do |app|
-
app.config.assets.precompile += %w[
-
redis_analytics/redis_analytics.js
-
redis_analytics/redis_analytics.css
-
]
-
end
-
end
-
end
-
end
-
1
module RedisAnalytics
-
1
class Filter
-
1
attr_reader :filter_proc
-
-
1
def initialize(filter_proc)
-
2
@filter_proc = filter_proc
-
end
-
-
1
def matches?(request, response)
-
5
filter_proc.call(request, response)
-
end
-
-
end
-
-
1
class PathFilter
-
1
attr_reader :filter_path
-
-
1
def initialize(filter_path)
-
3
@filter_path = filter_path
-
end
-
-
1
def matches?(request_path)
-
6
if filter_path.is_a?(String)
-
5
request_path == filter_path
-
1
elsif filter_path.is_a?(Regexp)
-
1
request_path =~ filter_path
-
end
-
end
-
end
-
-
end
-
-
1
module RedisAnalytics
-
1
module Helpers
-
-
1
FORMAT_SPECIFIER = [['%Y', 365], ['%m', 30], ['%d', 24], ['%H', 60], ['%M', 60]]
-
-
1
GRANULARITY = ['yearly', 'monthly', 'dayly', 'hourly', 'minutely']
-
-
1
private
-
1
def method_missing(meth, *args, &block)
-
if meth.to_s =~ /^(minute|hour|dai|day|month|year)ly_([a-z_0-9]+)$/
-
granularity = ($1 == 'dai' ? 'day' : $1) + 'ly'
-
metric_name = $2
-
data(granularity, metric_name, *args)
-
else
-
super
-
end
-
end
-
-
1
def metric_type(metric_name)
-
RedisAnalytics.redis_connection.hget("#{RedisAnalytics.redis_namespace}:#METRICS", metric_name)
-
end
-
-
1
def data(granularity, metric_name, from_date, options = {})
-
aggregate = options[:aggregate] || false
-
x = granularity[0..-3]
-
-
to_date = (options[:to_date] || Time.now).send("end_of_#{x}")
-
i = from_date.send("beginning_of_#{x}")
-
-
union = []
-
time = []
-
begin
-
slice_key = i.strftime(FORMAT_SPECIFIER[0..GRANULARITY.index(granularity)].map{|x| x[0]}.join('_'))
-
union << "#{RedisAnalytics.redis_namespace}:#{metric_name}:#{slice_key}"
-
time << slice_key.split('_')
-
i += 1.send(x)
-
end while i <= to_date
-
seq = get_next_seq
-
if metric_type(metric_name) == 'String'
-
if aggregate
-
union_key = "#{RedisAnalytics.redis_namespace}:#{seq}"
-
RedisAnalytics.redis_connection.zunionstore(union_key, union)
-
RedisAnalytics.redis_connection.expire(union_key, 100)
-
return Hash[RedisAnalytics.redis_connection.zrange(union_key, 0, -1, :with_scores => true)]
-
else
-
return time.zip(union.map{|x| Hash[RedisAnalytics.redis_connection.zrange(x, 0, -1, :with_scores => true)]})
-
end
-
elsif metric_type(metric_name) == 'Fixnum'
-
if aggregate
-
return RedisAnalytics.redis_connection.mget(*union).map(&:to_i).inject(:+)
-
else
-
return time.zip(RedisAnalytics.redis_connection.mget(*union).map(&:to_i))
-
end
-
else
-
if Metrics.public_instance_methods.any?{|m| m.to_s =~ /^#{metric_name}_ratio_per_(hit|visit)$/}
-
aggregate ? {} : time.zip([{}] * time.length)
-
elsif Metrics.public_instance_methods.any?{|m| m.to_s =~ /^#{metric_name}_count_per_(hit|visit)$/}
-
aggregate ? 0 : time.zip([0] * time.length)
-
else
-
aggregate ? 0 : time.zip([0] * time.length)
-
end
-
end
-
end
-
-
1
def get_next_seq
-
seq = RedisAnalytics.redis_connection.incr("#{RedisAnalytics.redis_namespace}:#SEQUENCER")
-
end
-
-
1
def time_range
-
(request.cookies["_rarng"] || RedisAnalytics.default_range).to_sym
-
end
-
end
-
end
-
-
1
module RedisAnalytics
-
1
module Metrics
-
-
1
attr_reader :visit_time_count_per_visit
-
1
attr_reader :visits_count_per_visit, :first_visits_count_per_visit, :repeat_visits_count_per_visit
-
1
attr_reader :unique_visits_ratio_per_visit
-
1
attr_reader :page_views_count_per_hit, :second_page_views_count_per_hit
-
-
# Developers can override or define new public methods here
-
# Methods should start with track and end with count or types
-
# Return types should be Fixnum or String resp.
-
# If you return nil or an error nothing will be tracked
-
-
1
def browser_ratio_per_visit
-
3
user_agent.name.to_s
-
end
-
-
1
def platform_ratio_per_visit
-
3
user_agent.platform.to_s
-
end
-
-
1
def country_ratio_per_visit
-
3
if defined?(GeoIP)
-
3
begin
-
3
g = GeoIP.new(RedisAnalytics.geo_ip_data_path)
-
3
geo_country_code = g.country(@rack_request.ip).to_hash[:country_code2]
-
3
if geo_country_code and geo_country_code =~ /^[A-Z]{2}$/
-
return geo_country_code
-
end
-
rescue Exception => e
-
warn "Unable to fetch country info #{e}"
-
end
-
end
-
end
-
-
1
def recency_ratio_per_visit
-
# tracking for visitor recency
-
3
if @last_visit_time # from first_visit_cookie
-
days_since_last_visit = ((@t.to_i - @last_visit_time.to_i)/(24*3600)).round
-
if days_since_last_visit <= 1
-
return 'd'
-
elsif days_since_last_visit <= 7
-
return 'w'
-
elsif days_since_last_visit <= 30
-
return 'm'
-
else
-
return 'o'
-
end
-
end
-
end
-
-
1
def device_ratio_per_visit
-
3
return ((user_agent.mobile? or user_agent.tablet?) ? 'mobile' : 'desktop')
-
end
-
-
1
def referrer_ratio_per_visit
-
3
if @rack_request.referrer
-
['google', 'bing', 'yahoo', 'cleartrip', 'github'].each do |referrer|
-
# this will track x.google.mysite.com as google so its buggy, fix the regex
-
if m = @rack_request.referrer.match(/^(https?:\/\/)?([a-zA-Z0-9\.\-]+\.)?(#{referrer})\.([a-zA-Z\.]+)(:[0-9]+)?(\/.*)?$/)
-
"REFERRER => #{m.to_a[3]}"
-
referrer = m.to_a[3]
-
else
-
referrer = 'other'
-
end
-
end
-
else
-
3
referrer = 'organic'
-
end
-
3
return referrer
-
end
-
-
# track the ratio of URL's visits
-
1
def url_ratio_per_hit
-
4
return @rack_request.path
-
end
-
-
# track the landing pages ratio
-
1
def landing_page_ratio_per_hit
-
4
return @rack_request.path if @page_view_seq_no.to_i == 0
-
end
-
-
# track the landing pages ratio
-
1
def http_response_ratio_per_hit
-
4
return @rack_response.status.to_s
-
end
-
-
1
private
-
1
def user_agent
-
12
Browser.new(:ua => @rack_request.user_agent, :accept_language => 'en-us')
-
end
-
-
end
-
end
-
-
1
require 'date'
-
1
module TimeExtensions
-
-
1
def end_of_minute
-
Time.local(self.year, self.mon, self.day, self.hour, self.min, 59)
-
end
-
-
1
def beginning_of_minute
-
Time.local(self.year, self.mon, self.day, self.hour, self.min, 0)
-
end
-
end
-
-
1
Time.send :include, TimeExtensions
-
# -*- coding: utf-8 -*-
-
1
require 'digest/md5'
-
1
module RedisAnalytics
-
1
class Tracker
-
-
1
def initialize(app)
-
2
@app = app
-
end
-
-
1
def call(env)
-
4
dup.call!(env)
-
end
-
-
1
def call!(env)
-
4
@env = env
-
4
@request = Rack::Request.new(env)
-
4
status, headers, body = @app.call(env)
-
4
@response = Rack::Response.new(body, status, headers)
-
4
record if should_record?
-
4
@response.finish
-
end
-
-
1
def should_record?
-
4
dashboard_path = Rails.application.routes.named_routes[:redis_analytics].path rescue nil
-
4
return false if dashboard_path =~ @request.path
-
4
return false unless @response.ok?
-
4
return false unless @response.content_type =~ /^text\/html/
-
4
RedisAnalytics.path_filters.each do |filter|
-
4
return false if filter.matches?(@request.path)
-
end
-
4
RedisAnalytics.filters.each do |filter|
-
4
return false if filter.matches?(@request, @response)
-
end
-
4
return true
-
end
-
-
1
def record
-
4
v = Visit.new(@request, @response)
-
4
@response = v.record
-
4
@response.set_cookie(RedisAnalytics.current_visit_cookie_name, v.updated_current_visit_info)
-
4
@response.set_cookie(RedisAnalytics.first_visit_cookie_name, v.updated_first_visit_info)
-
end
-
-
end
-
end
-
1
module RedisAnalytics
-
1
VERSION = '1.0.0'
-
end
-
-
1
module RedisAnalytics
-
1
class Visit
-
1
include Metrics
-
-
# This class represents one unique visit
-
# User may have never visited the site
-
# User may have visited before but his visit is expired
-
# Everything counted here is unique for a visit
-
-
# helpers
-
1
def for_each_time_range(t)
-
228
RedisAnalytics.redis_key_timestamps.map{|x, y| t.strftime(x)}.each do |ts|
-
190
yield(ts)
-
end
-
end
-
-
1
def first_visit_info
-
12
cookie = @rack_request.cookies[RedisAnalytics.first_visit_cookie_name]
-
12
return cookie ? cookie.split('.') : []
-
end
-
-
1
def current_visit_info
-
19
cookie = @rack_request.cookies[RedisAnalytics.current_visit_cookie_name]
-
19
return cookie ? cookie.split('.') : []
-
end
-
-
# method used in analytics.rb to initialize visit
-
1
def initialize(request, response)
-
4
@t = Time.now
-
4
@redis_key_prefix = "#{RedisAnalytics.redis_namespace}:"
-
4
@rack_request = request
-
4
@rack_response = response
-
4
@first_visit_seq = first_visit_info[0] || current_visit_info[0]
-
4
@current_visit_seq = current_visit_info[1]
-
-
4
@first_visit_time = first_visit_info[1]
-
4
@last_visit_time = first_visit_info[2]
-
-
4
@page_view_seq_no = current_visit_info[2] || 0
-
4
@last_visit_start_time = current_visit_info[3]
-
4
@last_visit_end_time = current_visit_info[4]
-
end
-
-
# called from analytics.rb
-
1
def record
-
4
if @current_visit_seq
-
1
track("visit_time", @t.to_i - @last_visit_end_time.to_i)
-
else
-
3
@current_visit_seq ||= counter("visits")
-
3
track("visits", 1) # track core 'visit' metric
-
3
if @first_visit_seq
-
track("repeat_visits", 1)
-
else
-
3
@first_visit_seq ||= counter("unique_visits")
-
3
track("first_visits", 1)
-
3
track("unique_visits", 1)
-
end
-
3
exec_custom_methods('visit') # custom methods that are measured on a per-visit basis
-
end
-
4
exec_custom_methods('hit') # custom methods that are measured on a per-page-view (per-hit) basis
-
4
track("page_views", 1) # track core 'page_view' metric
-
4
track("second_page_views", 1) if @page_view_seq_no.to_i == 1 # @last_visit_start_time and (@last_visit_start_time.to_i == @last_visit_end_time.to_i)
-
4
@rack_response
-
end
-
-
1
def exec_custom_methods(type)
-
7
Metrics.public_instance_methods.each do |meth|
-
112
if m = meth.to_s.match(/^([a-z_]*)_(count|ratio)_per_#{type}$/)
-
53
begin
-
53
return_value = self.send(meth)
-
53
track(m.to_a[1], return_value) if return_value
-
rescue => e
-
warn "#{meth} resulted in an exception #{e}"
-
end
-
end
-
end
-
end
-
-
# helpers
-
1
def counter(metric_name)
-
6
n = RedisAnalytics.redis_connection.incr("#{@redis_key_prefix}#{metric_name}")
-
# to debug, uncomment this line
-
# puts "COUNT #{metric_name} -> #{n}"
-
6
return n
-
end
-
-
1
def updated_current_visit_info
-
4
value = [@first_visit_seq.to_i, @current_visit_seq.to_i, @page_view_seq_no.to_i + 1, (@last_visit_start_time || @t).to_i, @t.to_i]
-
# to debug, uncomment this line
-
# puts "UPDATING VCN COOKIE -> #{value}"
-
4
expires = @t + RedisAnalytics.visit_timeout.to_i.minutes
-
4
{:value => value.join('.'), :expires => expires}
-
end
-
-
1
def updated_first_visit_info
-
4
value = [@first_visit_seq.to_i, (@first_visit_time || @t).to_i, @t.to_i]
-
# to debug, uncomment this line
-
# puts "UPDATING RUCN COOKIE -> #{value}"
-
4
expires = @t + 1.year
-
4
{:value => value.join('.'), :expires => expires}
-
end
-
-
1
def track(metric_name, metric_value)
-
38
n = 0
-
38
RedisAnalytics.redis_connection.hmset("#{@redis_key_prefix}#METRICS", metric_name, metric_value.class)
-
38
for_each_time_range(@t) do |ts|
-
190
key = "#{@redis_key_prefix}#{metric_name}:#{ts}"
-
190
if metric_value.is_a?(Fixnum)
-
75
n = RedisAnalytics.redis_connection.incrby(key, metric_value)
-
else
-
115
n = RedisAnalytics.redis_connection.zincrby(key, 1, metric_value)
-
end
-
end
-
# to debug, uncomment this line
-
# puts "TRACK #{metric_name} -> #{n}"
-
38
return n
-
end
-
-
end
-
end
-