rails_local_analytics 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +7 -0
- data/README.md +220 -0
- data/Rakefile +15 -0
- data/app/assets/config/rails_local_analytics_manifest.js +3 -0
- data/app/assets/javascripts/rails_local_analytics/application.js +7 -0
- data/app/assets/stylesheets/rails_local_analytics/application.css +10 -0
- data/app/controllers/rails_local_analytics/application_controller.rb +5 -0
- data/app/controllers/rails_local_analytics/dashboard_controller.rb +61 -0
- data/app/jobs/rails_local_analytics/application_job.rb +5 -0
- data/app/jobs/rails_local_analytics/record_request_job.rb +122 -0
- data/app/lib/rails_local_analytics/histogram.rb +47 -0
- data/app/models/rails_local_analytics/application_record.rb +34 -0
- data/app/models/tracked_requests_by_day_page.rb +8 -0
- data/app/models/tracked_requests_by_day_site.rb +7 -0
- data/app/views/layouts/rails_local_analytics/application.html.erb +33 -0
- data/app/views/rails_local_analytics/dashboard/index.html.erb +101 -0
- data/config/routes.rb +5 -0
- data/lib/rails_local_analytics/engine.rb +32 -0
- data/lib/rails_local_analytics/version.rb +3 -0
- data/lib/rails_local_analytics.rb +55 -0
- data/lib/tasks/rails_local_analytics_tasks.rake +4 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 43724c92d8b23ef42ba4b5334f39f500d69075ea86e732ec405a3c5157200058
|
4
|
+
data.tar.gz: e1572a460c8bbee78956a431c55afdc208d2cbbc952562b6982a07cc2c24a9c3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ba528aae30b5ba1b5bdf814f78ca87d0b4d6bea70e15b2a5fc036a2ac9f27c529af5d59ea66595e15436edb025c71b10af308546fd38bf32155150175d158599
|
7
|
+
data.tar.gz: 11575f750a08aaafcf066fb9719209c3e5d3a8dd1447210bee1476786d5e48a88b3a070e350de04eaffcfee04fcfe1e56365483b6c28cc94f91d0a90e81ffe4e
|
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2024 Weston Ganger
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
# Rails Local Analytics
|
2
|
+
|
3
|
+
<a href="https://badge.fury.io/rb/rails_local_analytics" target="_blank"><img height="21" style='border:0px;height:21px;' border='0' src="https://badge.fury.io/rb/rails_local_analytics.svg" alt="Gem Version"></a>
|
4
|
+
<a href='https://github.com/westonganger/rails_local_analytics/actions' target='_blank'><img src="https://github.com/westonganger/rails_local_analytics/actions/workflows/test.yml/badge.svg?branch=master" style="max-width:100%;" height='21' style='border:0px;height:21px;' border='0' alt="CI Status"></a>
|
5
|
+
<a href='https://rubygems.org/gems/rails_local_analytics' target='_blank'><img height='21' style='border:0px;height:21px;' src='https://img.shields.io/gem/dt/rails_local_analytics?color=brightgreen&label=Rubygems%20Downloads' border='0' alt='RubyGems Downloads' /></a>
|
6
|
+
|
7
|
+
Simple, performant, local analytics for Rails. Solves 95% of your needs until your ready to start taking analytics more seriously using another tool.
|
8
|
+
|
9
|
+
Out of the box the following request details are tracked:
|
10
|
+
|
11
|
+
- day
|
12
|
+
- total (count per day)
|
13
|
+
- url_hostname (site)
|
14
|
+
- url_path (page)
|
15
|
+
- referrer_hostname
|
16
|
+
- referrer_path
|
17
|
+
- platform (ios, android, linux, osx, windows, etc)
|
18
|
+
- [browser_engine](https://en.wikipedia.org/wiki/Comparison_of_browser_engines) (blink, gecko, webkit, or nil)
|
19
|
+
|
20
|
+
It is fully customizable to store more details if desired.
|
21
|
+
|
22
|
+
## Screenshots
|
23
|
+
|
24
|
+
![Screenshot 1](/screenshot_1.png)
|
25
|
+
|
26
|
+
![Screenshot 2](/screenshot_2.png)
|
27
|
+
|
28
|
+
![Screenshot 3](/screenshot_3.png)
|
29
|
+
|
30
|
+
## Installation
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
# Gemfile
|
34
|
+
gem "rails_local_analytics"
|
35
|
+
```
|
36
|
+
|
37
|
+
Add the following migration to your app:
|
38
|
+
|
39
|
+
```
|
40
|
+
bundle exec rails g migration CreateAnalyticsTables
|
41
|
+
```
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
# db/migrations/..._create_analytics_tables.rb
|
45
|
+
|
46
|
+
class CreateAnalyticsTables < ActiveRecord::Migration[6.0]
|
47
|
+
def up
|
48
|
+
create_table :tracked_requests_by_day_page do |t|
|
49
|
+
t.date :day, null: false
|
50
|
+
t.bigint :total, null: false, default: 1
|
51
|
+
t.string :url_hostname, null: false
|
52
|
+
t.string :url_path, null: false
|
53
|
+
t.string :referrer_hostname
|
54
|
+
t.string :referrer_path
|
55
|
+
end
|
56
|
+
add_index :tracked_requests_by_day_page, :day
|
57
|
+
|
58
|
+
create_table :tracked_requests_by_day_site do |t|
|
59
|
+
t.date :day, null: false
|
60
|
+
t.bigint :total, null: false, default: 1
|
61
|
+
t.string :url_hostname, null: false
|
62
|
+
t.string :platform
|
63
|
+
t.string :browser_engine
|
64
|
+
end
|
65
|
+
add_index :tracked_requests_by_day_site, :day
|
66
|
+
end
|
67
|
+
|
68
|
+
def down
|
69
|
+
drop_table :tracked_requests_by_day_page
|
70
|
+
drop_table :tracked_requests_by_day_site
|
71
|
+
end
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
Add the route for the analytics dashboard at the desired endpoint:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
# config/routes.rb
|
79
|
+
mount RailsLocalAnalytics::Engine, at: "/admin/analytics"
|
80
|
+
```
|
81
|
+
|
82
|
+
Its generally recomended to use a background job (especially since we now have [`solid_queue`](https://github.com/rails/solid_queue/)). If you would like to disable background jobs you can use the following config:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
# config/initializers/rails_local_analytics.rb
|
86
|
+
RailsLocalAnalytics.config.background_jobs = false # defaults to true
|
87
|
+
```
|
88
|
+
|
89
|
+
The next step is to collect traffic.
|
90
|
+
|
91
|
+
## Recording requests
|
92
|
+
|
93
|
+
There are two types of analytics that we mainly target:
|
94
|
+
|
95
|
+
- Site level analytics
|
96
|
+
* Stored in the table `tracked_requests_by_day_site`
|
97
|
+
- Page level analytics
|
98
|
+
* Stored in the table `tracked_requests_by_day_page`
|
99
|
+
|
100
|
+
Your controllers have to manually call `RailsLocalAnalytics.record_request`. For example:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
class ApplicationController < ActionController::Base
|
104
|
+
after_action :record_page_view
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def record_page_view
|
109
|
+
return if !request.format.html? && !request.format.json?
|
110
|
+
|
111
|
+
### We accept manual overrides of any of the database fields
|
112
|
+
### For example if you wanted to track bots:
|
113
|
+
site_based_attrs = {}
|
114
|
+
if some_custom_bot_detection_method
|
115
|
+
site_based_attrs[:platform] = "bot"
|
116
|
+
end
|
117
|
+
|
118
|
+
RailsLocalAnalytics.record_request(
|
119
|
+
request: request,
|
120
|
+
custom_attributes: { # optional
|
121
|
+
TrackedRequestsByDaySite.name => site_based_attrs,
|
122
|
+
#TrackedRequestsByDayPage.name => {},
|
123
|
+
},
|
124
|
+
)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
If you need to add more data to your events you can simply add more columns to the analytics tables and then populate these columns using the `:custom_attributes` argument.
|
130
|
+
|
131
|
+
Some examples of additional things you may want to track:
|
132
|
+
|
133
|
+
- Bot detection
|
134
|
+
* Bot detection is difficult. As such we dont try to include it by default. Recommended gem for detection is [`crawler_detect`](https://github.com/loadkpi/crawler_detect)
|
135
|
+
* One option is to consider not tracking bots at all in your analytics, just a thought
|
136
|
+
* You may not need to store this in a new column, one example pattern could be to store this data in the existing `platform` database field
|
137
|
+
- Country detection
|
138
|
+
* Country detection is difficult. As such we dont try to include it by default.
|
139
|
+
- Users or organizations
|
140
|
+
* You may want to track your users or another model which is a core tenant to your particular application
|
141
|
+
|
142
|
+
|
143
|
+
## Performance Optimization Techniques
|
144
|
+
|
145
|
+
There are a few techniques that you can use to tailor the database for your particular needs. Heres a few examples:
|
146
|
+
|
147
|
+
- If you drop any database columns from the analytics tables this will not cause any issues. It will continue to function as normal.
|
148
|
+
- `url_hostname` column
|
149
|
+
* If you wont ever have multi-site needs then you can consider removing this column
|
150
|
+
* If storage space is an issue you may consider switching to an enum column as the number of permutations is probably something that can be anticipated.
|
151
|
+
- `referrer_host` and `referrer_path` columns
|
152
|
+
* Consider just storing "local" or nil instead if the request originated from your website
|
153
|
+
- `platform` and `browser_engine` columns
|
154
|
+
* Consider dropping either of these if you do not need this information
|
155
|
+
|
156
|
+
## Usage where a request object is not available
|
157
|
+
|
158
|
+
If you are not in a controller or do not have access to the request object then you may pass in a hash representation. For example:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
RailsLocalAnalytics.record_request(
|
162
|
+
request: {
|
163
|
+
host: "http://example.com",
|
164
|
+
path: "/some/path",
|
165
|
+
referrer: "http://example.com/some/other/path",
|
166
|
+
user_agent: "some-user-agent",
|
167
|
+
http_accept_language: "some-http-accept-language",
|
168
|
+
},
|
169
|
+
# ...
|
170
|
+
)
|
171
|
+
```
|
172
|
+
|
173
|
+
## Deleting old data
|
174
|
+
|
175
|
+
By default all data is retained indefinately. If you would like to have automated deletion of the data, you might use the following example technique:
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
class ApplicationController
|
179
|
+
after_action :record_page_view
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
def record_page_view
|
184
|
+
# perform other logic and call RailsLocalAnalytics.record_request
|
185
|
+
|
186
|
+
TrackedRequestsByDayPage.where("date < ?", 3.months.ago).delete_all
|
187
|
+
TrackedRequestsByDaySite.where("date < ?", 3.months.ago).delete_all
|
188
|
+
end
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
## Page Performance Tracking
|
193
|
+
|
194
|
+
We dont do any page performance tracking (request/response time, etc), this gem only specializes in analytics.
|
195
|
+
|
196
|
+
If you are looking for a simple performance tracking solution, I highly recommend the gem [`inner_performance`](https://github.com/mbajur/inner_performance)
|
197
|
+
|
198
|
+
## Development
|
199
|
+
|
200
|
+
Run server using: `bin/dev` or `cd test/dummy/; rails s`
|
201
|
+
|
202
|
+
## Testing
|
203
|
+
|
204
|
+
```
|
205
|
+
bundle exec rspec
|
206
|
+
```
|
207
|
+
|
208
|
+
We can locally test different versions of Rails using `ENV['RAILS_VERSION']`
|
209
|
+
|
210
|
+
```
|
211
|
+
export RAILS_VERSION=7.0
|
212
|
+
bundle install
|
213
|
+
bundle exec rspec
|
214
|
+
```
|
215
|
+
|
216
|
+
## Credits
|
217
|
+
|
218
|
+
Created & Maintained by [Weston Ganger](https://westonganger.com) - [@westonganger](https://github.com/westonganger)
|
219
|
+
|
220
|
+
Imitated some parts of [`active_analytics`](https://github.com/BaseSecrete/active_analytics). Thanks to them for the aggregate database schema idea.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
|
3
|
+
APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
|
4
|
+
load 'rails/tasks/engine.rake'
|
5
|
+
|
6
|
+
load 'rails/tasks/statistics.rake'
|
7
|
+
|
8
|
+
require 'bundler/gem_tasks'
|
9
|
+
|
10
|
+
require 'rspec/core/rake_task'
|
11
|
+
RSpec::Core::RakeTask.new(:spec)
|
12
|
+
|
13
|
+
task test: [:spec]
|
14
|
+
|
15
|
+
task default: [:spec]
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module RailsLocalAnalytics
|
2
|
+
class DashboardController < ApplicationController
|
3
|
+
|
4
|
+
def index
|
5
|
+
params[:type] ||= "Site"
|
6
|
+
|
7
|
+
case params[:type]
|
8
|
+
when "Site"
|
9
|
+
@klass = TrackedRequestsByDaySite
|
10
|
+
when "Page"
|
11
|
+
@klass = TrackedRequestsByDayPage
|
12
|
+
else
|
13
|
+
raise ArgumentError
|
14
|
+
end
|
15
|
+
|
16
|
+
if params[:group_by].present? && !@klass.display_columns.include?(params[:group_by])
|
17
|
+
params[:group_by] = "All"
|
18
|
+
else
|
19
|
+
params[:group_by] ||= "All"
|
20
|
+
end
|
21
|
+
|
22
|
+
if params[:start_date].present?
|
23
|
+
@start_date = Date.parse(params[:start_date])
|
24
|
+
else
|
25
|
+
@start_date = Date.today
|
26
|
+
end
|
27
|
+
|
28
|
+
if params[:end_date]
|
29
|
+
@end_date = Date.parse(params[:end_date])
|
30
|
+
else
|
31
|
+
@end_date = Date.today
|
32
|
+
end
|
33
|
+
|
34
|
+
if @end_date < @start_date
|
35
|
+
@end_date = @start_date
|
36
|
+
end
|
37
|
+
|
38
|
+
@tracked_requests = fetch_records(@start_date, @end_date)
|
39
|
+
|
40
|
+
prev_start_date = @start_date - (@end_date - @start_date)
|
41
|
+
prev_end_date = @end_date - (@end_date - @start_date)
|
42
|
+
|
43
|
+
@prev_period_tracked_requests = fetch_records(prev_start_date, prev_end_date)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def fetch_records(start_date, end_date)
|
49
|
+
tracked_requests = @klass
|
50
|
+
.where("day >= ?", @start_date)
|
51
|
+
.where("day <= ?", @end_date)
|
52
|
+
.order(total: :desc)
|
53
|
+
|
54
|
+
if params[:search].present?
|
55
|
+
tracked_requests = tracked_requests.multi_search(params[:search])
|
56
|
+
end
|
57
|
+
|
58
|
+
return tracked_requests
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module RailsLocalAnalytics
|
2
|
+
class RecordRequestJob < ApplicationJob
|
3
|
+
MODELS = [TrackedRequestsByDaySite, TrackedRequestsByDayPage].freeze
|
4
|
+
def perform(json)
|
5
|
+
if json.is_a?(String)
|
6
|
+
json = JSON.parse(json)
|
7
|
+
end
|
8
|
+
|
9
|
+
request_hash = json.fetch("request_hash")
|
10
|
+
|
11
|
+
custom_attributes_by_model = json.fetch("custom_attributes")
|
12
|
+
|
13
|
+
MODELS.each do |model|
|
14
|
+
custom_attrs = custom_attributes_by_model && custom_attributes_by_model[model.name]
|
15
|
+
|
16
|
+
attrs = build_attrs(model, custom_attrs, request_hash)
|
17
|
+
|
18
|
+
attrs["day"] = json.fetch("day")
|
19
|
+
|
20
|
+
existing_record = model.find_by(attrs)
|
21
|
+
|
22
|
+
if existing_record
|
23
|
+
existing_record.increment!(:total, 1)
|
24
|
+
else
|
25
|
+
model.create!(attrs)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def build_attrs(model, attrs, request_hash)
|
33
|
+
attrs ||= {}
|
34
|
+
|
35
|
+
field = "url_hostname"
|
36
|
+
if !skip_field?(field, attrs, model)
|
37
|
+
attrs[field] = request_hash.fetch("host").downcase
|
38
|
+
end
|
39
|
+
|
40
|
+
field = "url_path"
|
41
|
+
if !skip_field?(field, attrs, model)
|
42
|
+
attrs[field] = request_hash.fetch("path").downcase
|
43
|
+
end
|
44
|
+
|
45
|
+
if request_hash.fetch("referrer").present?
|
46
|
+
field = "referrer_hostname"
|
47
|
+
if !skip_field?(field,attrs, model)
|
48
|
+
referrer_hostname, referrer_path = split_referrer(request_hash.fetch("referrer"))
|
49
|
+
attrs[field] = referrer_hostname
|
50
|
+
end
|
51
|
+
|
52
|
+
field = "referrer_path"
|
53
|
+
if !skip_field?(field, attrs, model)
|
54
|
+
if referrer_path.nil?
|
55
|
+
referrer_hostname, referrer_path = split_referrer(request_hash.fetch("referrer"))
|
56
|
+
end
|
57
|
+
attrs[field] = referrer_path
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
if request_hash.fetch("user_agent").present?
|
62
|
+
field = "platform"
|
63
|
+
if !skip_field?(field, attrs, model)
|
64
|
+
browser ||= create_browser_object(request_hash)
|
65
|
+
attrs[field] = browser.platform.name
|
66
|
+
end
|
67
|
+
|
68
|
+
field = "browser_engine"
|
69
|
+
if !skip_field?(field, attrs, model)
|
70
|
+
browser ||= create_browser_object(request_hash)
|
71
|
+
attrs[field] = get_browser_engine(browser)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
return attrs
|
76
|
+
end
|
77
|
+
|
78
|
+
def split_referrer(referrer)
|
79
|
+
referrer = referrer.downcase
|
80
|
+
|
81
|
+
uri = URI(referrer)
|
82
|
+
|
83
|
+
if uri.host.present?
|
84
|
+
return [
|
85
|
+
uri.host,
|
86
|
+
uri.path.presence,
|
87
|
+
]
|
88
|
+
else
|
89
|
+
strings = referrer.split("/", 2)
|
90
|
+
return [
|
91
|
+
strings[0],
|
92
|
+
(strings[1].present? ? "/#{strings[1]}" : nil),
|
93
|
+
]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def get_browser_engine(browser)
|
98
|
+
if browser.webkit?
|
99
|
+
# must come before all other checks because Firefox/Chrome on iOS devices is actually using Safari under the hood
|
100
|
+
"webkit"
|
101
|
+
elsif browser.chromium_based?
|
102
|
+
"blink"
|
103
|
+
elsif browser.firefox?
|
104
|
+
"gecko"
|
105
|
+
else
|
106
|
+
nil # store nothing, data is not useful
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def create_browser_object(request_hash)
|
111
|
+
Browser.new(
|
112
|
+
request_hash.fetch("user_agent"),
|
113
|
+
accept_language: request_hash.fetch("http_accept_language"),
|
114
|
+
)
|
115
|
+
end
|
116
|
+
|
117
|
+
def skip_field?(field, attrs, model)
|
118
|
+
attrs&.has_key?(field) || !model.column_names.include?(field)
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module RailsLocalAnalytics
|
2
|
+
class Histogram
|
3
|
+
attr_reader :bars, :from_date, :to_date
|
4
|
+
|
5
|
+
def initialize(scope, from_date, to_date)
|
6
|
+
@scope = scope
|
7
|
+
@from_date, @to_date = from_date, to_date
|
8
|
+
@bars = scope.map { |record| Bar.new(record.date, record.total, self) }
|
9
|
+
fill_missing_days(@bars, @from_date, @to_date)
|
10
|
+
end
|
11
|
+
|
12
|
+
def fill_missing_days(bars, from, to)
|
13
|
+
i = 0
|
14
|
+
while (day = from + i) <= to
|
15
|
+
if !@bars[i] || @bars[i].label != day
|
16
|
+
@bars.insert(i, Bar.new(day, 0, self))
|
17
|
+
end
|
18
|
+
i += 1
|
19
|
+
end
|
20
|
+
@bars
|
21
|
+
end
|
22
|
+
|
23
|
+
def max_value
|
24
|
+
@max_total ||= bars.map(&:value).max
|
25
|
+
end
|
26
|
+
|
27
|
+
def total
|
28
|
+
@bars.reduce(0) { |sum, bar| sum += bar.value }
|
29
|
+
end
|
30
|
+
|
31
|
+
class Bar
|
32
|
+
attr_reader :label, :value, :histogram
|
33
|
+
|
34
|
+
def initialize(label, value, histogram)
|
35
|
+
@label, @value, @histogram = label, value, histogram
|
36
|
+
end
|
37
|
+
|
38
|
+
def height
|
39
|
+
if histogram.max_value > 0
|
40
|
+
(value.to_f / histogram.max_value).round(2)
|
41
|
+
else
|
42
|
+
0
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module RailsLocalAnalytics
|
2
|
+
class ApplicationRecord < ActiveRecord::Base
|
3
|
+
self.abstract_class = true
|
4
|
+
|
5
|
+
scope :multi_search, ->(full_str){
|
6
|
+
if full_str.present?
|
7
|
+
relation = self
|
8
|
+
|
9
|
+
full_str.split(' ').each do |str|
|
10
|
+
like = connection.adapter_name.downcase.to_s == "postgres" ? "ILIKE" : "LIKE"
|
11
|
+
|
12
|
+
sql_conditions = []
|
13
|
+
|
14
|
+
display_columns.each do |col|
|
15
|
+
sql_conditions << "(#{col} #{like} :search)"
|
16
|
+
end
|
17
|
+
|
18
|
+
relation = self.where(sql_conditions.join(" OR "), search: "%#{str}%")
|
19
|
+
end
|
20
|
+
|
21
|
+
next relation
|
22
|
+
end
|
23
|
+
}
|
24
|
+
|
25
|
+
def self.display_columns
|
26
|
+
column_names - ["id", "created_at", "updated_at", "total", "day"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def matches?(other_record)
|
30
|
+
day == other_record.day && self.class.display_columns.all?{|col_name| self.send(col_name) == other_record.send(col_name) }
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
<% title = RailsLocalAnalytics.name.titleize %>
|
2
|
+
|
3
|
+
<!DOCTYPE html>
|
4
|
+
<html>
|
5
|
+
<head>
|
6
|
+
<title><%= title %></title>
|
7
|
+
<meta name=viewport content="width=device-width, initial-scale=1">
|
8
|
+
<%= csrf_meta_tags %>
|
9
|
+
<%= csp_meta_tag %>
|
10
|
+
|
11
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.4.0/cosmo/bootstrap.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
12
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.32.0/css/theme.default.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
13
|
+
<%= stylesheet_link_tag "rails_local_analytics/application" %>
|
14
|
+
|
15
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
16
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.0/js/jquery.tablesorter.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
17
|
+
<%= javascript_include_tag "rails_local_analytics/application" %>
|
18
|
+
</head>
|
19
|
+
|
20
|
+
<body>
|
21
|
+
<nav class="navbar navbar-default">
|
22
|
+
<div class="container-fluid">
|
23
|
+
<div class="navbar-header">
|
24
|
+
<%= link_to title , root_path, class: "navbar-brand" %>
|
25
|
+
</h1>
|
26
|
+
</div>
|
27
|
+
</nav>
|
28
|
+
|
29
|
+
<div class="container-fluid">
|
30
|
+
<%= yield %>
|
31
|
+
</main>
|
32
|
+
</body>
|
33
|
+
</html>
|
@@ -0,0 +1,101 @@
|
|
1
|
+
<% page_group_by_options = options_for_select([["All", "All"]] + TrackedRequestsByDayPage.display_columns.map{|x| [x.titleize.sub("Url ", "URL "), x]}) %>
|
2
|
+
<% site_group_by_options = options_for_select([["All", "All"]] + TrackedRequestsByDaySite.display_columns.map{|x| [x.titleize.sub("Url ", "URL "), x]}) %>
|
3
|
+
|
4
|
+
<%= form_tag url_for(params.except(:start_date, :to).to_unsafe_hash), method: "get", class: "well well-sm" do %>
|
5
|
+
<div>
|
6
|
+
<label style="margin-right: 10px;">Type: <%= select_tag :type, options_for_select(["Site", "Page"], params[:type]) %></label>
|
7
|
+
|
8
|
+
<label style="margin-right: 10px;">Group By: <%= select_tag :group_by, options_for_select([["All", "All"]] + @klass.display_columns.map{|x| [x.titleize.sub("Url ", "URL "), x]} , params[:group_by]) %></label>
|
9
|
+
<label style="margin-right: 10px;">Search: <%= text_field_tag :search, params[:search] %></label>
|
10
|
+
|
11
|
+
<label style="margin-right: 10px;">From: <%= date_field_tag :start_date, @start_date %></label>
|
12
|
+
<label style="margin-right: 10px;">To: <%= date_field_tag :end_date, @end_date %></label>
|
13
|
+
<button type="submit">Filter</button>
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<div>
|
17
|
+
<%= link_to "Today", url_for(params.merge(start_date: Date.today, end_date: Date.today).to_unsafe_hash) %>
|
18
|
+
|
|
19
|
+
<%= link_to "Yesterday", url_for(params.merge(start_date: (Date.today - 1.day), end_date: (Date.today - 1.day)).to_unsafe_hash) %>
|
20
|
+
|
|
21
|
+
<%= link_to "Last 7 days", url_for(params.merge(start_date: 7.days.ago.to_date, end_date: Date.today).to_unsafe_hash) %>
|
22
|
+
|
|
23
|
+
<%= link_to "Last 30 days", url_for(params.merge(start_date: 30.days.ago.to_date, end_date: Date.today).to_unsafe_hash) %>
|
24
|
+
</div>
|
25
|
+
<% end %>
|
26
|
+
|
27
|
+
<h2>Tracked Requests - <%= @klass.name.titleize.split(" ").last %></h2>
|
28
|
+
|
29
|
+
<table class="table table-striped table-condensed table-sortable">
|
30
|
+
<thead>
|
31
|
+
<% if params[:group_by] == "All" %>
|
32
|
+
<% @klass.display_columns.each do |col_name| %>
|
33
|
+
<th>
|
34
|
+
<%= col_name.titleize.sub("Url ", "URL ") %>
|
35
|
+
<span title="Sort column">↕</span>
|
36
|
+
</th>
|
37
|
+
<% end %>
|
38
|
+
<% else %>
|
39
|
+
<td>
|
40
|
+
<%= params[:group_by].titleize.sub("Url ", "URL ") %>
|
41
|
+
<span title="Sort column">↕</span>
|
42
|
+
</td>
|
43
|
+
<% end %>
|
44
|
+
|
45
|
+
<th>
|
46
|
+
Total <span title="Difference compared to previous period">(Difference)</span>
|
47
|
+
<span title="Sort column">↕</span>
|
48
|
+
</th>
|
49
|
+
</thead>
|
50
|
+
|
51
|
+
<tbody>
|
52
|
+
<% @tracked_requests.group_by{|x| params[:group_by] != "All" ? x.send(params[:group_by]) : @klass.display_columns.map{|col| x.send(col)}}.each do |_grouping, records| %>
|
53
|
+
<% first_record = records.first %>
|
54
|
+
|
55
|
+
<tr>
|
56
|
+
<% if params[:group_by] == "All" %>
|
57
|
+
<% @klass.display_columns.each do |col_name| %>
|
58
|
+
<td>
|
59
|
+
<%= first_record.send(col_name) %>
|
60
|
+
</td>
|
61
|
+
<% end %>
|
62
|
+
<% else %>
|
63
|
+
<td>
|
64
|
+
<%= first_record.send(params[:group_by]) %>
|
65
|
+
</td>
|
66
|
+
<% end %>
|
67
|
+
|
68
|
+
<td>
|
69
|
+
<% total = records.sum(&:total) %>
|
70
|
+
<%= total %>
|
71
|
+
|
72
|
+
<% _grouping, prev_period_records = @prev_period_tracked_requests.select{|x| first_record.matches?(x) }.group_by{|x| params[:group_by] != "All" ? x.send(params[:group_by]) : @klass.display_columns.map{|col| x.send(col)}}.first %>
|
73
|
+
<% diff = total - prev_period_records.sum(&:total) %>
|
74
|
+
|
75
|
+
<% if diff >= 0 %>
|
76
|
+
(+<%= diff %>)
|
77
|
+
<% else %>
|
78
|
+
(<%= diff %>)
|
79
|
+
<% end %>
|
80
|
+
</td>
|
81
|
+
</tr>
|
82
|
+
<% end %>
|
83
|
+
</tbody>
|
84
|
+
</table>
|
85
|
+
|
86
|
+
<script>
|
87
|
+
$(function(){
|
88
|
+
var siteGroupByOptions = "<%= j site_group_by_options %>";
|
89
|
+
var pageGroupByOptions = "<%= j page_group_by_options %>";
|
90
|
+
|
91
|
+
$(document).on("change", "select[name='type']", function(){
|
92
|
+
var group_by_select = $("select[name='group_by']");
|
93
|
+
|
94
|
+
if($(this).val() == "Site"){
|
95
|
+
group_by_select.html(siteGroupByOptions);
|
96
|
+
}else{
|
97
|
+
group_by_select.html(pageGroupByOptions);
|
98
|
+
}
|
99
|
+
});
|
100
|
+
});
|
101
|
+
</script>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module RailsLocalAnalytics
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace RailsLocalAnalytics
|
4
|
+
|
5
|
+
initializer "rails_local_analytics.assets.precompile" do |app|
|
6
|
+
# this initializer is only called when sprockets is in use
|
7
|
+
|
8
|
+
app.config.assets.precompile << "rails_local_analytics_manifest.js" ### manifest file required
|
9
|
+
|
10
|
+
### Automatically precompile assets in specified folders
|
11
|
+
["app/assets/images/"].each do |folder|
|
12
|
+
dir = app.root.join(folder)
|
13
|
+
|
14
|
+
if Dir.exist?(dir)
|
15
|
+
Dir.glob(File.join(dir, "**/*")).each do |f|
|
16
|
+
asset_name = f.to_s
|
17
|
+
.split(folder).last # Remove fullpath
|
18
|
+
.sub(/^\/*/, '') ### Remove leading '/'
|
19
|
+
|
20
|
+
app.config.assets.precompile << asset_name
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
initializer "rails_local_analytics.load_static_assets" do |app|
|
27
|
+
### Expose static assets
|
28
|
+
app.middleware.use ::ActionDispatch::Static, "#{root}/public"
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "rails_local_analytics/version"
|
2
|
+
require "rails_local_analytics/engine"
|
3
|
+
require "browser/browser"
|
4
|
+
|
5
|
+
module RailsLocalAnalytics
|
6
|
+
|
7
|
+
def self.record_request(request:, custom_attributes: nil)
|
8
|
+
if request.is_a?(Hash)
|
9
|
+
request_hash = request
|
10
|
+
else
|
11
|
+
### Make request object generic so that it can be used outside of the controller
|
12
|
+
|
13
|
+
request_hash = {
|
14
|
+
referrer: request.referrer,
|
15
|
+
host: request.host,
|
16
|
+
path: request.path,
|
17
|
+
user_agent: request.user_agent,
|
18
|
+
http_accept_language: request.env["HTTP_ACCEPT_LANGUAGE"],
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
json_hash = {
|
23
|
+
day: Date.today.to_s,
|
24
|
+
request_hash: request_hash,
|
25
|
+
custom_attributes: custom_attributes,
|
26
|
+
}
|
27
|
+
|
28
|
+
if RailsLocalAnalytics.config.background_jobs
|
29
|
+
json_str = JSON.generate(json_hash) # convert to json string so that its compatible with all job backends
|
30
|
+
RecordRequestJob.perform_later(json_str)
|
31
|
+
else
|
32
|
+
RecordRequestJob.new.perform(json_hash)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.config(&block)
|
37
|
+
c = Config
|
38
|
+
|
39
|
+
if block_given?
|
40
|
+
block.call(c)
|
41
|
+
else
|
42
|
+
return c
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Config
|
47
|
+
@@background_jobs = true
|
48
|
+
mattr_reader :background_jobs
|
49
|
+
|
50
|
+
def self.background_jobs=(val)
|
51
|
+
@@background_jobs = !!val
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails_local_analytics
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Weston Ganger
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-12-03 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: '6.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: browser
|
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: rspec-rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
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: database_cleaner
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
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: rails-controller-testing
|
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: Simple, performant, local analytics for Rails. Solves 95% of your needs
|
84
|
+
until your ready to start taking analytics more seriously using another tool.
|
85
|
+
email:
|
86
|
+
- weston@westonganger.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- LICENSE
|
92
|
+
- README.md
|
93
|
+
- Rakefile
|
94
|
+
- app/assets/config/rails_local_analytics_manifest.js
|
95
|
+
- app/assets/javascripts/rails_local_analytics/application.js
|
96
|
+
- app/assets/stylesheets/rails_local_analytics/application.css
|
97
|
+
- app/controllers/rails_local_analytics/application_controller.rb
|
98
|
+
- app/controllers/rails_local_analytics/dashboard_controller.rb
|
99
|
+
- app/jobs/rails_local_analytics/application_job.rb
|
100
|
+
- app/jobs/rails_local_analytics/record_request_job.rb
|
101
|
+
- app/lib/rails_local_analytics/histogram.rb
|
102
|
+
- app/models/rails_local_analytics/application_record.rb
|
103
|
+
- app/models/tracked_requests_by_day_page.rb
|
104
|
+
- app/models/tracked_requests_by_day_site.rb
|
105
|
+
- app/views/layouts/rails_local_analytics/application.html.erb
|
106
|
+
- app/views/rails_local_analytics/dashboard/index.html.erb
|
107
|
+
- config/routes.rb
|
108
|
+
- lib/rails_local_analytics.rb
|
109
|
+
- lib/rails_local_analytics/engine.rb
|
110
|
+
- lib/rails_local_analytics/version.rb
|
111
|
+
- lib/tasks/rails_local_analytics_tasks.rake
|
112
|
+
homepage: https://github.com/westonganger/rails_local_analytics
|
113
|
+
licenses:
|
114
|
+
- MIT
|
115
|
+
metadata: {}
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '2.6'
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
requirements: []
|
131
|
+
rubygems_version: 3.4.22
|
132
|
+
signing_key:
|
133
|
+
specification_version: 4
|
134
|
+
summary: Simple, performant, local analytics for Rails. Solves 95% of your needs until
|
135
|
+
your ready to start taking analytics more seriously using another tool.
|
136
|
+
test_files: []
|