blazer 2.3.1 → 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 +6 -0
- data/README.md +28 -10
- data/app/assets/stylesheets/blazer/application.css +4 -0
- data/app/controllers/blazer/base_controller.rb +8 -0
- data/app/controllers/blazer/dashboards_controller.rb +2 -0
- data/app/controllers/blazer/queries_controller.rb +108 -1
- data/app/models/blazer/query.rb +7 -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/run.html.erb +15 -17
- data/app/views/blazer/queries/show.html.erb +1 -1
- data/lib/blazer/adapters/base_adapter.rb +8 -0
- data/lib/blazer/adapters/sql_adapter.rb +41 -0
- data/lib/blazer/data_source.rb +1 -1
- data/lib/blazer/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c9f173935d9c63e3537014420daf0af3017f6f9c51615d102fb6f0b4b3b45ba8
|
4
|
+
data.tar.gz: 8743bc1783f5181bd946d6263a70ac7ae02cbb26d3663b59c8915cdc9495e80f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bfe97805e455f1ec96cc825563460a303006c6c379f9adfb3b24ed2f9f3b86f57f9600648a8fbc01a564ba62f78346582659dab9c121d239f91383211a3b8ad3
|
7
|
+
data.tar.gz: 52a1ce2a7a489105ba1c1ece23e412117de868bf6c50a07b968056b6ccbbe40d90031087c836f7831d04f12bd7d88593260a0a98bfdcb15659e1158c3fc070d6
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -25,6 +25,7 @@ Blazer is also available as a [Docker image](https://github.com/ankane/blazer-do
|
|
25
25
|
- [Charts](#charts)
|
26
26
|
- [Dashboards](#dashboards)
|
27
27
|
- [Checks](#checks)
|
28
|
+
- [Cohorts](#cohorts)
|
28
29
|
- [Anomaly Detection](#anomaly-detection)
|
29
30
|
- [Forecasting](#forecasting)
|
30
31
|
- [Uploads](#uploads)
|
@@ -388,6 +389,31 @@ SELECT * FROM ratings WHERE user_id IS NULL /* all ratings should have a user */
|
|
388
389
|
|
389
390
|
Then create check with optional emails if you want to be notified. Emails are sent when a check starts failing, and when it starts passing again.
|
390
391
|
|
392
|
+
## Cohorts
|
393
|
+
|
394
|
+
Create a cohort analysis from a simple SQL query. [Example](https://blazer.dokkuapp.com/queries/19-cohort-analysis-from-first-order)
|
395
|
+
|
396
|
+
Create a query with the comment `/* cohort analysis */`. The result should have columns named `user_id` and `conversion_time` and optionally `cohort_time`.
|
397
|
+
|
398
|
+
You can generate cohorts from the first conversion time:
|
399
|
+
|
400
|
+
```sql
|
401
|
+
/* cohort analysis */
|
402
|
+
SELECT user_id, created_at AS conversion_time FROM orders
|
403
|
+
```
|
404
|
+
|
405
|
+
(the first conversion isn’t counted in the first time period with this format)
|
406
|
+
|
407
|
+
Or from another time, like sign up:
|
408
|
+
|
409
|
+
```sql
|
410
|
+
/* cohort analysis */
|
411
|
+
SELECT users.id AS user_id, orders.created_at AS conversion_time, users.created_at AS cohort_time
|
412
|
+
FROM users LEFT JOIN orders ON orders.user_id = users.id
|
413
|
+
```
|
414
|
+
|
415
|
+
This feature requires PostgreSQL.
|
416
|
+
|
391
417
|
## Anomaly Detection
|
392
418
|
|
393
419
|
Blazer supports two different approaches to anomaly detection.
|
@@ -456,7 +482,7 @@ Commit and deploy away. The first deploy may take a few minutes.
|
|
456
482
|
|
457
483
|
## Forecasting
|
458
484
|
|
459
|
-
Blazer
|
485
|
+
Blazer supports for two different forecasting methods. [Example](https://blazer.dokkuapp.com/queries/18-forecast?forecast=t)
|
460
486
|
|
461
487
|
A forecast link will appear for queries that return 2 columns with types timestamp and numeric.
|
462
488
|
|
@@ -492,7 +518,7 @@ forecasting: trend
|
|
492
518
|
|
493
519
|
## Uploads
|
494
520
|
|
495
|
-
|
521
|
+
Creating database tables from CSV files. [Example](https://blazer.dokkuapp.com/uploads)
|
496
522
|
|
497
523
|
Run:
|
498
524
|
|
@@ -657,8 +683,6 @@ data_sources:
|
|
657
683
|
|
658
684
|
### InfluxDB
|
659
685
|
|
660
|
-
*Experimental*
|
661
|
-
|
662
686
|
Add [influxdb](https://github.com/influxdata/influxdb-ruby) to your Gemfile and set:
|
663
687
|
|
664
688
|
```yml
|
@@ -692,8 +716,6 @@ data_sources:
|
|
692
716
|
|
693
717
|
### Neo4j
|
694
718
|
|
695
|
-
*Experimental*
|
696
|
-
|
697
719
|
Add [neo4j-core](https://github.com/neo4jrb/neo4j-core) to your Gemfile and set:
|
698
720
|
|
699
721
|
```yml
|
@@ -735,8 +757,6 @@ data_sources:
|
|
735
757
|
|
736
758
|
### Salesforce
|
737
759
|
|
738
|
-
*Experimental*
|
739
|
-
|
740
760
|
Add [restforce](https://github.com/restforce/restforce) to your Gemfile and set:
|
741
761
|
|
742
762
|
```yml
|
@@ -760,8 +780,6 @@ Supports [SOQL](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta
|
|
760
780
|
|
761
781
|
### Socrata Open Data API (SODA)
|
762
782
|
|
763
|
-
*Experimental*
|
764
|
-
|
765
783
|
Set:
|
766
784
|
|
767
785
|
```yml
|
@@ -6,6 +6,8 @@ module Blazer
|
|
6
6
|
skip_after_action(*filters, raise: false)
|
7
7
|
skip_around_action(*filters, raise: false)
|
8
8
|
|
9
|
+
clear_helpers
|
10
|
+
|
9
11
|
protect_from_forgery with: :exception
|
10
12
|
|
11
13
|
if ENV["BLAZER_PASSWORD"]
|
@@ -65,6 +67,12 @@ module Blazer
|
|
65
67
|
end
|
66
68
|
end
|
67
69
|
|
70
|
+
def add_cohort_analysis_vars
|
71
|
+
@bind_vars << "cohort_period" unless @bind_vars.include?("cohort_period")
|
72
|
+
@smart_vars["cohort_period"] = ["day", "week", "month"]
|
73
|
+
params[:cohort_period] ||= "week"
|
74
|
+
end
|
75
|
+
|
68
76
|
def parse_smart_variables(var, data_source)
|
69
77
|
smart_var_data_source =
|
70
78
|
([data_source] + Array(data_source.settings["inherit_smart_settings"]).map { |ds| Blazer.data_sources[ds] }).find { |ds| ds.smart_variables[var] }
|
@@ -74,6 +74,8 @@ module Blazer
|
|
74
74
|
@query.update!(status: "active") if @query.try(:status) == "archived"
|
75
75
|
|
76
76
|
Blazer.transform_statement.call(data_source, @statement) if Blazer.transform_statement
|
77
|
+
|
78
|
+
add_cohort_analysis_vars if @query.cohort_analysis?
|
77
79
|
end
|
78
80
|
|
79
81
|
def edit
|
@@ -81,6 +83,8 @@ module Blazer
|
|
81
83
|
|
82
84
|
def run
|
83
85
|
@statement = params[:statement]
|
86
|
+
# before process_vars
|
87
|
+
@cohort_analysis = Query.new(statement: @statement).cohort_analysis?
|
84
88
|
data_source = params[:data_source]
|
85
89
|
process_vars(@statement, data_source)
|
86
90
|
@only_chart = params[:only_chart]
|
@@ -89,6 +93,8 @@ module Blazer
|
|
89
93
|
data_source = @query.data_source if @query && @query.data_source
|
90
94
|
@data_source = Blazer.data_sources[data_source]
|
91
95
|
|
96
|
+
run_cohort_analysis if @cohort_analysis
|
97
|
+
|
92
98
|
# ensure viewable
|
93
99
|
if !(@query || Query.new(data_source: @data_source.id)).viewable?(blazer_user)
|
94
100
|
render_forbidden
|
@@ -164,6 +170,7 @@ module Blazer
|
|
164
170
|
@statement = @query.statement.dup
|
165
171
|
process_vars(@statement, @query.data_source)
|
166
172
|
Blazer.transform_statement.call(data_source, @statement) if Blazer.transform_statement
|
173
|
+
@statement = cohort_analysis_statement(data_source, @statement) if @query.cohort_analysis?
|
167
174
|
data_source.clear_cache(@statement)
|
168
175
|
redirect_to query_path(@query, variable_params(@query))
|
169
176
|
end
|
@@ -241,7 +248,6 @@ module Blazer
|
|
241
248
|
end
|
242
249
|
end
|
243
250
|
|
244
|
-
@filename = @query.name.parameterize if @query
|
245
251
|
@min_width_types = @columns.each_with_index.select { |c, i| @first_row[i].is_a?(Time) || @first_row[i].is_a?(String) || @data_source.smart_columns[c] }.map(&:last)
|
246
252
|
|
247
253
|
@boom = @result.boom if @result
|
@@ -268,6 +274,8 @@ module Blazer
|
|
268
274
|
end
|
269
275
|
end
|
270
276
|
|
277
|
+
render_cohort_analysis if @cohort_analysis && !@error
|
278
|
+
|
271
279
|
respond_to do |format|
|
272
280
|
format.html do
|
273
281
|
render layout: false
|
@@ -352,5 +360,104 @@ module Blazer
|
|
352
360
|
def blazer_run_id
|
353
361
|
params[:run_id].to_s.gsub(/[^a-z0-9\-]/i, "")
|
354
362
|
end
|
363
|
+
|
364
|
+
def run_cohort_analysis
|
365
|
+
unless @data_source.supports_cohort_analysis?
|
366
|
+
@cohort_error = "This data source does not support cohort analysis"
|
367
|
+
end
|
368
|
+
|
369
|
+
@show_cohort_rows = !params[:query_id] || @cohort_error
|
370
|
+
|
371
|
+
unless @show_cohort_rows
|
372
|
+
@statement = cohort_analysis_statement(@data_source, @statement)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
def cohort_analysis_statement(data_source, statement)
|
377
|
+
@cohort_period = params["cohort_period"] || "week"
|
378
|
+
@cohort_period = "week" unless ["day", "week", "month"].include?(@cohort_period)
|
379
|
+
|
380
|
+
# for now
|
381
|
+
@conversion_period = @cohort_period
|
382
|
+
@cohort_days =
|
383
|
+
case @cohort_period
|
384
|
+
when "day"
|
385
|
+
1
|
386
|
+
when "week"
|
387
|
+
7
|
388
|
+
when "month"
|
389
|
+
30
|
390
|
+
end
|
391
|
+
|
392
|
+
data_source.cohort_analysis_statement(statement, period: @cohort_period, days: @cohort_days)
|
393
|
+
end
|
394
|
+
|
395
|
+
def render_cohort_analysis
|
396
|
+
if @show_cohort_rows
|
397
|
+
@cohort_analysis = false
|
398
|
+
|
399
|
+
@row_limit = 1000
|
400
|
+
|
401
|
+
# check results
|
402
|
+
unless @cohort_error
|
403
|
+
# check names
|
404
|
+
expected_columns = ["user_id", "conversion_time"]
|
405
|
+
missing_columns = expected_columns - @result.columns
|
406
|
+
if missing_columns.any?
|
407
|
+
@cohort_error = "Expected user_id and conversion_time columns"
|
408
|
+
end
|
409
|
+
|
410
|
+
# check types (user_id can be any type)
|
411
|
+
unless @cohort_error
|
412
|
+
column_types = @result.columns.zip(@result.column_types).to_h
|
413
|
+
|
414
|
+
if !column_types["cohort_time"].in?(["time", nil])
|
415
|
+
@cohort_error = "cohort_time must be time column"
|
416
|
+
elsif !column_types["conversion_time"].in?(["time", nil])
|
417
|
+
@cohort_error = "conversion_time must be time column"
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
else
|
422
|
+
@today = Blazer.time_zone.today
|
423
|
+
@min_cohort_date, @max_cohort_date = @result.rows.map { |r| r[0] }.minmax
|
424
|
+
@buckets = {}
|
425
|
+
@rows.each do |r|
|
426
|
+
@buckets[[r[0], r[1]]] = r[2]
|
427
|
+
end
|
428
|
+
|
429
|
+
@cohort_dates = []
|
430
|
+
current_date = @max_cohort_date
|
431
|
+
while current_date && current_date >= @min_cohort_date
|
432
|
+
@cohort_dates << current_date
|
433
|
+
current_date =
|
434
|
+
case @cohort_period
|
435
|
+
when "day"
|
436
|
+
current_date - 1
|
437
|
+
when "week"
|
438
|
+
current_date - 7
|
439
|
+
else
|
440
|
+
current_date.prev_month
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
num_cols = @cohort_dates.size
|
445
|
+
@columns = ["Cohort", "Users"] + num_cols.times.map { |i| "#{@conversion_period.titleize} #{i + 1}" }
|
446
|
+
rows = []
|
447
|
+
date_format = @cohort_period == "month" ? "%b %Y" : "%b %-e, %Y"
|
448
|
+
@cohort_dates.each do |date|
|
449
|
+
row = [date.strftime(date_format), @buckets[[date, 0]] || 0]
|
450
|
+
|
451
|
+
num_cols.times do |i|
|
452
|
+
if @today >= date + (@cohort_days * i)
|
453
|
+
row << (@buckets[[date, i + 1]] || 0)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
rows << row
|
458
|
+
end
|
459
|
+
@rows = rows
|
460
|
+
end
|
461
|
+
end
|
355
462
|
end
|
356
463
|
end
|
data/app/models/blazer/query.rb
CHANGED
@@ -35,7 +35,13 @@ module Blazer
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def variables
|
38
|
-
Blazer.extract_vars(statement)
|
38
|
+
variables = Blazer.extract_vars(statement)
|
39
|
+
variables += ["cohort_period"] if cohort_analysis?
|
40
|
+
variables
|
41
|
+
end
|
42
|
+
|
43
|
+
def cohort_analysis?
|
44
|
+
/\/\*\s*cohort analysis\s*\*\//i.match?(statement)
|
39
45
|
end
|
40
46
|
end
|
41
47
|
end
|
@@ -39,7 +39,7 @@
|
|
39
39
|
</div>
|
40
40
|
</div>
|
41
41
|
<script>
|
42
|
-
<%= blazer_js_var "data", {statement: @statements[i], query_id: query.id, data_source: query.data_source, only_chart: true} %>
|
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 %>
|
@@ -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 %>
|
@@ -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>
|
@@ -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 = [])
|
data/lib/blazer/data_source.rb
CHANGED
@@ -6,7 +6,7 @@ module Blazer
|
|
6
6
|
|
7
7
|
attr_reader :id, :settings
|
8
8
|
|
9
|
-
def_delegators :adapter_instance, :schema, :tables, :preview_statement, :reconnect, :cost, :explain, :cancel
|
9
|
+
def_delegators :adapter_instance, :schema, :tables, :preview_statement, :reconnect, :cost, :explain, :cancel, :supports_cohort_analysis?, :cohort_analysis_statement
|
10
10
|
|
11
11
|
def initialize(id, settings)
|
12
12
|
@id = id
|
data/lib/blazer/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: blazer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-12-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: railties
|
@@ -170,6 +170,8 @@ files:
|
|
170
170
|
- app/views/blazer/dashboards/edit.html.erb
|
171
171
|
- app/views/blazer/dashboards/new.html.erb
|
172
172
|
- app/views/blazer/dashboards/show.html.erb
|
173
|
+
- app/views/blazer/queries/_caching.html.erb
|
174
|
+
- app/views/blazer/queries/_cohorts.html.erb
|
173
175
|
- app/views/blazer/queries/_form.html.erb
|
174
176
|
- app/views/blazer/queries/docs.html.erb
|
175
177
|
- app/views/blazer/queries/edit.html.erb
|