Module: DulyNoted

Extended by:
DulyNoted
Includes:
Helpers, Updater
Included in:
DulyNoted
Defined in:
lib/duly_noted.rb,
lib/duly_noted/helpers.rb,
lib/duly_noted/updater.rb,
lib/duly_noted/version.rb

Overview

  • ‘track`

  • ‘update`

  • ‘query`

  • ‘count`

  • ‘chart`

Defined Under Namespace

Modules: Helpers, Updater Classes: InvalidId, InvalidOptions, InvalidStep, NotValidMetric, UpdateError

Constant Summary collapse

VERSION =
"1.0.1"

Instance Method Summary collapse

Methods included from Updater

#check_schema, #update_schema

Methods included from Helpers

#assemble_for, #build_key, #fields_for, #find_keys, #metrics, #normalize, #parse_time_range, #valid_metric?

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object

##Behind the curtain (metaprogramming)

###method_missing

As I’m sure you’re aware, method_missing is the magic tool for ruby developers to define dynamic methods like the above ‘count_x_by_y`, which is exactly what we use it for.



340
341
342
343
344
345
346
# File 'lib/duly_noted.rb', line 340

def method_missing(method, *args, &block)
  if method.to_s =~ /^count_(.+)_by_(.+)$/
    count_x_by_y($1, $2, args[0])
  else
    super
  end
end

Instance Method Details

#chart(metric_name, options = {}) ⇒ Object

##Chart

_parameters: ‘metric_name`, `data_points`(required),`for`(optional), `time_start`(optional), `time_end`(optional), `time_range`(optional)_

Chart is a little complex, but I’ll try to explain all of the possibilities. It’s main purpose is to pull out your data and prepare it in a way that makes it easy to chart. The smallest amount of input it will take is just a ‘metric_name` and an amount of `data_points` to capture. This will check the time of the earliest known data point, and the time of the last known data point, and run chart with those values as the `time_start` and `time_end` respectively. It will take the amount of time that that spans, and divide it by the number of data points you asked for, and will split the time up evenly, and return a hash of times, and counts. If you specify both a `time_start` and a `time_end`, and a number of `data_points`, then it will divide the amount of time that that spans and will return a hash of times and counts. The other option is that you can specify either a `time_start` OR a `time_end` and a `step` and a number of `data_points`. This will start at whatever time you specified, and (if it’s ‘time_end`) count down by the step (if you specified `time_start`, it would count up), as many times as the number of data points you requested.

###Usage

DulyNoted.chart("page_views",
  :time_range => 1.month.ago..Time.now,
  :step => 1.day)

DulyNoted.chart("page_views",
  :time_range => 1.day.ago..Time.now,
  :data_points => 12)

DulyNoted.chart("page_views",
  :time_start => 1.day.ago,
  :step => 1.hour,
  :data_points => 12)

DulyNoted.chart("downloads",
  :time_end => Time.now,
  :step => 1.month,
  :data_points => 12)

DulyNoted.chart("page_views",
  :data_points => 100)

Chart can be a little confusing but it’s pretty powerful, so play around with it.



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/duly_noted.rb', line 269

def chart(metric_name, options={})
  parse_time_range(options)
  chart = Hash.new(0)
  if options[:time_start] && options[:time_end]
    time = options[:time_start]
    if options[:data_points]
      total_time = options[:time_end] - options[:time_start]
      options[:step] = total_time.to_i / options[:data_points]
    end
    while time < options[:time_end]
      chart[time.to_i] = DulyNoted.count(metric_name, :time_start => time, :time_end => time+options[:step], :for => options[:for])
      time += options[:step]
    end
  elsif options[:step] && options[:data_points] && (options[:time_end] || options[:time_start])
    raise InvalidStep if options[:step] == 0
    options[:step] *= -1 if (options[:step] > 0 && options[:time_end]) || (options[:step] < 0 && options[:time_start])
    time = options[:time_start] || options[:time_end]
    step = options[:step]
    options[:data_points].times do
      options[:time_start] = time
      options[:time_start] += step if step < 0
      options[:time_end] = time
      options[:time_end] += step if step > 0
      chart[time.to_i] = DulyNoted.count(metric_name, options)
      time += step
    end
  elsif options[:data_points]
    key = build_key(metric_name)
    key << assemble_for(options)
    options[:time_start] = Time.at(DulyNoted.redis.zrange(key, 0, 0, :withscores => true)[1].to_f)
    options[:time_end] = Time.at(DulyNoted.redis.zrevrange(key, 0, 0, :withscores => true)[1].to_f)
    chart = DulyNoted.chart(metric_name, options)
  else
    raise InvalidOptions
  end
  return chart
end

#count(metric_name, options = {}) ⇒ Object

##Count

_parameters: ‘metric_name`, `for`(optional), `time_start`(optional), `time_end`(optional), `time_range`(optional)_

Count will return the number of events logged in a given time range, or if no time range is given, the total count. As with ‘#query`, you can specify `for` to return a subset of counts, or you can leave it off to get the count across the whole `metric_name`.

###Usage

DulyNoted.count("page_views",
  for: "home_page",
  time_start: 1.day.ago,
  time_end: Time.now)


218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/duly_noted.rb', line 218

def count(metric_name, options={})
  parse_time_range(options)
  key = build_key(metric_name)
  key << assemble_for(options)
  keys = find_keys(key)
  sum = 0
  if options[:time_start] && options[:time_end]
    keys.each do |key|
      sum += DulyNoted.redis.zcount(key, options[:time_start].to_f, options[:time_end].to_f)
    end
    return sum
  else
    keys.each do |key|
      sum += DulyNoted.redis.zcard(key)
    end
    return sum
  end
end

#count_x_by_y(metric_name, meta_field, options) ⇒ Object

##Magic

###count_x_by_y

If you want to count a number of events by a meta field, you can use this magic command. So imagine this scenario:

DulyNoted.track("page_views", meta: {browser: "chrome"})

And you wanted to see a break down of page views by various browsers, you can call ‘DulyNoted.count_page_views_by_browser` and you’d get a hash that looked something like this:

{"chrome" => 2913, "firefox" => 5281, "IE" => 7182, "safari" => 3213}

So that method will work as soon as you’ve tracked something with that metric name. If you try to call the method on a metric that you haven’t yet tracked you will get a ‘DulyNoted::NotValidMetric`. But if you reference a meta field that didn’t exist, you’d just get a hash that looks like

{nil => 1}


323
324
325
326
327
328
329
330
331
332
# File 'lib/duly_noted.rb', line 323

def count_x_by_y(metric_name, meta_field, options)
  options ||= {}
  options = {:meta_fields => [meta_field]}.merge(options)
  meta_hashes = query(metric_name, options)
  result = Hash.new(0)
  meta_hashes.each do |meta_hash|
    result[meta_hash[meta_field]] += 1
  end
  result
end

#query(metric_name, options = {}) ⇒ Object

##Query

_parameters: ‘metric_name`, `for`(optional), `meta_fields`(optional), `time_start`(optional), `time_end`(optional), `time_range`(optional)_

Query will return an array of all the metadata in chronological order from a time range, or for the whole data set. If for is specified, it will limit it by that context. For instance, if you have ‘track`ed several page views with `for` set to the name of the page that was viewed, you could query with `for` set to `home_page` to get all of the metadata from the page views from the home page, or you could leave off the `for`, and return all of the metadata for all of the page views, across all pages.

###Usage

DulyNoted.query("page_views",
  for: "home_page",
  time_start: 1.day.ago,
  time_end: Time.now)


160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/duly_noted.rb', line 160

def query(metric_name, options={})
  key = build_key(metric_name)
  parse_time_range(options)
  key << assemble_for(options)
  if options[:id]
    key = "dnid:#{options[:id]}"
    real_key = DulyNoted.redis.get key
    if options[:meta_fields]
      options[:meta_fields].collect! { |x| x.to_s }
      result = {}
      options[:meta_fields].each do |field|
        result[field] = DulyNoted.redis.hget real_key, field
      end
      results = [result]
    else
      results = [DulyNoted.redis.hgetall(real_key)]
    end
  else
    keys = find_keys(key)
    grab_results = Proc.new do |metric|
      if options[:meta_fields]
        options[:meta_fields].collect! { |x| x.to_s }
        result = {}
        options[:meta_fields].each do |field|
          result[field] = DulyNoted.redis.hget metric, field
        end
        result
      else
        DulyNoted.redis.hgetall metric
      end
    end
    results = []
    if options[:time_start] && options[:time_end]
      keys.each do |key|
        results += DulyNoted.redis.zrangebyscore(key, options[:time_start].to_f, options[:time_end].to_f).collect(&grab_results)
      end
    else
      keys.each do |key|
        results += DulyNoted.redis.zrange(key, 0, -1).collect(&grab_results)
      end
    end
  end
  return results
end

#redisObject



360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/duly_noted.rb', line 360

def redis
  @redis ||= (
    url = URI(@redis_url || "redis://127.0.0.1:6379/0")

    check_schema(Redis.new({
      :host => url.host,
      :port => url.port,
      :db => url.path[1..-1],
      :password => url.password
    }))
  )
end

#redis=(url) ⇒ Object

##Redis

DulyNoted will try to connect to Redis’s default url and port if you don’t specify a Redis connection URL. You can set the url with the method

DulyNoted.redis = REDIS_URL


354
355
356
357
358
# File 'lib/duly_noted.rb', line 354

def redis=(url)
  @redis = nil
  @redis_url = url
  redis
end

#track(metric_name, options = {}) ⇒ Object

DulyNoted.track(“page_views”,

  for: "home",
  meta: {browser: "chrome"})

DulyNoted.track("video_plays",
  for: ["user_7261", "video_917216"],
  meta: {amount_watched: 0})

DulyNoted.track("purchases",
  for: "user_281",
  generated_at: 1.day.ago)


106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/duly_noted.rb', line 106

def track(metric_name, options={})
  options = {:generated_at => Time.now}.merge(options)
  key = build_key(metric_name)
  key_without_for = key.dup
  id = DulyNoted.redis.incr "dnid"
  key << assemble_for(options)
  DulyNoted.redis.pipelined do
    DulyNoted.redis.sadd build_key("metrics", false), key_without_for
    DulyNoted.redis.zadd key, options[:generated_at].to_f, "#{key}:#{id}:meta"
    DulyNoted.redis.set "dnid:#{id}", "#{key}:#{id}:meta" # set alias key
    if options[:meta] # set meta data
      DulyNoted.redis.mapped_hmset "#{key}:#{id}:meta", options[:meta]
      options[:meta].keys.each do |field|
        DulyNoted.redis.sadd "#{key}:fields", field
      end
    end
  end
  id
end

#update(id, options = {}) ⇒ Object

##Update

_parameters: ‘id`, `meta`(optional)_

Use update to add, or edit the metadata stored with a metric.

###Usage

DulyNoted.track("page_views",
  meta: {time_on_page: 0, browser: "chrome"}) # => 5673
DulyNoted.update(5673,
  meta: { time_on_page: 30 })

Raises:



139
140
141
142
143
144
# File 'lib/duly_noted.rb', line 139

def update(id, options={})
  key = "dnid:#{id}"
  real_key = DulyNoted.redis.get key
  raise InvalidId if real_key == nil
  DulyNoted.redis.mapped_hmset real_key, options[:meta] if options[:meta] 
end