class Boffin::Tracker

Attributes

config[R]
hit_types[RW]
namespace[R]

Public Class Methods

new(class_or_ns, hit_types = [], config = Boffin.config.dup) click to toggle source

@param [String, Symbol, to_s] class_or_ns

A string, symbol or any object that responds to `#to_s` that will be
used to namespace this keys of this Tracker.

@param [Array<Symbol>] hit_types

A list of hit types that this Tracker will allow, if empty then any
hit type will be allowed.

@param [Config] config

A Config instance to use instead of Boffin.config

@example

Tracker.new(MyModel, [:views, likes])
Tracker.new(:urls,   [:shares, :clicks])
# File lib/boffin/tracker.rb, line 18
def initialize(class_or_ns, hit_types = [], config = Boffin.config.dup)
  @namespace = Utils.object_as_namespace(class_or_ns)
  @hit_types = hit_types
  @config    = config
  @keyspace  = Keyspace.new(self)
  @ukeyspace = Keyspace.new(self, true)
end

Public Instance Methods

count(hit_type, instance, opts = {}) click to toggle source

@param [Symbol] hit_type

Type of hit.

@param [#as_member, id, to_s] instance

Object to track.

@param [Hash] opts @option opts [true, as_member, id, to_s] :unique (false)

If `true` will return a count of unique hits. If passed an object, will
use that object as a unique identifier and return the score associated
with it.

@return [Float] @raise Boffin::UndefinedHitTypeError

Raised if a list of hit types is available and the provided hit type is
not in the list.
# File lib/boffin/tracker.rb, line 53
def count(hit_type, instance, opts = {})
  validate_hit_type(hit_type)
  count = case
  when opts[:unique] == true
    redis.zcard(keyspace.hits(hit_type, instance))
  when opts[:unique]
    uid = Utils.object_as_uid(opts[:unique])
    redis.zscore(keyspace.hits(hit_type, instance), uid)
  else
    redis.get(keyspace.hit_count(hit_type, instance))
  end
  (count && count.to_f) || 0.0
end
hit(hit_type, instance, opts = {}) click to toggle source

@param [Symbol] hit_type @param [#as_member, id, to_s] instance @param [Hash] opts @option opts [Array] :unique ([]) uniquenesses @option opts [Fixnum] :increment (1) hit increment @return [Hit] @raise Boffin::UndefinedHitTypeError

Raised if a list of hit types is available and the provided hit type is
not in the list.
# File lib/boffin/tracker.rb, line 35
def hit(hit_type, instance, opts = {})
  validate_hit_type(hit_type)
  Hit.new(self, hit_type, instance, opts)
end
keyspace(uniq = false) click to toggle source

@param [true, false] uniq

If `true` the unique-scoped keyspace is returned

@return [Keyspace]

Keyspace associated with this tracker
# File lib/boffin/tracker.rb, line 116
def keyspace(uniq = false)
  uniq ? @ukeyspace : @keyspace
end
redis() click to toggle source

@return [Redis] The Redis connection for this Tracker’s config

# File lib/boffin/tracker.rb, line 121
def redis
  @config.redis
end
top(type_or_weights, opts = {}) click to toggle source

Performs set union across the specified number of hours, days, or months to calculate the members with the highest hit counts. The operation can be performed on one hit type, or multiple hit types with weights. @param [Symbol, Hash] type_or_weights

When Hash the set union is calculated

@param [Hash] opts @option opts [true, false] :unique (false)

If `true` then only unique hits are considered in the calculation

@option opts [true, false] :counts (false)

If `true` then scores are returned along with the top members

@option opts [:desc, :asc] :order (:desc)

The order of the results, in decending (most hits to least hits) or
ascending (least hits to most hits) order.

@option opts [Fixnum] :hours

Perform union for hit counts over the last _n_ hours.

@option opts [Fixnum] :days

Perform union for hit counts over the last _n_ days.

@option opts [Fixnum] :months

Perform union for hit counts over the last _n_ months.

@example Return IDs of most viewed and liked listings in the past 6 days with scores

@tracker.top({ views: 1, likes: 1 }, counts: true, days: 6)

@example Return IDS of most viewed and liked listings in the past 6 days with scores (Alternate syntax)

@tracker.top([[:views, 1], [:likes, 1]], counts: true, days: 6)

@example Return IDs of most viewed listings in the past 12 hours

@tracker.top(:views, hours: 12)

@note

The result set returned is cached in Redis for the duration of
{Config#cache_expire_secs}

@note

Only one of `:hours`, `:days`, or `:months` should be specified in the
options hash as they can not be combined.

@raise Boffin::UndefinedHitTypeError

If a list of hit types is available and any of the provided hit types is
not in the list.
# File lib/boffin/tracker.rb, line 101
def top(type_or_weights, opts = {})
  validate_hit_type(type_or_weights)
  unit, size = *Utils.extract_time_unit(opts)
  keyspace   = keyspace(opts[:unique])
  if type_or_weights.is_a?(Hash)
    multiunion(keyspace, type_or_weights, unit, size, opts)
  else
    union(keyspace, type_or_weights, unit, size, opts)
  end
end

Private Instance Methods

multiunion(ks, weights, unit, size, opts = {}) click to toggle source

Performs {#union} for each hit type, then performs a union on those result sets with the provided weights. @param [Keyspace] ks

Keyspace to perform the union on

@param [Hash] weights @param [:hours, :days, :months] unit @param [Fixnum] size

Number of intervals to include in the union

@param [Hash] opts @option opts [true, false] :counts (false) @option opts [:asc, :desc] :order (:desc) @return [Array<String>, Array<Array>] @see zfetch

# File lib/boffin/tracker.rb, line 172
def multiunion(ks, weights, unit, size, opts = {})
  weights.keys.each { |t| union(ks, t, unit, size, opts) }
  keys = weights.keys.map { |t| ks.hits_union(t, unit, size) }
  zfetch(ks.hits_union_multi(weights, unit, size), keys, {
    :weights => weights.values
  }.merge(opts))
end
union(ks, hit_type, unit, size, opts = {}) click to toggle source

@param [Keyspace] ks

Keyspace to perform the union on

@param [Symbol] hit_type @param [:hours, :days, :months] unit @param [Fixnum] size

Number of intervals to include in the union

@param [Hash] opts @option opts [true, false] :counts (false) @option opts [:asc, :desc] :order (:desc) @return [Array<String>, Array<Array>] @see zfetch

# File lib/boffin/tracker.rb, line 154
def union(ks, hit_type, unit, size, opts = {})
  keys = ks.hit_time_windows(hit_type, unit, size)
  zfetch(ks.hits_union(hit_type, unit, size), keys, opts)
end
validate_hit_type(hit_type) click to toggle source

Checks to see if ‘hit_type` exists in the list of hit types. If no elements exist in @hit_types then the check is skipped. @param [Symbol] hit_type @raise Boffin::UndefinedHitTypeError

Raised if a list of hit types is available and the provided hit type is
not in the list.
# File lib/boffin/tracker.rb, line 133
def validate_hit_type(hit_type)
  return if @hit_types.empty?
  (hit_type.is_a?(Hash) ? hit_type.keys : [hit_type]).each do |type|
    next if @hit_types.include?(type)
    raise UndefinedHitTypeError, "#{type} is not in the list of " \
    "valid hit types for this Tracker, valid types are: " \
    "#{@hit_types.inspect}"
  end
end
zfetch(storkey, keys, opts = {}) click to toggle source

Checks to see if the result set exists (is cached), if it does the set is returned, otherwise a union of the keys is performed, cached, and returned. @param [String] storkey

Key to store the result set under

@param [Array<String>] keys @param [Hash] opts @option opts [true, false] :counts (false) @option opts [:asc, :desc] :order (:desc) @see zrange

# File lib/boffin/tracker.rb, line 190
def zfetch(storkey, keys, opts = {})
  zrangeopts = {
    :counts => opts.delete(:counts),
    :order  => (opts.delete(:order) || :desc).to_sym }
  if redis.zcard(storkey) < 1
    redis.zunionstore(storkey, keys, opts)
    redis.expire(storkey, @config.cache_expire_secs)
  end
  zrange(storkey, zrangeopts)
end
zrange(key, opts) click to toggle source

Performs a range on a sorted set at key. @param [String] key @param [Hash] opts @option opts [true, false] :counts (false) @option opts [:asc, :desc] :order (:desc) @return [Array<String>, Array<Array>]

Returns an array of members in sorted order, optionally if the `:counts`
option is `true` it returns an array of pairs where the first value is
the member, and the second value is the member's score.
# File lib/boffin/tracker.rb, line 210
def zrange(key, opts)
  args = [key, 0, -1]
  args << { :with_scores => true } if opts[:counts]
  case opts[:order]
  when :asc
    redis.zrange(*args)
  when :desc
    redis.zrevrange(*args)
  else
    raise ArgumentError, "unknown order type: #{opts[:order].inspect}"
  end
end