Class: Retained::Tracker

Inherits:
Object
  • Object
show all
Defined in:
lib/retained/tracker.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config = Configuration.new) ⇒ Tracker

Returns a new instance of Tracker.



10
11
12
# File 'lib/retained/tracker.rb', line 10

def initialize(config = Configuration.new)
  @config = config
end

Instance Attribute Details

#configObject

Returns the value of attribute config.



8
9
10
# File 'lib/retained/tracker.rb', line 8

def config
  @config
end

Instance Method Details

#active?(entity, group: nil, period: Time.now) ⇒ Boolean

Returns true if the entity was active in the given period, or now if no period is provided. If a group or an array of groups is provided activity will only be considered based on those groups.

Returns:

  • (Boolean)


87
88
89
90
91
92
93
94
95
# File 'lib/retained/tracker.rb', line 87

def active?(entity, group: nil, period: Time.now)
  group = [group] if group.is_a?(String)
  group = groups if group == [] || !group

  group.to_a.each do |g|
    return true if config.redis_connection.getbit(key_period(g, period), entity_index(entity, g)) == 1
  end
  false
end

#configure {|config| ... } ⇒ Object

Yields:



14
15
16
# File 'lib/retained/tracker.rb', line 14

def configure
  yield(config)
end

#entity_index(entity, group) ⇒ Object

Returns the index (offset) of the entity within the group.

Thanks to crashlytics for the monotonic_zadd approach taken here www.slideshare.net/crashlytics/crashlytics-on-redis-analytics



106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/retained/tracker.rb', line 106

def entity_index(entity, group)
  monotonic_zadd = <<LUA
    local sequential_id = redis.call('zscore', KEYS[1], ARGV[1])
    if not sequential_id then
      sequential_id = redis.call('zcard', KEYS[1])
      redis.call('zadd', KEYS[1], sequential_id, ARGV[1])
    end
    return sequential_id
LUA

  key = "#{config.prefix}:entity_ids:#{group}"
  config.redis_connection.eval(monotonic_zadd, [key], [entity.to_s]).to_i
end

#groupsObject

Returns an array of all groups



98
99
100
# File 'lib/retained/tracker.rb', line 98

def groups
  config.redis_connection.smembers "#{config.prefix}:groups"
end

#key_period(group, period) ⇒ Object

Returns the key for the group at the period. All periods are internally stored relative to UTC.



122
123
124
# File 'lib/retained/tracker.rb', line 122

def key_period(group, period)
  "#{config.prefix}:#{group}:#{period_start(group, period).to_i}"
end

#retain(entity, group: 'default', period: Time.now) ⇒ Object

Tracks the entity as active at the period, or now if no period is provided.



20
21
22
23
# File 'lib/retained/tracker.rb', line 20

def retain(entity, group: 'default', period: Time.now)
  index = entity_index(entity, group)
  config.redis_connection.setbit key_period(group, period), index, 1
end

#retention(group: 'default', initial_start:, initial_stop:, final_start:, final_stop:) ⇒ Object

Returns the percent retained (as a float) retained between an initial and a final period range. Each period range consists of a start period and an end period (inclusive). The final period range’s starting period must be after the inital period range’s ending period. If there are no entities in the initial period, Float::NAN is returned.



74
75
76
77
78
79
80
81
82
# File 'lib/retained/tracker.rb', line 74

def retention(group: 'default', initial_start:, initial_stop:,
                                final_start:  , final_stop:)
  initial_count = unique_active(group: group, start: initial_start, stop: initial_stop)
  retained = total_retained(group: group, initial_start: initial_start,
                                          initial_stop:  initial_stop,
                                          final_start: final_start,
                                          final_stop: final_stop)
  return retained / initial_count.to_f
end

#total_active(group: 'default', period: Time.now) ⇒ Object

Total active entities in the period, or now if no period, is provided.



27
28
29
# File 'lib/retained/tracker.rb', line 27

def total_active(group: 'default', period: Time.now)
  config.redis_connection.bitcount key_period(group, period)
end

#total_retained(group: 'default', initial_start:, initial_stop:, final_start:, final_stop:) ⇒ Object

Returns the total number of unique active entities retained between an initial and a final period range. Each period range consists of a start period and an end period (inclusive). The final period range’s starting period must be after the inital period range’s ending period.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/retained/tracker.rb', line 49

def total_retained(group: 'default', initial_start:, initial_stop:,
                                     final_start:  , final_stop:)
  #raise ArgumentError, "final_start must be after initial_stop"  if final_start <= initial_stop
  initial_keys = period_range_keys(group, initial_start, initial_stop)
  final_keys   = period_range_keys(group, final_start,    final_stop)

  return 0  if initial_keys == 0 || final_keys == 0

  temp_bitmap do |key|
    temp_bitmap do |initial_key|
      config.redis_connection.bitop 'OR', initial_key, *initial_keys
      temp_bitmap do |final_key|
        config.redis_connection.bitop 'OR', final_key, *final_keys
        config.redis_connection.bitop 'AND', key, initial_key, final_key
        config.redis_connection.bitcount key
      end
    end
  end
end

#unique_active(group: 'default', start:, stop: Time.now) ⇒ Object

Returns the total number of unique active entities between the start and end periods (inclusive), or now if no stop period is provided.



34
35
36
37
38
39
40
41
42
# File 'lib/retained/tracker.rb', line 34

def unique_active(group: 'default', start:, stop: Time.now)
  keys = period_range_keys(group, start, stop)
  return 0  if keys.length == 0

  temp_bitmap do |key|
    config.redis_connection.bitop 'OR', key, *keys
    config.redis_connection.bitcount key
  end
end