growth 0.1.0

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 (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: []