cohortly 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. data/Gemfile +11 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +61 -0
  4. data/Rakefile +29 -0
  5. data/app/controllers/cohortly/cohortly_controller.rb +8 -0
  6. data/app/controllers/cohortly/metrics_controller.rb +14 -0
  7. data/app/controllers/cohortly/reports_controller.rb +8 -0
  8. data/app/models/cohortly/metric.rb +79 -0
  9. data/app/models/cohortly/report.rb +78 -0
  10. data/app/views/cohortly/metrics/_metric.html.erb +8 -0
  11. data/app/views/cohortly/metrics/_tags.html.erb +8 -0
  12. data/app/views/cohortly/metrics/index.html.erb +22 -0
  13. data/app/views/cohortly/reports/index.html.erb +32 -0
  14. data/app/views/cohortly/reports/show.html.erb +22 -0
  15. data/app/views/layouts/cohortly/application.html.erb +144 -0
  16. data/config/routes.rb +7 -0
  17. data/lib/cohortly.rb +15 -0
  18. data/lib/cohortly/config.rb +6 -0
  19. data/lib/cohortly/engine.rb +16 -0
  20. data/lib/cohortly/tag_config.rb +85 -0
  21. data/lib/cohortly/version.rb +3 -0
  22. data/lib/tasks/run_reports.rake +11 -0
  23. data/test/cohortly_test.rb +119 -0
  24. data/test/dummy/Rakefile +7 -0
  25. data/test/dummy/app/controllers/application_controller.rb +17 -0
  26. data/test/dummy/app/controllers/stuff_controller.rb +12 -0
  27. data/test/dummy/app/helpers/application_helper.rb +2 -0
  28. data/test/dummy/app/helpers/stuff_helper.rb +2 -0
  29. data/test/dummy/app/views/layouts/application.html.erb +19 -0
  30. data/test/dummy/app/views/stuff/index.html.erb +1 -0
  31. data/test/dummy/app/views/stuff/my_stuff.html.erb +2 -0
  32. data/test/dummy/app/views/stuff/your_stuff.html.erb +2 -0
  33. data/test/dummy/config.ru +4 -0
  34. data/test/dummy/config/application.rb +48 -0
  35. data/test/dummy/config/boot.rb +9 -0
  36. data/test/dummy/config/cohortly.yml +15 -0
  37. data/test/dummy/config/database.yml +22 -0
  38. data/test/dummy/config/environment.rb +5 -0
  39. data/test/dummy/config/environments/development.rb +26 -0
  40. data/test/dummy/config/environments/production.rb +49 -0
  41. data/test/dummy/config/environments/test.rb +35 -0
  42. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  43. data/test/dummy/config/initializers/cohortly_config.rb +15 -0
  44. data/test/dummy/config/initializers/inflections.rb +10 -0
  45. data/test/dummy/config/initializers/mime_types.rb +5 -0
  46. data/test/dummy/config/initializers/secret_token.rb +7 -0
  47. data/test/dummy/config/initializers/session_store.rb +8 -0
  48. data/test/dummy/config/locales/en.yml +5 -0
  49. data/test/dummy/config/routes.rb +62 -0
  50. data/test/dummy/public/404.html +26 -0
  51. data/test/dummy/public/422.html +26 -0
  52. data/test/dummy/public/500.html +26 -0
  53. data/test/dummy/public/favicon.ico +0 -0
  54. data/test/dummy/public/javascripts/application.js +2 -0
  55. data/test/dummy/public/javascripts/controls.js +965 -0
  56. data/test/dummy/public/javascripts/dragdrop.js +974 -0
  57. data/test/dummy/public/javascripts/effects.js +1123 -0
  58. data/test/dummy/public/javascripts/prototype.js +6001 -0
  59. data/test/dummy/public/javascripts/rails.js +191 -0
  60. data/test/dummy/script/rails +6 -0
  61. data/test/integration/navigation_test.rb +14 -0
  62. data/test/support/integration_case.rb +5 -0
  63. data/test/support/test_tag_config.rb +40 -0
  64. data/test/test_helper.rb +27 -0
  65. metadata +154 -0
@@ -0,0 +1,7 @@
1
+ Rails.application.routes.draw do
2
+ namespace :cohortly do
3
+ match 'report_all', :controller => 'reports', :action => 'show', :as => :report_all
4
+ resources :metrics, :only => [:index]
5
+ resources :reports, :only => [:index, :show]
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ require 'mongo_mapper'
2
+ require "cohortly/config"
3
+ require "cohortly/engine"
4
+ require "cohortly/tag_config"
5
+
6
+ MongoMapper.database = "cohortly-#{Rails.env}"
7
+
8
+ require "active_support/notifications"
9
+
10
+ ActiveSupport::Notifications.subscribe "cohortly.event" do |*args|
11
+ Cohortly::Metric.store!(args)
12
+ end
13
+
14
+ module Cohortly
15
+ end
@@ -0,0 +1,6 @@
1
+ module Cohortly
2
+ class Config
3
+ include ActiveSupport::Configurable
4
+
5
+ end
6
+ end
@@ -0,0 +1,16 @@
1
+ module Cohortly
2
+ class Engine < Rails::Engine
3
+ initializer "read_config" do |app|
4
+ config_file = File.join(Rails.root, 'config', 'cohortly.yml')
5
+ if File.exists?(config_file)
6
+ Cohortly::Config.config.tap do |cfg|
7
+ conf = YAML.load_file(config_file)[Rails.env]
8
+ conf.keys.each { |k| cfg.send("#{k}=", conf[k]) }
9
+ Cohortly::Metric.connection Mongo::Connection.new(cfg.host, cfg.port)
10
+ Cohortly::Metric.set_database_name cfg.database
11
+ Cohortly::Metric.database.authenticate(cfg.username, cfg.password) if(cfg.password)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,85 @@
1
+ require 'singleton'
2
+ module Cohortly
3
+ class TagConfig
4
+ include Singleton
5
+ attr_accessor :_tags, :lookup_table
6
+ def self.draw_tags(&block)
7
+ instance._tags = []
8
+ instance.lookup_table = {}
9
+ instance.instance_eval(&block)
10
+ instance.compile!
11
+ instance
12
+ end
13
+
14
+ def tag(tag_name, &block)
15
+ self._tags << Tag.new(tag_name, &block)
16
+ end
17
+
18
+ def tags(*args, &block)
19
+ args.each {|x| tag(x, &block) }
20
+ end
21
+
22
+ def compile!
23
+ self._tags.each do |tag|
24
+ tag._controllers.each do |cont|
25
+ lookup_table[cont._name] ||= {}
26
+ cont._acts.each do |a|
27
+ lookup_table[cont._name][a] ||= []
28
+ tag_names = lookup_table[cont._name][a] << tag._name
29
+ lookup_table[cont._name][a] = tag_names.uniq
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ def tags_for(controller, action = :_all)
36
+ res = []
37
+ if lookup_table[controller.to_sym]
38
+ res += lookup_table[controller.to_sym][action.to_sym] || []
39
+ res += lookup_table[controller.to_sym][:_all] || []
40
+ end
41
+ res.uniq.collect &:to_s
42
+ end
43
+
44
+ def self.tags_for(controller, action = :_all)
45
+ return [] if controller.nil?
46
+ instance.tags_for(controller, action)
47
+ end
48
+
49
+ def self.all_tags
50
+ if instance._tags
51
+ instance._tags.collect {|x| x._name.to_s }
52
+ else
53
+ []
54
+ end
55
+ end
56
+
57
+ class Tag
58
+ attr_accessor :_name, :_controllers
59
+ def initialize(tag_name, &block)
60
+ self._controllers ||= []
61
+ self._name = tag_name.to_sym
62
+ instance_eval(&block)
63
+ end
64
+ def controller(controller_name, &block)
65
+ _controllers << Controller.new(controller_name, &block)
66
+ end
67
+ def controllers(*args)
68
+ args.each { |name| controller(name) { actions :_all } }
69
+ end
70
+ end
71
+
72
+ class Controller
73
+ attr_accessor :_name, :_acts
74
+ def initialize(controller_name, &block)
75
+ self._acts ||= []
76
+ self._name = controller_name.to_sym
77
+ self.instance_eval(&block)
78
+ end
79
+
80
+ def actions(*act_names)
81
+ self._acts = act_names.collect &:to_sym
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,3 @@
1
+ module Cohortly
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,11 @@
1
+ namespace :cohortly do
2
+ namespace :run do
3
+ desc "run the reports for all the tags"
4
+ task :reports => :environment do
5
+ Cohortly::Metric.cohort_chart_for_tag
6
+ Cohortly::TagConfig.all_tags.each do |tag|
7
+ Cohortly::Metric.cohort_chart_for_tag(tag)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,119 @@
1
+ require 'test_helper'
2
+
3
+
4
+
5
+
6
+ class CohortlyTest < ActiveSupport::TestCase
7
+
8
+ test "tag config" do
9
+ assert_equal Cohortly::TagConfig.tags_for(:hi_there, :index), ['hello']
10
+ assert_equal Cohortly::TagConfig.tags_for(:see_ya, :index), []
11
+ assert_equal Cohortly::TagConfig.tags_for(:see_ya, :create), ['goodbye']
12
+ assert_equal Cohortly::TagConfig.tags_for(:hi_there, :what), []
13
+ assert_equal Cohortly::TagConfig.tags_for(:hi_there, :update), ['hello', 'goodbye']
14
+ assert_equal Cohortly::TagConfig.tags_for(:stuff, :a), ['only_good', 'only_bad']
15
+ assert_equal Cohortly::TagConfig.tags_for(:stuff, :b), ['only_good', 'only_bad']
16
+ assert_equal Cohortly::TagConfig.tags_for(:goodies, :a), ['only_good', 'only_bad']
17
+ assert_equal Cohortly::TagConfig.tags_for(:goodies, :b), ['only_good', 'only_bad']
18
+ assert_equal Cohortly::TagConfig.tags_for(:hellas, :b), ['heh', 'whoa']
19
+ assert_equal Cohortly::TagConfig.tags_for(:hellas, :b), ['heh', 'whoa']
20
+
21
+ assert_equal Cohortly::TagConfig.all_tags, ['hello', 'goodbye', 'only_good', 'only_bad', 'heh', 'whoa', 'over13', 'login']
22
+ end
23
+
24
+ test "cohortly record event" do
25
+
26
+
27
+ payload = { :user_start_date => Time.now - 1.month,
28
+ :user_id => 5,
29
+ :user_email => "jordon@example.com",
30
+ :controller => "session",
31
+ :action => "login"
32
+ }
33
+
34
+ ActiveSupport::Notifications.instrument("cohortly.event", payload)
35
+
36
+ metric = Cohortly::Metric.first
37
+ assert metric, "should create metric"
38
+ assert metric.created_at
39
+ assert metric.tags.include? 'login'
40
+ assert metric.tags.include? 'over13'
41
+ assert_equal metric.controller, 'session'
42
+ assert_equal metric.action, 'login'
43
+ assert_equal metric.user_email, 'jordon@example.com'
44
+ assert_equal metric.user_start_date.utc.to_s, payload[:user_start_date].utc.to_s
45
+
46
+ end
47
+
48
+ test "report map reduce" do
49
+ setup_data_to_report_on
50
+ Cohortly::Metric.cohort_chart_for_tag
51
+ assert_equal (Cohortly::Metric.all.collect &:user_id).uniq.length, 105
52
+
53
+ report = Cohortly::Report.new('cohort_report')
54
+ assert_equal report.month_to_time('2011-08'), Time.utc(2011, 8)
55
+ assert_equal report.time_to_month(Time.utc(2011,8)), '2011-08'
56
+ assert_equal report.start_month, (Time.now - 15.months).year.to_s + '-0' + (Time.now - 14.months).month.to_s
57
+ assert_equal report.month_cohorts.length, 15
58
+
59
+ # assert_equal report.report_line(report.month_cohorts[2]), []
60
+ assert_equal report.report_totals, [[14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1],
61
+ [13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1],
62
+ [12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1],
63
+ [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1],
64
+ [10, 9, 8, 7, 6, 5, 4, 3, 2, 1],
65
+ [9, 8, 7, 6, 5, 4, 3, 2, 1],
66
+ [8, 7, 6, 5, 4, 3, 2, 1],
67
+ [7, 6, 5, 4, 3, 2, 1],
68
+ [6, 5, 4, 3, 2, 1],
69
+ [5, 4, 3, 2, 1],
70
+ [4, 3, 2, 1],
71
+ [3, 2, 1],
72
+ [2, 1],
73
+ [1],
74
+ []]
75
+ end
76
+
77
+ test "counting uniq users in cohort" do
78
+ setup_data_to_report_on
79
+ Cohortly::Metric.cohort_chart_for_tag
80
+ report = Cohortly::Report.new('cohort_report')
81
+ start_month = report.start_month
82
+ start_month_time = report.month_to_time(report.start_month)
83
+ next_month = report.time_to_month(start_month_time + 1.month)
84
+
85
+ assert_equal report.user_count_in_cohort(start_month), 14
86
+ assert_equal report.user_count_in_cohort(next_month), 13
87
+ end
88
+
89
+ test "getting a line of percentages" do
90
+ setup_data_to_report_on
91
+ Cohortly::Metric.cohort_chart_for_tag
92
+ report = Cohortly::Report.new('cohort_report')
93
+ line = report.percent_line(report.start_month)
94
+ cohort_count = report.user_count_in_cohort(report.start_month)
95
+ assert_equal line, [cohort_count, 100, 93, 86, 79, 71, 64, 57, 50, 43, 36, 29, 21, 14, 7]
96
+
97
+ end
98
+
99
+ def setup_data_to_report_on
100
+ payload = { :user_start_date => Time.now - 1.month,
101
+ :user_id => 5,
102
+ :controller => "session",
103
+ :action => "login"
104
+ }
105
+ # 15 months of data
106
+ 15.times do |start_offset|
107
+ start_offset.times do |m|
108
+ ((start_offset * 100)..((start_offset * 100) + m)).to_a.each do |user_id|
109
+ payload[:user_id] = user_id
110
+ payload[:user_start_date] = Time.now - start_offset.months
111
+ payload[:created_at] = Time.now - m.months
112
+
113
+ 5.times { ActiveSupport::Notifications.instrument("cohortly.event", payload) }
114
+ end
115
+ end
116
+ end
117
+
118
+ end
119
+ end
@@ -0,0 +1,7 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+ require 'rake'
6
+
7
+ Dummy::Application.load_tasks
@@ -0,0 +1,17 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery
3
+
4
+ before_filter :record_event
5
+
6
+
7
+ protected
8
+
9
+ def record_event
10
+ payload = { :user_start_date => Time.now - 1.month,
11
+ :user_id => 5,
12
+ :controller => params[:controller],
13
+ :action => params[:action]
14
+ }
15
+ ActiveSupport::Notifications.instrument("cohortly.event", payload)
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ class StuffController < ApplicationController
2
+
3
+ def index
4
+ end
5
+
6
+ def my_stuff
7
+ end
8
+
9
+ def your_stuff
10
+ end
11
+
12
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,2 @@
1
+ module StuffHelper
2
+ end
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dummy</title>
5
+ <%= csrf_meta_tag %>
6
+ </head>
7
+ <body>
8
+
9
+ <ul>
10
+ <li><%= link_to "My Stuff", '/stuff/my_stuff' %></li>
11
+ <li><%= link_to "Your Stuff", '/stuff/your_stuff' %></li>
12
+ <li><%= link_to "Metrics", cohortly_metrics_path %></li>
13
+ <li><%= link_to "Cohort Reports", cohortly_reports_path %></li>
14
+ </ul>
15
+
16
+ <%= yield %>
17
+
18
+ </body>
19
+ </html>
@@ -0,0 +1 @@
1
+ <h1>Home page</h1>
@@ -0,0 +1,2 @@
1
+ <h1>Stuff#my_stuff</h1>
2
+ <p>Find me in app/views/stuff/my_stuff.html.erb</p>
@@ -0,0 +1,2 @@
1
+ <h1>Stuff#your_stuff</h1>
2
+ <p>Find me in app/views/stuff/your_stuff.html.erb</p>
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Dummy::Application
@@ -0,0 +1,48 @@
1
+ require File.expand_path('../boot', __FILE__)
2
+
3
+ require "active_model/railtie"
4
+ # require "active_record/railtie"
5
+ require "action_controller/railtie"
6
+ require "action_view/railtie"
7
+ require "action_mailer/railtie"
8
+
9
+ Bundler.require
10
+ require "cohortly"
11
+
12
+ module Dummy
13
+ class Application < Rails::Application
14
+ # Settings in config/environments/* take precedence over those specified here.
15
+ # Application configuration should go into files in config/initializers
16
+ # -- all .rb files in that directory are automatically loaded.
17
+
18
+ # Custom directories with classes and modules you want to be autoloadable.
19
+ # config.autoload_paths += %W(#{config.root}/extras)
20
+
21
+ # Only load the plugins named here, in the order given (default is alphabetical).
22
+ # :all can be used as a placeholder for all plugins not explicitly named.
23
+ # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
24
+
25
+ # Activate observers that should always be running.
26
+ # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
27
+
28
+ # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
29
+ # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
30
+ # config.time_zone = 'Central Time (US & Canada)'
31
+
32
+ # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
33
+ # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
34
+ # config.i18n.default_locale = :de
35
+
36
+ # JavaScript files you want as :defaults (application.js is always included).
37
+ # config.action_view.javascript_expansions[:defaults] = %w(jquery rails)
38
+
39
+ # Configure the default encoding used in templates for Ruby 1.9.
40
+ config.encoding = "utf-8"
41
+
42
+ # Configure sensitive parameters which will be filtered from the log file.
43
+ config.filter_parameters += [:password]
44
+
45
+
46
+ end
47
+ end
48
+
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ gemfile = File.expand_path('../../../../Gemfile', __FILE__)
3
+ if File.exist?(gemfile)
4
+ ENV['BUNDLE_GEMFILE'] = gemfile
5
+ require 'bundler'
6
+ Bundler.setup
7
+ end
8
+
9
+ $:.unshift File.expand_path('../../../../lib', __FILE__)
@@ -0,0 +1,15 @@
1
+ defaults: &defaults
2
+ host: localhost
3
+ port: 27017
4
+
5
+ test:
6
+ <<: *defaults
7
+ database: cohortly-test
8
+
9
+ development:
10
+ <<: *defaults
11
+ database: cohortly-development
12
+
13
+ production:
14
+ <<: *defaults
15
+ database: cohortly-production
@@ -0,0 +1,22 @@
1
+ # SQLite version 3.x
2
+ # gem install sqlite3
3
+ development:
4
+ adapter: sqlite3
5
+ database: db/development.sqlite3
6
+ pool: 5
7
+ timeout: 5000
8
+
9
+ # Warning: The database defined as "test" will be erased and
10
+ # re-generated from your development database when you run "rake".
11
+ # Do not set this db to the same as development or production.
12
+ test:
13
+ adapter: sqlite3
14
+ database: db/test.sqlite3
15
+ pool: 5
16
+ timeout: 5000
17
+
18
+ production:
19
+ adapter: sqlite3
20
+ database: db/production.sqlite3
21
+ pool: 5
22
+ timeout: 5000