growth 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +52 -0
  4. data/Rakefile +36 -0
  5. data/app/assets/config/growth_manifest.js +2 -0
  6. data/app/assets/images/growth/baseline-trending_down-24px.svg +4 -0
  7. data/app/assets/images/growth/baseline-trending_up-24px.svg +4 -0
  8. data/app/assets/javascripts/growth/application.js +13 -0
  9. data/app/assets/stylesheets/growth/application.scss +2 -0
  10. data/app/assets/stylesheets/growth/sidebar.scss +51 -0
  11. data/app/assets/stylesheets/growth/stats.scss +52 -0
  12. data/app/controllers/growth/application_controller.rb +7 -0
  13. data/app/controllers/growth/stats_controller.rb +72 -0
  14. data/app/helpers/growth/application_helper.rb +111 -0
  15. data/app/views/growth/stats/_associations.html.erb +14 -0
  16. data/app/views/growth/stats/_growth.html.erb +75 -0
  17. data/app/views/growth/stats/_report.html.erb +24 -0
  18. data/app/views/growth/stats/_sidebar.html.erb +14 -0
  19. data/app/views/growth/stats/_totals.html.erb +12 -0
  20. data/app/views/growth/stats/index.html.erb +2 -0
  21. data/app/views/growth/stats/show.html.erb +36 -0
  22. data/app/views/layouts/growth/application.html.erb +28 -0
  23. data/config/routes.rb +5 -0
  24. data/lib/generators/growth/install_generator.rb +20 -0
  25. data/lib/generators/templates/growth.rb +23 -0
  26. data/lib/growth.rb +19 -0
  27. data/lib/growth/engine.rb +9 -0
  28. data/lib/growth/operations/retention_report/generate.rb +42 -0
  29. data/lib/growth/operations/retention_report/prepare.rb +33 -0
  30. data/lib/growth/operations/retention_report/validate.rb +18 -0
  31. data/lib/growth/system/container.rb +31 -0
  32. data/lib/growth/transactions/generate_retention_report.rb +13 -0
  33. data/lib/growth/version.rb +3 -0
  34. data/lib/tasks/growth_tasks.rake +4 -0
  35. metadata +148 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c6dfb52ff76c3eaf1a240204c4b4b756f1101a66
4
+ data.tar.gz: 6703922f42436afdf9742d0effcf6147133fa3e6
5
+ SHA512:
6
+ metadata.gz: a7860472cecf90c1562f412c4efde29a0f0d3c679258496652f2a7e5a7b4cccea4473eedb5d8cb78eefd0dd83030fecc0efa8056b5a3eb8f53263be3d2e4d5e0
7
+ data.tar.gz: 9172015a11666bf11bf5ef6de1f47aef8d64a22a3f97e6d83f43bc8c151cb89b205661bba6ac17fa1a436e91b1b277bdd22cded0dbf37f2f97aeb8a486aa561d
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2018 Ryan Friedman
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # Grower
2
+ Measure growth in your Rails models through charts and informational tables and custom views.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'growth'
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle install
17
+ ```
18
+
19
+ Next, you need to run the install generator:
20
+ ```bash
21
+ $ rails generate growth:install
22
+ ```
23
+
24
+ Running the install generator does two things:
25
+
26
+ 1. Mounts the stats engine at '/stats' by adding to your routes.rb
27
+
28
+ ```ruby
29
+ Rails.application.routes.draw do
30
+ mount Growth::Engine, at: "/stats"
31
+ end
32
+ ```
33
+
34
+ 2. Creates an initializer at '/config/initalizers/growth.rb'
35
+
36
+ ```ruby
37
+ Growth.models_to_measure = ApplicationRecord.descendants.map { |model| model.to_s }
38
+ ```
39
+
40
+ ```Growth.models_to_measure ``` takes an array of models as strings, ex. ["Posts", "Users", "Comments"], that you'd like to measure. The default measures all models.
41
+
42
+ ```Growth.model_blacklist``` takes an array of models as strings, ex. ["AdminUsers"], that you want to prevent the gem from measuring.
43
+
44
+ ```Growth.username ``` is your username for http_basic_auth
45
+ ```Growth.password``` is your password for http_basic_auth
46
+
47
+
48
+ ## Contributing
49
+ Contribution directions go here.
50
+
51
+ ## License
52
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,36 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Growth'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path('../spec/dummy/Rakefile', __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+ load 'rails/tasks/statistics.rake'
22
+
23
+
24
+
25
+ require 'bundler/gem_tasks'
26
+
27
+ require 'rake/testtask'
28
+
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'test'
31
+ t.pattern = 'test/**/*_test.rb'
32
+ t.verbose = false
33
+ end
34
+
35
+
36
+ task default: :test
@@ -0,0 +1,2 @@
1
+ //= link_directory ../javascripts/growth .js
2
+ //= link_directory ../stylesheets/growth .css
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
2
+ <path d="M16 18l2.29-2.29-4.88-4.88-4 4L2 7.41 3.41 6l6 6 4-4 6.3 6.29L22 12v6z"/>
3
+ <path d="M0 0h24v24H0z" fill="none"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
2
+ <path d="M16 6l2.29 2.29-4.88 4.88-4-4L2 16.59 3.41 18l6-6 4 4 6.3-6.29L22 12V6z"/>
3
+ <path d="M0 0h24v24H0z" fill="none"/>
4
+ </svg>
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,2 @@
1
+ @import "sidebar";
2
+ @import "stats";
@@ -0,0 +1,51 @@
1
+ .sidebar {
2
+ position: fixed;
3
+ top: 0;
4
+ bottom: 0;
5
+ left: 0;
6
+ z-index: 100;
7
+ padding: 0 0 48px 0;
8
+ box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
9
+ }
10
+
11
+ .sidebar-sticky {
12
+ position: relative;
13
+ top: 0;
14
+ height: calc(100vh - 48px);
15
+ padding-top: .5rem;
16
+ overflow-x: hidden;
17
+ overflow-y: auto;
18
+ }
19
+
20
+ .sidebar-sticky {
21
+ position: -webkit-sticky;
22
+ position: sticky;
23
+ }
24
+
25
+ .flex-column {
26
+ -ms-flex-direction: column!important;
27
+ flex-direction: column!important;
28
+ }
29
+
30
+ .bg-purple {
31
+ background-color: #282952;
32
+ box-shadow: 4px 0 8px 0 rgba(0, 0, 0, 0.1);
33
+ }
34
+
35
+ .bg-blue {
36
+ background-color: #0b7ef4;
37
+ box-shadow: 4px 0 8px 0 rgba(0, 0, 0, 0.1);
38
+ }
39
+
40
+ .nav-link, .logo {
41
+ color: #FFF;
42
+ }
43
+
44
+ .nav-item:hover {
45
+ background-color: #499df4;
46
+ border-right: 2px solid #fff;
47
+ }
48
+
49
+ .nav-item:hover a {
50
+ color: #FFF;
51
+ }
@@ -0,0 +1,52 @@
1
+ /*
2
+ Place all the styles related to the matching controller here.
3
+ They will automatically be included in application.css.
4
+ */
5
+
6
+ .increase {
7
+ color: #2ecc71;
8
+ background-color: #e8f5e9;
9
+ display: inline;
10
+ padding: 8px;
11
+ border-radius: 10em;
12
+ font-size: 16px;
13
+ }
14
+
15
+ .decrease {
16
+ color: #e74c3c;
17
+ background-color: #ffebee;
18
+ display: inline;
19
+ padding: 8px;
20
+ border-radius: 10em;
21
+ font-size: 16px;
22
+ }
23
+
24
+ .growth {
25
+ .blue-text {
26
+ color: #74A9F8;
27
+ }
28
+ }
29
+
30
+ .box {
31
+ border: 1px solid #DDD;
32
+ border-radius: 5px;
33
+ padding: 20px;
34
+ background-color: #FFF;
35
+
36
+ h6, p, b {
37
+ color: #72777a;
38
+ }
39
+ }
40
+
41
+ .table {
42
+ background-color: #FFF !important;
43
+ border-radius: 5px;
44
+ }
45
+
46
+ body {
47
+ background-color: rgb(249, 250, 251) !important;
48
+ }
49
+
50
+ .child-resource {
51
+ text-decoration: underline;
52
+ }
@@ -0,0 +1,7 @@
1
+ module Growth
2
+ class ApplicationController < ActionController::Base
3
+ http_basic_authenticate_with name: Growth.username, password: Growth.password unless Rails.env.test?
4
+
5
+ protect_from_forgery with: :exception
6
+ end
7
+ end
@@ -0,0 +1,72 @@
1
+ require 'csv'
2
+
3
+ require_dependency "growth/application_controller"
4
+ require_dependency "growth/transactions/generate_retention_report"
5
+
6
+ module Growth
7
+ class StatsController < ApplicationController
8
+ def index
9
+ resources = Growth.models_to_measure
10
+
11
+ render :index, locals: {year: get_year, resources: resources}
12
+ end
13
+
14
+ def show
15
+ resource = params[:id].camelize
16
+
17
+ respond_to do |format|
18
+ format.html do
19
+ Growth::Transactions::GenerateRetentionReport.new.call(associations: params['association']) do |m|
20
+ m.success do |result|
21
+ render :show, locals: {resource: resource, report: result[:report]}
22
+ end
23
+
24
+ m.failure do |result|
25
+ render :show, locals: {resource: resource, report: result[:report]}
26
+ end
27
+ end
28
+ end
29
+
30
+ format.csv do
31
+ source_resources_count = params[:source_resources_count].to_i
32
+ target_resources_count = params[:target_resources_count].to_i
33
+
34
+ Growth::Transactions::GenerateRetentionReport.new.call(associations: params['association']) do |m|
35
+ m.success do |result|
36
+ stats = result[:report][:resources_stats].find do |stats|
37
+ stats[:total_source_resources] == source_resources_count
38
+ stats[:total_target_resources] == target_resources_count
39
+ end
40
+
41
+ resources = resource.constantize.find(stats[:total_source_resources_ids])
42
+
43
+ send_data to_csv(resources), filename: "#{resource.pluralize}-#{Date.today}.csv"
44
+ end
45
+
46
+ m.failure do |result|
47
+ raise 'failed to export csv'
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def get_year
57
+ params[:year].present? ? params[:year].to_i : Date.current.year
58
+ end
59
+
60
+ def to_csv(resources)
61
+ attributes = %w{email}
62
+
63
+ CSV.generate(headers: true) do |csv|
64
+ csv << attributes
65
+
66
+ resources.each do |user|
67
+ csv << attributes.map{ |attr| user.send(attr) }
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,111 @@
1
+ module Growth
2
+ module ApplicationHelper
3
+ def get_grouped_options(resources)
4
+ resources.map do |model|
5
+ [
6
+ model,
7
+ model.reflect_on_all_associations(:has_many).map do |reflection|
8
+ name = reflection.name.to_s.singularize.camelize
9
+ [name, "#{model}-#{name}"]
10
+ end
11
+ ]
12
+ end
13
+ end
14
+
15
+ def counts(grouped_models)
16
+ counts = {}
17
+ grouped_models&.count&.each do |key, value|
18
+ counts.has_key?(value) ? counts[value] += 1 : counts[value] = 1
19
+ end
20
+
21
+ counts
22
+ end
23
+
24
+ def group_resource_by_month(resource, year)
25
+ from, to = Date.parse("#{year.to_i - 1}-12-01"), Date.parse("#{year}-12-31")
26
+ grouped_resource_by_month = resource
27
+ .unscoped
28
+ .group_by_month(:created_at, range: from..to)
29
+ .count
30
+
31
+ map_grouped_resource(grouped_resource_by_month, 1.month)
32
+ end
33
+
34
+ def group_resource_by_year(resource, resources)
35
+ from = Date.parse("#{years_since_first_resource(resources).first}-01-01")
36
+ to = Date.parse("#{years_since_first_resource(resources).last}-12-31")
37
+
38
+ grouped_resource_by_year = resource.unscoped.group_by_year(:created_at, range: from..to).count
39
+ map_grouped_resource(grouped_resource_by_year, 1.year)
40
+ end
41
+
42
+ def growth_today(resource)
43
+ resource.constantize.where(created_at: Time.current.beginning_of_day..Time.current.end_of_day).count
44
+ end
45
+
46
+ def growth_month(resource)
47
+ resource.constantize.where(created_at: Date.current.beginning_of_month..Date.current.end_of_month).count
48
+ end
49
+
50
+ def growth_year_to_date(resource)
51
+ resource.constantize.unscoped.where('extract(year from created_at) = ?', Date.current.year).count
52
+ end
53
+
54
+ def years_since_first_resource(resources)
55
+ return @years if defined? @years
56
+
57
+ mapped_resources = resources.map do |resource|
58
+ resource.unscoped.order(:created_at).first
59
+ end.compact.map(&:created_at).sort
60
+
61
+ @years = mapped_resources.empty? ? [Date.current.year] : (mapped_resources.first.to_date.year..Date.current.year).to_a
62
+ end
63
+
64
+ def pluralize_constant(count = nil, constant)
65
+ return constant.to_s.pluralize if count == nil
66
+ return pluralize(count, constant.to_s)
67
+ end
68
+
69
+ private
70
+
71
+ def percentage_to_string(percentage)
72
+ return percentage if percentage == '-'
73
+ return '0%' if percentage == 0
74
+ percentage > 0 ? "+#{percentage}%" : "#{percentage}%"
75
+ end
76
+
77
+ def growth_css_class(growth)
78
+ return '' if growth == 0 || growth == '-'
79
+ growth > 0 ? 'increase' : 'decrease'
80
+ end
81
+
82
+ def map_grouped_resource(grouped_resource, interval)
83
+ grouped_resource.each do |date, count|
84
+ percentage = get_change_in_percentage(grouped_resource[date - interval].try(:fetch, :count), count)
85
+
86
+ grouped_resource[date] = {
87
+ count: count,
88
+ growth: percentage_to_string(percentage),
89
+ css: growth_css_class(percentage)
90
+ }
91
+ end
92
+ end
93
+
94
+ def get_change_in_percentage(previous_value, current_value)
95
+ return 0 if previous_value == current_value
96
+ return '-' if previous_value.nil? || previous_value == 0
97
+
98
+ if current_value > previous_value
99
+ increase = current_value - previous_value
100
+ increase_in_percentage = (increase / previous_value.to_f) * 100
101
+
102
+ increase_in_percentage.round(2)
103
+ else
104
+ decrease = previous_value - current_value
105
+ decrease_in_percentage = (decrease / previous_value.to_f) * 100
106
+
107
+ -decrease_in_percentage.round(2)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,14 @@
1
+ <h5 class="my-4">Associations</h5>
2
+
3
+ <%= form_tag '/growth/stats', method: :get, class: 'mb-5' do %>
4
+ <div class="row">
5
+ <div class="col-md-2">
6
+ <%= select_tag 'association-select', grouped_options_for_select(get_grouped_options(resources), nil, prompt: 'Select model'), class: "form-control" %>
7
+ </div>
8
+ <div class="col-md-1">
9
+ <%= submit_tag("Submit", class: "btn btn-primary") %>
10
+ </div>
11
+ </div>
12
+ <% end %>
13
+
14
+ <%= render "report", report: report %>
@@ -0,0 +1,75 @@
1
+ <h5 class="my-4 blue-text">MoM Growth</h5>
2
+
3
+ <%= form_tag '/growth/stats', method: :get do %>
4
+ <div class="row">
5
+ <div class="col-md-2">
6
+ <%= select_tag 'year', options_for_select((2014..Date.current.year).to_a, year), class: "form-control" %>
7
+ </div>
8
+ <div class="col-md-1">
9
+ <%= submit_tag("Submit", class: "btn btn-primary") %>
10
+ </div>
11
+ </div>
12
+ <% end %>
13
+
14
+ <div class="row mt-2">
15
+ <div class="col-md-12">
16
+ <table class="table table-bordered">
17
+ <tr>
18
+ <th></th>
19
+ <th>January</th>
20
+ <th>February</th>
21
+ <th>March</th>
22
+ <th>April</th>
23
+ <th>May</th>
24
+ <th>June</th>
25
+ <th>July</th>
26
+ <th>August</th>
27
+ <th>September</th>
28
+ <th>October</th>
29
+ <th>November</th>
30
+ <th>December</th>
31
+ </tr>
32
+ <% resources.each do |resource| %>
33
+ <tr>
34
+ <td><b><%= pluralize_constant(resource) %></b></td>
35
+
36
+ <% group_resource_by_month(resource, year).each do |date, stats| %>
37
+ <% if date.year == year %>
38
+ <td>
39
+ <p><%= stats[:count] %></p>
40
+ <p class="<%= stats[:css] %>"><%= stats[:growth] %></p>
41
+ </td>
42
+ <% end %>
43
+ <% end %>
44
+ </tr>
45
+ <% end %>
46
+ </table>
47
+ </div>
48
+ </div>
49
+
50
+ <h5 class="my-4 blue-text">YoY Growth</h5>
51
+
52
+ <div class="row mt-2">
53
+ <div class="col-md-12">
54
+ <table class="table table-bordered">
55
+ <tr>
56
+ <th></th>
57
+ <% years_since_first_resource(resources).each do |year| %>
58
+ <th><%= year %></th>
59
+ <% end %>
60
+ </tr>
61
+ <% resources.each do |resource| %>
62
+ <tr>
63
+ <td><b><%= pluralize_constant(resource) %></b></td>
64
+
65
+ <% group_resource_by_year(resource, resources).each do |date, stats| %>
66
+ <td>
67
+ <p><%= stats[:count] %></p>
68
+ <p class="<%= stats[:css] %>"><%= stats[:growth] %></p>
69
+ </td>
70
+ <% end %>
71
+ </tr>
72
+ <% end %>
73
+ </table>
74
+ </div>
75
+ </div>
@@ -0,0 +1,24 @@
1
+ <% if report[:resources_stats].any? %>
2
+ <div class="mt-2">
3
+ <p><%= pluralize_constant(report[:source_resource]).capitalize %> that have <%= pluralize_constant(report[:target_resource]) %>: <%= report[:total_associated_resources] %></p>
4
+ <p>Total <%= pluralize_constant(report[:target_resource]) %> created: <%= report[:total_target_resources] %></p>
5
+
6
+ <% report[:resources_stats].each do |stats| %>
7
+ <p>
8
+ <b><%= pluralize_constant(stats[:total_source_resources], report[:source_resource]) %></b>
9
+ have <b><%= pluralize(stats[:total_target_resources], report[:target_resource].to_s.downcase) %> (<%= stats[:total_source_resources_percentage] %>%
10
+ of <%= pluralize_constant(report[:source_resource]).capitalize %>)
11
+ </b>
12
+ <% if report[:source_resource].has_attribute? :email %>
13
+ <%= link_to 'export emails to csv', stat_path(report[:source_resource],
14
+ format: 'csv',
15
+ association: "#{report[:source_resource]}-#{report[:target_resource]}",
16
+ source_resources_count: stats[:total_source_resources],
17
+ target_resources_count: stats[:total_target_resources]),
18
+ class: 'ml-3'
19
+ %>
20
+ <% end %>
21
+ </p>
22
+ <% end %>
23
+ </div>
24
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <div class="sidebar-sticky">
2
+ <ul class="nav flex-column">
3
+ <li class="nav-link">
4
+ <%= link_to root_path do %>
5
+ <h3 class="logo"><b>Growth</b>Engine</h3>
6
+ <% end %>
7
+ </li>
8
+ <% resources.each do |resource| %>
9
+ <li class="nav-item">
10
+ <%= link_to resource.to_s.pluralize, stat_path(resource.to_s.camelize(:lower)), class: 'nav-link' %>
11
+ </li>
12
+ <% end %>
13
+ </ul>
14
+ </div>
@@ -0,0 +1,12 @@
1
+ <h5 class="my-4 blue-text">Totals</h5>
2
+
3
+ <div class="row">
4
+ <% resources.each do |resource| %>
5
+ <div class="col-md-2">
6
+ <div class="box">
7
+ <p><%= pluralize_constant(resource) %></p>
8
+ <h4><%= number_with_delimiter(resource.count, delimiter: ",") %></h4>
9
+ </div>
10
+ </div>
11
+ <% end %>
12
+ </div>
@@ -0,0 +1,2 @@
1
+ <%= render "totals", resources: resources %>
2
+ <%= render "growth", resources: resources, year: year %>
@@ -0,0 +1,36 @@
1
+ <h4 class="my-4"><%= resource.pluralize %></h4>
2
+
3
+ <div class="row">
4
+ <div class="col-md-2">
5
+ <div class="box">
6
+ <h6>Today</h6>
7
+ <h2><%= number_with_delimiter(growth_today(resource), delimiter: ",") %></h2>
8
+ </div>
9
+ </div>
10
+ <div class="col-md-2">
11
+ <div class="box">
12
+ <h6>This Month</h6>
13
+ <h2><%= number_with_delimiter(growth_month(resource), delimiter: ",") %></h2>
14
+ </div>
15
+ </div>
16
+ <div class="col-md-2">
17
+ <div class="box">
18
+ <h6>Year to date</h6>
19
+ <h2><%= number_with_delimiter(growth_year_to_date(resource), delimiter: ",") %></h2>
20
+ </div>
21
+ </div>
22
+ </div>
23
+
24
+ <h4 class="my-4">Associations</h4>
25
+
26
+ <div class="row">
27
+ <div class="col-md-12">
28
+ <% resource.constantize.reflect_on_all_associations(:has_many).map do |reflection| %>
29
+ <span class="mr-3">
30
+ <%= link_to reflection.name.to_s.singularize.camelize, stat_path(resource, association: "#{resource}-#{reflection.name.to_s.singularize.camelize}"), class: 'child-resource' %>
31
+ </span>
32
+ <% end %>
33
+
34
+ <%= render "report", report: report %>
35
+ </div>
36
+ </div>
@@ -0,0 +1,28 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Growth</title>
5
+ <%= stylesheet_link_tag "growth/application", media: "all" %>
6
+ <%= javascript_include_tag "growth/application" %>
7
+
8
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
9
+
10
+ <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
12
+ <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
13
+
14
+ <%= csrf_meta_tags %>
15
+ </head>
16
+ <body class="growth">
17
+ <div class="container-fluid">
18
+ <div class="row">
19
+ <div class="col-md-2 d-none d-md-block bg-blue sidebar px-0">
20
+ <%= render "sidebar", resources: Growth.models_to_measure %>
21
+ </div>
22
+ <div class="col-md-10 pl-3">
23
+ <%= yield %>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </body>
28
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ Growth::Engine.routes.draw do
2
+ root to: 'stats#index'
3
+
4
+ resources :stats, only: [:index, :show]
5
+ end
@@ -0,0 +1,20 @@
1
+ require 'rails/generators/base'
2
+ require 'securerandom'
3
+
4
+ module Growth
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("../../templates", __FILE__)
8
+
9
+ desc "Creates the growth.rb initializer and adds '/stats' routes to your application."
10
+
11
+ def copy_initializer
12
+ template "growth.rb", "config/initializers/growth.rb"
13
+ end
14
+
15
+ def setup_routes
16
+ route 'mount Growth::Engine, at: "/growth"'
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ Growth.setup do |config|
2
+ # Default: Measures all models.
3
+ # models_to_measure takes an array of Rails models, for example:
4
+ #
5
+ # Growth.models_to_measure = [User, Product, Order, Payment]
6
+
7
+ # Default: Prevent specific models from being measured
8
+ # model_blacklist takes an array of Rails models, for example:
9
+ #
10
+ # Growth.model_blacklist = [AdminUser]
11
+
12
+ # This is your username for securing the '/stats' URL
13
+ # You will be prompted to enter this when viewing the page
14
+ # This value would be better stored in an environment variabe
15
+ #
16
+ # Growth.username = ENV['growth_username']
17
+
18
+ # This is your password for securing the /stats page
19
+ # You will be prompted to enter this when viewing the page
20
+ # This value would be better stored in an environment variabe
21
+ #
22
+ # Growth.password = ENV['growth_password']
23
+ end
data/lib/growth.rb ADDED
@@ -0,0 +1,19 @@
1
+ require "growth/engine"
2
+
3
+ module Growth
4
+ mattr_accessor :username
5
+ mattr_accessor :password
6
+
7
+ mattr_accessor :model_blacklist
8
+ @@model_blacklist = []
9
+
10
+ mattr_writer :models_to_measure
11
+
12
+ def self.models_to_measure
13
+ @@models_to_measure ||= ::ActiveRecord::Base.descendants.map(&:name) - ::ActiveRecord::Base.send(:subclasses).map(&:name)
14
+ end
15
+
16
+ def self.setup
17
+ yield if block_given?
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ require "rubygems"
2
+ require "pry"
3
+ require "dry-transaction"
4
+
5
+ module Growth
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace Growth
8
+ end
9
+ end
@@ -0,0 +1,42 @@
1
+ module Growth
2
+ module Operations
3
+ module RetentionReport
4
+ class Generate
5
+ include Dry::Transaction::Operation
6
+
7
+ def call(input)
8
+ report = []
9
+ input[:grouped_resources_count].each do |count, source_resources_ids|
10
+ report.push(
11
+ {
12
+ total_source_resources_percentage: calculate_percentage(source_resources_ids.count, input[:resources_distinct_count]),
13
+ total_source_resources: source_resources_ids.count,
14
+ total_target_resources: count,
15
+ total_source_resources_ids: source_resources_ids.sort
16
+ }
17
+ )
18
+ end
19
+
20
+ Success(
21
+ {
22
+ report: {
23
+ source_resource: input[:source_resource],
24
+ target_resource: input[:target_resource],
25
+ total_associated_resources: input[:resources_distinct_count],
26
+ total_target_resources: input[:target_resource].count,
27
+ resources_stats: report
28
+ }
29
+ }
30
+ )
31
+ end
32
+
33
+ private
34
+
35
+ def calculate_percentage(number, total)
36
+ ((number.to_f / total.to_f) * 100).round(2)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
@@ -0,0 +1,33 @@
1
+ module Growth
2
+ module Operations
3
+ module RetentionReport
4
+ class Prepare
5
+ include Dry::Transaction::Operation
6
+
7
+ def call(input)
8
+ begin
9
+ source_resource, target_resource = input[:associations].split('-').map(&:constantize)
10
+ resources = source_resource.unscoped.joins(target_resource.to_s.pluralize.underscore.to_sym)
11
+ grouped_resources = resources.group(:id).order("#{target_resource.to_s.pluralize.underscore}.count ASC")
12
+
13
+ Success({
14
+ source_resource: source_resource,
15
+ target_resource: target_resource,
16
+ grouped_resources_count: invert(grouped_resources.count),
17
+ resources_distinct_count: resources.distinct.count
18
+ })
19
+ rescue => e
20
+ Failure({report: {resources_stats: []}, error: e})
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def invert(hash)
27
+ hash.each_with_object({}) {|(k, v), o| (o[v] ||= []) << k}
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,18 @@
1
+ module Growth
2
+ module Operations
3
+ module RetentionReport
4
+ class Validate
5
+ include Dry::Transaction::Operation
6
+
7
+ def call(input)
8
+ if input[:associations].blank?
9
+ Failure({report: {resources_stats: []}})
10
+ else
11
+ Success(input)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,31 @@
1
+ require "dry/transaction/operation"
2
+
3
+ require_relative '../operations/retention_report/validate'
4
+ require_relative '../operations/retention_report/prepare'
5
+ require_relative '../operations/retention_report/generate'
6
+
7
+ module Growth
8
+ module System
9
+ class Container
10
+ extend Dry::Container::Mixin
11
+
12
+ namespace "growth" do
13
+ namespace "operations" do
14
+ namespace "retention_report" do
15
+ register "validate" do
16
+ Growth::Operations::RetentionReport::Validate.new
17
+ end
18
+
19
+ register "prepare" do
20
+ Growth::Operations::RetentionReport::Prepare.new
21
+ end
22
+
23
+ register "generate" do
24
+ Growth::Operations::RetentionReport::Generate.new
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ require_relative '../system/container'
2
+
3
+ module Growth
4
+ module Transactions
5
+ class GenerateRetentionReport
6
+ include Dry::Transaction(container: Growth::System::Container)
7
+
8
+ step :validate, with: "growth.operations.retention_report.validate"
9
+ step :prepare, with: "growth.operations.retention_report.prepare"
10
+ step :generate, with: "growth.operations.retention_report.generate"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module Growth
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :growth do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: growth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ryan Friedman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-10-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-container
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-transaction
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: groupdate
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Make what you measure and get beatiful charts and statistics about your
84
+ Rails application
85
+ email:
86
+ - ryan@soundtribe.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - MIT-LICENSE
92
+ - README.md
93
+ - Rakefile
94
+ - app/assets/config/growth_manifest.js
95
+ - app/assets/images/growth/baseline-trending_down-24px.svg
96
+ - app/assets/images/growth/baseline-trending_up-24px.svg
97
+ - app/assets/javascripts/growth/application.js
98
+ - app/assets/stylesheets/growth/application.scss
99
+ - app/assets/stylesheets/growth/sidebar.scss
100
+ - app/assets/stylesheets/growth/stats.scss
101
+ - app/controllers/growth/application_controller.rb
102
+ - app/controllers/growth/stats_controller.rb
103
+ - app/helpers/growth/application_helper.rb
104
+ - app/views/growth/stats/_associations.html.erb
105
+ - app/views/growth/stats/_growth.html.erb
106
+ - app/views/growth/stats/_report.html.erb
107
+ - app/views/growth/stats/_sidebar.html.erb
108
+ - app/views/growth/stats/_totals.html.erb
109
+ - app/views/growth/stats/index.html.erb
110
+ - app/views/growth/stats/show.html.erb
111
+ - app/views/layouts/growth/application.html.erb
112
+ - config/routes.rb
113
+ - lib/generators/growth/install_generator.rb
114
+ - lib/generators/templates/growth.rb
115
+ - lib/growth.rb
116
+ - lib/growth/engine.rb
117
+ - lib/growth/operations/retention_report/generate.rb
118
+ - lib/growth/operations/retention_report/prepare.rb
119
+ - lib/growth/operations/retention_report/validate.rb
120
+ - lib/growth/system/container.rb
121
+ - lib/growth/transactions/generate_retention_report.rb
122
+ - lib/growth/version.rb
123
+ - lib/tasks/growth_tasks.rake
124
+ homepage: http://vibrantlight.co
125
+ licenses:
126
+ - MIT
127
+ metadata: {}
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubyforge_project:
144
+ rubygems_version: 2.6.14
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: Monitor the growth of your Rails database
148
+ test_files: []