blazer 2.2.6 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of blazer might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/LICENSE.txt +1 -1
- data/README.md +105 -29
- data/app/assets/javascripts/blazer/Chart.js +13794 -12099
- data/app/assets/javascripts/blazer/Sortable.js +3695 -1526
- data/app/assets/javascripts/blazer/chartkick.js +296 -46
- data/app/assets/javascripts/blazer/daterangepicker.js +194 -269
- data/app/assets/javascripts/blazer/jquery.js +1150 -642
- data/app/assets/javascripts/blazer/moment-timezone-with-data.js +621 -287
- data/app/assets/javascripts/blazer/moment.js +5085 -2460
- data/app/assets/stylesheets/blazer/application.css +4 -0
- data/app/assets/stylesheets/blazer/daterangepicker.css +394 -253
- data/app/controllers/blazer/base_controller.rb +12 -4
- data/app/controllers/blazer/dashboards_controller.rb +7 -2
- data/app/controllers/blazer/queries_controller.rb +119 -3
- data/app/controllers/blazer/uploads_controller.rb +147 -0
- data/app/mailers/blazer/slack_notifier.rb +1 -1
- data/app/models/blazer/query.rb +8 -1
- data/app/models/blazer/upload.rb +11 -0
- data/app/models/blazer/uploads_connection.rb +7 -0
- data/app/views/blazer/_nav.html.erb +3 -0
- data/app/views/blazer/_variables.html.erb +3 -1
- data/app/views/blazer/checks/_form.html.erb +1 -1
- data/app/views/blazer/checks/index.html.erb +3 -0
- data/app/views/blazer/dashboards/_form.html.erb +1 -1
- data/app/views/blazer/dashboards/show.html.erb +1 -1
- data/app/views/blazer/queries/_caching.html.erb +16 -0
- data/app/views/blazer/queries/_cohorts.html.erb +48 -0
- data/app/views/blazer/queries/docs.html.erb +6 -0
- data/app/views/blazer/queries/home.html.erb +3 -0
- data/app/views/blazer/queries/run.html.erb +20 -22
- data/app/views/blazer/queries/show.html.erb +1 -1
- data/app/views/blazer/uploads/_form.html.erb +27 -0
- data/app/views/blazer/uploads/edit.html.erb +3 -0
- data/app/views/blazer/uploads/index.html.erb +55 -0
- data/app/views/blazer/uploads/new.html.erb +3 -0
- data/config/routes.rb +5 -0
- data/lib/blazer.rb +18 -0
- data/lib/blazer/adapters/base_adapter.rb +8 -0
- data/lib/blazer/adapters/sql_adapter.rb +42 -1
- data/lib/blazer/data_source.rb +1 -1
- data/lib/blazer/version.rb +1 -1
- data/lib/generators/blazer/templates/config.yml.tt +6 -0
- data/lib/generators/blazer/templates/install.rb.tt +3 -2
- data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
- data/lib/generators/blazer/uploads_generator.rb +18 -0
- data/lib/tasks/blazer.rake +9 -0
- data/licenses/LICENSE-ace.txt +24 -0
- data/licenses/LICENSE-bootstrap.txt +21 -0
- data/licenses/LICENSE-chart.js.txt +9 -0
- data/licenses/LICENSE-chartkick.js.txt +22 -0
- data/licenses/LICENSE-daterangepicker.txt +21 -0
- data/licenses/LICENSE-fuzzysearch.txt +20 -0
- data/licenses/LICENSE-highlight.js.txt +29 -0
- data/licenses/LICENSE-jquery-ujs.txt +20 -0
- data/licenses/LICENSE-jquery.txt +20 -0
- data/licenses/LICENSE-moment-timezone.txt +20 -0
- data/licenses/LICENSE-moment.txt +22 -0
- data/licenses/LICENSE-selectize.txt +202 -0
- data/licenses/LICENSE-sortable.txt +21 -0
- data/licenses/LICENSE-stickytableheaders.txt +20 -0
- data/licenses/LICENSE-stupidtable.txt +19 -0
- data/licenses/LICENSE-vue.txt +21 -0
- metadata +34 -7
@@ -6,6 +6,9 @@
|
|
6
6
|
</button>
|
7
7
|
<ul class="dropdown-menu">
|
8
8
|
<li><%= link_to "Checks", checks_path %></li>
|
9
|
+
<% if Blazer.uploads? %>
|
10
|
+
<li><%= link_to "Uploads", uploads_path %></li>
|
11
|
+
<% end %>
|
9
12
|
<li role="separator" class="divider"></li>
|
10
13
|
<li><%= link_to "New Query", new_query_path %></li>
|
11
14
|
<li><%= link_to "New Dashboard", new_dashboard_path %></li>
|
@@ -42,6 +42,7 @@
|
|
42
42
|
singleDatePicker: true,
|
43
43
|
locale: {format: format},
|
44
44
|
autoUpdateInput: false,
|
45
|
+
autoApply: true,
|
45
46
|
startDate: input.val().length > 0 ? moment.tz(input.val(), timeZone) : now
|
46
47
|
})
|
47
48
|
// hack to start with empty date
|
@@ -95,7 +96,8 @@
|
|
95
96
|
},
|
96
97
|
startDate: dateStr(29),
|
97
98
|
endDate: dateStr(),
|
98
|
-
opens: "right"
|
99
|
+
opens: "right",
|
100
|
+
alwaysShowCalendars: true
|
99
101
|
},
|
100
102
|
function(start, end) {
|
101
103
|
setTimeInputs(start, end)
|
@@ -13,7 +13,7 @@
|
|
13
13
|
<%= f.select :query_id, [], {include_blank: true} %>
|
14
14
|
</div>
|
15
15
|
<script>
|
16
|
-
<%= blazer_js_var "queries", Blazer::Query.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
|
16
|
+
<%= blazer_js_var "queries", Blazer::Query.active.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
|
17
17
|
<%= blazer_js_var "items", [@check.query_id].compact %>
|
18
18
|
|
19
19
|
$("#check_query_id").selectize({options: queries, items: items, highlight: false, maxOptions: 100}).parents(".hide").removeClass("hide");
|
@@ -10,6 +10,9 @@
|
|
10
10
|
</button>
|
11
11
|
<ul class="dropdown-menu">
|
12
12
|
<li><%= link_to "Home", root_path %></li>
|
13
|
+
<% if Blazer.uploads? %>
|
14
|
+
<li><%= link_to "Uploads", uploads_path %></li>
|
15
|
+
<% end %>
|
13
16
|
<li role="separator" class="divider"></li>
|
14
17
|
<li><%= link_to "New Query", new_query_path %></li>
|
15
18
|
<li><%= link_to "New Dashboard", new_dashboard_path %></li>
|
@@ -31,7 +31,7 @@
|
|
31
31
|
<% end %>
|
32
32
|
|
33
33
|
<script>
|
34
|
-
<%= blazer_js_var "queries", Blazer::Query.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
|
34
|
+
<%= blazer_js_var "queries", Blazer::Query.active.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
|
35
35
|
<%= blazer_js_var "dashboardQueries", @queries || @dashboard.dashboard_queries.order(:position).map(&:query) %>
|
36
36
|
|
37
37
|
var app = new Vue({
|
@@ -39,7 +39,7 @@
|
|
39
39
|
</div>
|
40
40
|
</div>
|
41
41
|
<script>
|
42
|
-
<%= blazer_js_var "data", {statement:
|
42
|
+
<%= blazer_js_var "data", {statement: @statements[i], query_id: query.id, data_source: query.data_source, only_chart: true, cohort_period: params[:cohort_period]} %>
|
43
43
|
|
44
44
|
runQuery(data, function (data) {
|
45
45
|
$("#chart-<%= i %>").html(data)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<% if @cached_at || @just_cached %>
|
2
|
+
<p class="text-muted" style="float: right;">
|
3
|
+
<% if @cached_at %>
|
4
|
+
Cached <%= time_ago_in_words(@cached_at, include_seconds: true) %> ago
|
5
|
+
<% elsif params[:query_id] %>
|
6
|
+
Cached just now
|
7
|
+
<% if @data_source.cache_mode == "slow" %>
|
8
|
+
(over <%= "%g" % @data_source.cache_slow_threshold %>s)
|
9
|
+
<% end %>
|
10
|
+
<% end %>
|
11
|
+
|
12
|
+
<% if @query && params[:query_id] %>
|
13
|
+
<%= link_to "Refresh", refresh_query_path(@query, variable_params(@query)), method: :post %>
|
14
|
+
<% end %>
|
15
|
+
</p>
|
16
|
+
<% end %>
|
@@ -0,0 +1,48 @@
|
|
1
|
+
<% unless @only_chart %>
|
2
|
+
<%= render partial: "caching" %>
|
3
|
+
<p class="text-muted" style="margin-bottom: 10px;">
|
4
|
+
<%= pluralize(@rows.size, "cohort") %>
|
5
|
+
</p>
|
6
|
+
<% end %>
|
7
|
+
<% if @rows.any? %>
|
8
|
+
<div class="results-container">
|
9
|
+
<table class="table results-table">
|
10
|
+
<thead>
|
11
|
+
<tr>
|
12
|
+
<th style="min-width: 100px;">Cohort</th>
|
13
|
+
<% 12.times do |i| %>
|
14
|
+
<th style="width: 7.5%; text-align: right;"><%= @conversion_period.titleize %> <%= i + 1 %></th>
|
15
|
+
<% end %>
|
16
|
+
</tr>
|
17
|
+
</thead>
|
18
|
+
<tbody>
|
19
|
+
<% @rows.each do |row| %>
|
20
|
+
<tr>
|
21
|
+
<td>
|
22
|
+
<%= row[0] %>
|
23
|
+
<div style="font-size: 12px; color: #999;"><%= row[1] == 1 ? "1 user" : "#{number_with_delimiter(row[1])} users" %></div>
|
24
|
+
</td>
|
25
|
+
<% 12.times do |i| %>
|
26
|
+
<td style="text-align: right;">
|
27
|
+
<% num = row[i + 2] %>
|
28
|
+
<% if num %>
|
29
|
+
<% denom = row[1] %>
|
30
|
+
<% if denom > 0 %>
|
31
|
+
<%= (100.0 * num / denom).round %>%
|
32
|
+
<% else %>
|
33
|
+
-
|
34
|
+
<% end %>
|
35
|
+
<div style="font-size: 12px; color: #999;"><%= number_with_delimiter(num) %></div>
|
36
|
+
<% else %>
|
37
|
+
-
|
38
|
+
<% end %>
|
39
|
+
</td>
|
40
|
+
<% end %>
|
41
|
+
</tr>
|
42
|
+
<% end %>
|
43
|
+
</tbody>
|
44
|
+
</table>
|
45
|
+
</div>
|
46
|
+
<% elsif @only_chart %>
|
47
|
+
<p class="text-muted">No cohorts</p>
|
48
|
+
<% end %>
|
@@ -129,3 +129,9 @@
|
|
129
129
|
</table>
|
130
130
|
|
131
131
|
<p>Use the column name <code>target</code> to draw a line for goals.</p>
|
132
|
+
|
133
|
+
<% if @data_source.supports_cohort_analysis? %>
|
134
|
+
<h2>Cohort Analysis</h2>
|
135
|
+
|
136
|
+
<p>Create a query with the comment <code>/* cohort analysis */</code>. The result should have columns named <code>user_id</code> and <code>conversion_time</code> and optionally <code>cohort_time</code>.</p>
|
137
|
+
<% end %>
|
@@ -19,6 +19,9 @@
|
|
19
19
|
</button>
|
20
20
|
<ul class="dropdown-menu">
|
21
21
|
<li><%= link_to "Checks", checks_path %></li>
|
22
|
+
<% if Blazer.uploads? %>
|
23
|
+
<li><%= link_to "Uploads", uploads_path %></li>
|
24
|
+
<% end %>
|
22
25
|
<li role="separator" class="divider"></li>
|
23
26
|
<li><%= link_to "New Dashboard", new_dashboard_path %></li>
|
24
27
|
<li><%= link_to "New Check", new_check_path %></li>
|
@@ -6,25 +6,20 @@
|
|
6
6
|
<% else %>
|
7
7
|
<div class="alert alert-info">Can’t preview queries with variables...yet!</div>
|
8
8
|
<% end %>
|
9
|
+
<% elsif @cohort_analysis %>
|
10
|
+
<% if @cohort_error %>
|
11
|
+
<div class="alert alert-info"><%= @cohort_error %></div>
|
12
|
+
<% else %>
|
13
|
+
<%= render partial: "cohorts" %>
|
14
|
+
<% end %>
|
9
15
|
<% else %>
|
10
16
|
<% unless @only_chart %>
|
11
|
-
|
12
|
-
<p class="text-muted" style="float: right;">
|
13
|
-
<% if @cached_at %>
|
14
|
-
Cached <%= time_ago_in_words(@cached_at, include_seconds: true) %> ago
|
15
|
-
<% elsif params[:query_id] %>
|
16
|
-
Cached just now
|
17
|
-
<% if @data_source.cache_mode == "slow" %>
|
18
|
-
(over <%= "%g" % @data_source.cache_slow_threshold %>s)
|
19
|
-
<% end %>
|
20
|
-
<% end %>
|
21
|
-
|
22
|
-
<% if @query && params[:query_id] %>
|
23
|
-
<%= link_to "Refresh", refresh_query_path(@query, variable_params(@query)), method: :post %>
|
24
|
-
<% end %>
|
25
|
-
</p>
|
26
|
-
<% end %>
|
17
|
+
<%= render partial: "caching" %>
|
27
18
|
<p class="text-muted" style="margin-bottom: 10px;">
|
19
|
+
<% if @row_limit && @rows.size > @row_limit %>
|
20
|
+
First
|
21
|
+
<% @rows = @rows.first(@row_limit) %>
|
22
|
+
<% end %>
|
28
23
|
<%= pluralize(@rows.size, "row") %>
|
29
24
|
|
30
25
|
<% @checks.select(&:state).each do |check| %>
|
@@ -43,6 +38,9 @@
|
|
43
38
|
<% if @forecast_error %>
|
44
39
|
<div class="alert alert-danger"><%= @forecast_error %></div>
|
45
40
|
<% end %>
|
41
|
+
<% if @cohort_error %>
|
42
|
+
<div class="alert alert-info"><%= @cohort_error %></div>
|
43
|
+
<% end %>
|
46
44
|
<% if @rows.any? %>
|
47
45
|
<% values = @rows.first %>
|
48
46
|
<% chart_id = SecureRandom.hex %>
|
@@ -107,13 +105,13 @@
|
|
107
105
|
</script>
|
108
106
|
<% elsif chart_type == "line" %>
|
109
107
|
<% chart_data = @columns[1..-1].each_with_index.map{ |k, i| {name: blazer_series_name(k), data: @rows.map{ |r| [r[0], r[i + 1]] }, library: series_library[i]} } %>
|
110
|
-
<%= line_chart chart_data, chart_options %>
|
108
|
+
<%= line_chart chart_data, **chart_options %>
|
111
109
|
<% elsif chart_type == "line2" %>
|
112
|
-
<%= line_chart @rows.group_by { |r| v = r[1]; (@boom[@columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: blazer_series_name(name), data: v.map { |v2| [v2[0], v2[2]] }, library: series_library[i]} }, chart_options %>
|
110
|
+
<%= line_chart @rows.group_by { |r| v = r[1]; (@boom[@columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: blazer_series_name(name), data: v.map { |v2| [v2[0], v2[2]] }, library: series_library[i]} }, **chart_options %>
|
113
111
|
<% elsif chart_type == "pie" %>
|
114
|
-
<%= pie_chart @rows.map { |r| [(@boom[@columns[0]] || {})[r[0].to_s] || r[0], r[1]] }, chart_options %>
|
112
|
+
<%= pie_chart @rows.map { |r| [(@boom[@columns[0]] || {})[r[0].to_s] || r[0], r[1]] }, **chart_options %>
|
115
113
|
<% elsif chart_type == "bar" %>
|
116
|
-
<%= column_chart (values.size - 1).times.map { |i| name = @columns[i + 1]; {name: blazer_series_name(name), data: @rows.first(20).map { |r| [(@boom[@columns[0]] || {})[r[0].to_s] || r[0], r[i + 1]] } } }, chart_options %>
|
114
|
+
<%= column_chart (values.size - 1).times.map { |i| name = @columns[i + 1]; {name: blazer_series_name(name), data: @rows.first(20).map { |r| [(@boom[@columns[0]] || {})[r[0].to_s] || r[0], r[i + 1]] } } }, **chart_options %>
|
117
115
|
<% elsif chart_type == "bar2" %>
|
118
116
|
<% first_20 = @rows.group_by { |r| r[0] }.values.first(20).flatten(1) %>
|
119
117
|
<% labels = first_20.map { |r| r[0] }.uniq %>
|
@@ -123,7 +121,7 @@
|
|
123
121
|
<% first_20 << [l, s, 0] unless first_20.find { |r| r[0] == l && r[1] == s } %>
|
124
122
|
<% end %>
|
125
123
|
<% end %>
|
126
|
-
<%= column_chart first_20.group_by { |r| v = r[1]; (@boom[@columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: blazer_series_name(name), data: v.sort_by { |r2| labels.index(r2[0]) }.map { |v2| v3 = v2[0]; [(@boom[@columns[0]] || {})[v3.to_s] || v3, v2[2]] }} }, chart_options %>
|
124
|
+
<%= column_chart first_20.group_by { |r| v = r[1]; (@boom[@columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: blazer_series_name(name), data: v.sort_by { |r2| labels.index(r2[0]) }.map { |v2| v3 = v2[0]; [(@boom[@columns[0]] || {})[v3.to_s] || v3, v2[2]] }} }, **chart_options %>
|
127
125
|
<% elsif chart_type == "scatter" %>
|
128
126
|
<%= scatter_chart @rows, xtitle: @columns[0], ytitle: @columns[1], **chart_options %>
|
129
127
|
<% elsif @only_chart %>
|
@@ -147,7 +145,7 @@
|
|
147
145
|
<% elsif @columns == ["PLAN"] && @data_source.adapter == "druid" %>
|
148
146
|
<pre><code><%= @rows[0][0] %></code></pre>
|
149
147
|
<% else %>
|
150
|
-
<table class="table results-table"
|
148
|
+
<table class="table results-table">
|
151
149
|
<thead>
|
152
150
|
<tr>
|
153
151
|
<% @columns.each_with_index do |key, i| %>
|
@@ -14,7 +14,7 @@
|
|
14
14
|
<%= link_to "Fork", new_query_path(variable_params(@query).merge(fork_query_id: @query.id, data_source: @query.data_source, name: @query.name)), class: "btn btn-info" %>
|
15
15
|
|
16
16
|
<% if !@error && @success %>
|
17
|
-
<%= button_to "Download", run_queries_path(query_id: @query.id, format: "csv", forecast: params[:forecast]), params: {statement: @statement}, class: "btn btn-primary" %>
|
17
|
+
<%= button_to "Download", run_queries_path(query_id: @query.id, format: "csv", forecast: params[:forecast], cohort_period: params[:cohort_period]), params: {statement: @statement}, class: "btn btn-primary" %>
|
18
18
|
<% end %>
|
19
19
|
</div>
|
20
20
|
</div>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<%= form_for @upload, html: {class: "small-form"} do |f| %>
|
2
|
+
<% if @upload.errors.any? %>
|
3
|
+
<div class="alert alert-danger"><%= @upload.errors.full_messages.first %></div>
|
4
|
+
<% elsif !@upload.persisted? %>
|
5
|
+
<p>Create a database table from a CSV file. The table will be created in the <code><%= Blazer.settings["uploads"]["schema"] %></code> schema.</p>
|
6
|
+
<% end %>
|
7
|
+
|
8
|
+
<div class="form-group">
|
9
|
+
<%= f.label :table %>
|
10
|
+
<%= f.text_field :table, class: "form-control" %>
|
11
|
+
</div>
|
12
|
+
<div class="form-group">
|
13
|
+
<%= f.label :description %>
|
14
|
+
<%= f.text_area :description, placeholder: "Optional", style: "height: 60px;", class: "form-control" %>
|
15
|
+
</div>
|
16
|
+
<div class="form-group">
|
17
|
+
<%= f.label :file %>
|
18
|
+
<%= f.file_field :file, accept: "text/csv", style: "margin-top: 6px; margin-bottom: 21px;" %>
|
19
|
+
</div>
|
20
|
+
<p>
|
21
|
+
<% if @upload.persisted? %>
|
22
|
+
<%= link_to "Delete", upload_path(@upload), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
|
23
|
+
<% end %>
|
24
|
+
<%= f.submit "Save", class: "btn btn-success" %>
|
25
|
+
<%= link_to "Back", :back, class: "btn btn-link" %>
|
26
|
+
</p>
|
27
|
+
<% end %>
|
@@ -0,0 +1,55 @@
|
|
1
|
+
<% blazer_title "Uploads" %>
|
2
|
+
|
3
|
+
<div id="header">
|
4
|
+
<div class="pull-right" style="line-height: 34px;">
|
5
|
+
<div class="btn-group">
|
6
|
+
<%= link_to "New Upload", new_upload_path, class: "btn btn-info" %>
|
7
|
+
<button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
8
|
+
<span class="caret"></span>
|
9
|
+
<span class="sr-only">Toggle Dropdown</span>
|
10
|
+
</button>
|
11
|
+
<ul class="dropdown-menu">
|
12
|
+
<li><%= link_to "Home", root_path %></li>
|
13
|
+
<li><%= link_to "Checks", checks_path %></li>
|
14
|
+
<li role="separator" class="divider"></li>
|
15
|
+
<li><%= link_to "New Query", new_query_path %></li>
|
16
|
+
<li><%= link_to "New Dashboard", new_dashboard_path %></li>
|
17
|
+
<li><%= link_to "New Check", new_check_path %></li>
|
18
|
+
</ul>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
|
22
|
+
<input id="search" type="text" placeholder="Start typing a table or person" style="width: 300px; display: inline-block;" class="search form-control" />
|
23
|
+
</div>
|
24
|
+
|
25
|
+
<table id="uploads" class="table">
|
26
|
+
<thead>
|
27
|
+
<tr>
|
28
|
+
<th>Table</th>
|
29
|
+
<th style="width: 60%;"></th>
|
30
|
+
<% if Blazer.user_class %>
|
31
|
+
<th style="width: 20%; text-align: right;">Mastermind</th>
|
32
|
+
<% end%>
|
33
|
+
</tr>
|
34
|
+
</thead>
|
35
|
+
<tbody>
|
36
|
+
<% @uploads.each do |upload| %>
|
37
|
+
<tr>
|
38
|
+
<td><%= link_to upload.table, edit_upload_path(upload) %></td>
|
39
|
+
<td><%= truncate(upload.description, length: 100, separator: " ") %></td>
|
40
|
+
<% if Blazer.user_class %>
|
41
|
+
<td class="creator"><%= blazer_user && upload.creator == blazer_user ? "You" : upload.creator.try(Blazer.user_name) %></td>
|
42
|
+
<% end %>
|
43
|
+
</tr>
|
44
|
+
<% end %>
|
45
|
+
</tbody>
|
46
|
+
</table>
|
47
|
+
|
48
|
+
<script>
|
49
|
+
$("#search").on("keyup", function() {
|
50
|
+
var value = $(this).val().toLowerCase()
|
51
|
+
$("#uploads tbody tr").filter( function() {
|
52
|
+
$(this).toggle($(this).text().toLowerCase().indexOf(value) > -1)
|
53
|
+
})
|
54
|
+
}).focus()
|
55
|
+
</script>
|
data/config/routes.rb
CHANGED
data/lib/blazer.rb
CHANGED
@@ -32,6 +32,7 @@ require "blazer/engine"
|
|
32
32
|
|
33
33
|
module Blazer
|
34
34
|
class Error < StandardError; end
|
35
|
+
class UploadError < Error; end
|
35
36
|
class TimeoutNotSupported < Error; end
|
36
37
|
|
37
38
|
class << self
|
@@ -206,6 +207,23 @@ module Blazer
|
|
206
207
|
slack_webhook_url.present?
|
207
208
|
end
|
208
209
|
|
210
|
+
def self.uploads?
|
211
|
+
settings.key?("uploads")
|
212
|
+
end
|
213
|
+
|
214
|
+
def self.uploads_connection
|
215
|
+
raise "Empty url for uploads" unless settings.dig("uploads", "url")
|
216
|
+
Blazer::UploadsConnection.connection
|
217
|
+
end
|
218
|
+
|
219
|
+
def self.uploads_schema
|
220
|
+
settings.dig("uploads", "schema") || "uploads"
|
221
|
+
end
|
222
|
+
|
223
|
+
def self.uploads_table_name(name)
|
224
|
+
uploads_connection.quote_table_name("#{uploads_schema}.#{name}")
|
225
|
+
end
|
226
|
+
|
209
227
|
def self.adapters
|
210
228
|
@adapters ||= {}
|
211
229
|
end
|
@@ -27,7 +27,7 @@ module Blazer
|
|
27
27
|
result = select_all("#{statement} /*#{comment}*/")
|
28
28
|
columns = result.columns
|
29
29
|
result.rows.each do |untyped_row|
|
30
|
-
rows << (result.column_types.empty? ? untyped_row : columns.each_with_index.map { |c, i| untyped_row[i] ? result.column_types[c].send(:cast_value, untyped_row[i]) : untyped_row[i] })
|
30
|
+
rows << (result.column_types.empty? ? untyped_row : columns.each_with_index.map { |c, i| untyped_row[i] && result.column_types[c] ? result.column_types[c].send(:cast_value, untyped_row[i]) : untyped_row[i] })
|
31
31
|
end
|
32
32
|
end
|
33
33
|
rescue => e
|
@@ -122,6 +122,47 @@ module Blazer
|
|
122
122
|
!%w[CREATE ALTER UPDATE INSERT DELETE].include?(statement.split.first.to_s.upcase)
|
123
123
|
end
|
124
124
|
|
125
|
+
def supports_cohort_analysis?
|
126
|
+
postgresql?
|
127
|
+
end
|
128
|
+
|
129
|
+
# TODO treat date columns as already in time zone
|
130
|
+
def cohort_analysis_statement(statement, period:, days:)
|
131
|
+
raise "Cohort analysis not supported" unless supports_cohort_analysis?
|
132
|
+
|
133
|
+
cohort_column = statement =~ /\bcohort_time\b/ ? "cohort_time" : "conversion_time"
|
134
|
+
|
135
|
+
# WITH not an optimization fence in Postgres 12+
|
136
|
+
statement = <<~SQL
|
137
|
+
WITH query AS (
|
138
|
+
#{statement}
|
139
|
+
),
|
140
|
+
cohorts AS (
|
141
|
+
SELECT user_id, MIN(#{cohort_column}) AS cohort_time FROM query
|
142
|
+
WHERE user_id IS NOT NULL AND #{cohort_column} IS NOT NULL
|
143
|
+
GROUP BY 1
|
144
|
+
)
|
145
|
+
SELECT
|
146
|
+
date_trunc(?, cohorts.cohort_time::timestamptz AT TIME ZONE ?)::date AS period,
|
147
|
+
0 AS bucket,
|
148
|
+
COUNT(DISTINCT cohorts.user_id)
|
149
|
+
FROM cohorts GROUP BY 1
|
150
|
+
UNION ALL
|
151
|
+
SELECT
|
152
|
+
date_trunc(?, cohorts.cohort_time::timestamptz AT TIME ZONE ?)::date AS period,
|
153
|
+
CEIL(EXTRACT(EPOCH FROM query.conversion_time - cohorts.cohort_time) / ?)::int AS bucket,
|
154
|
+
COUNT(DISTINCT query.user_id)
|
155
|
+
FROM cohorts INNER JOIN query ON query.user_id = cohorts.user_id
|
156
|
+
WHERE query.conversion_time IS NOT NULL
|
157
|
+
AND query.conversion_time >= cohorts.cohort_time
|
158
|
+
#{cohort_column == "conversion_time" ? "AND query.conversion_time != cohorts.cohort_time" : ""}
|
159
|
+
GROUP BY 1, 2
|
160
|
+
SQL
|
161
|
+
tzname = Blazer.time_zone.tzinfo.name
|
162
|
+
params = [statement, period, tzname, period, tzname, days.to_i * 86400]
|
163
|
+
connection_model.send(:sanitize_sql_array, params)
|
164
|
+
end
|
165
|
+
|
125
166
|
protected
|
126
167
|
|
127
168
|
def select_all(statement, params = [])
|