├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── Rakefile ├── active_analytics.gemspec ├── app ├── assets │ ├── config │ │ └── active_analytics_manifest.js │ └── images │ │ ├── active_analytics.png │ │ ├── active_analytics │ │ └── .keep │ │ └── active_analytics_screenshot.png ├── controllers │ └── active_analytics │ │ ├── application_controller.rb │ │ ├── assets_controller.rb │ │ ├── browsers_controller.rb │ │ ├── pages_controller.rb │ │ ├── referrers_controller.rb │ │ └── sites_controller.rb ├── helpers │ └── active_analytics │ │ ├── application_helper.rb │ │ ├── browsers_helper.rb │ │ ├── pages_helper.rb │ │ ├── referrers_helper.rb │ │ └── sites_helper.rb ├── jobs │ └── active_analytics │ │ └── application_job.rb ├── lib │ └── active_analytics │ │ └── histogram.rb ├── mailers │ └── active_analytics │ │ └── application_mailer.rb ├── models │ └── active_analytics │ │ ├── application_record.rb │ │ ├── browsers_per_day.rb │ │ └── views_per_day.rb └── views │ ├── active_analytics │ ├── assets │ │ ├── _charts.css │ │ ├── _style.css │ │ ├── application.css.erb │ │ ├── application.js │ │ ├── ariato.css │ │ ├── ariato.js │ │ └── browsers │ │ │ ├── arc.svg │ │ │ ├── brave.svg │ │ │ ├── chrome.svg │ │ │ ├── default.svg │ │ │ ├── firefox.svg │ │ │ ├── internet_explorer.svg │ │ │ ├── microsoft_edge.svg │ │ │ ├── opera.svg │ │ │ ├── safari.svg │ │ │ ├── samsung_internet.svg │ │ │ ├── vivaldi.svg │ │ │ └── yandex.svg │ ├── browsers │ │ ├── _table.html.erb │ │ ├── _version_table.html.erb │ │ ├── index.html.erb │ │ └── show.html.erb │ ├── pages │ │ ├── _table.html.erb │ │ ├── index.html.erb │ │ └── show.html.erb │ ├── referrers │ │ ├── _table.html.erb │ │ ├── index.html.erb │ │ └── show.html.erb │ └── sites │ │ ├── _histogram.html.erb │ │ ├── _histogram_header.html.erb │ │ ├── index.html.erb │ │ └── show.html.erb │ └── layouts │ └── active_analytics │ ├── _footer.html.erb │ ├── _header.html.erb │ └── application.html.erb ├── bin └── rails ├── config └── routes.rb ├── db └── migrate │ ├── 20210303094108_create_active_analytics_views_per_days.rb │ └── 20240823150626_create_active_analytics_browsers_per_days.rb ├── lib ├── active_analytics.rb ├── active_analytics │ ├── engine.rb │ └── version.rb └── tasks │ └── active_analytics_tasks.rake └── test ├── active_analytics_test.rb ├── controllers └── active_analytics │ ├── browsers_controller_test.rb │ ├── pages_controller_test.rb │ ├── referrers_controller_test.rb │ └── sites_controller_test.rb ├── dummy ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── images │ │ │ └── .keep │ │ └── stylesheets │ │ │ └── application.css │ ├── channels │ │ └── application_cable │ │ │ ├── channel.rb │ │ │ └── connection.rb │ ├── controllers │ │ ├── application_controller.rb │ │ └── concerns │ │ │ └── .keep │ ├── helpers │ │ └── application_helper.rb │ ├── javascript │ │ └── packs │ │ │ └── application.js │ ├── jobs │ │ └── application_job.rb │ ├── mailers │ │ └── application_mailer.rb │ ├── models │ │ ├── application_record.rb │ │ └── concerns │ │ │ └── .keep │ └── views │ │ └── layouts │ │ ├── application.html.erb │ │ ├── mailer.html.erb │ │ └── mailer.text.erb ├── bin │ ├── rails │ ├── rake │ └── setup ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── application_controller_renderer.rb │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── content_security_policy.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── permissions_policy.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ └── storage.yml ├── db │ └── schema.rb ├── lib │ └── assets │ │ └── .keep ├── log │ └── .keep └── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ └── favicon.ico ├── fixtures └── active_analytics │ ├── browsers_per_days.yml │ └── views_per_days.yml ├── integration └── navigation_test.rb ├── models └── active_analytics │ ├── browsers_per_day_test.rb │ └── views_per_day_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /doc/ 3 | /log/*.log 4 | /pkg/ 5 | /tmp/ 6 | /test/dummy/db/*.sqlite3 7 | /test/dummy/db/*.sqlite3-* 8 | /test/dummy/log/*.log 9 | /test/dummy/storage/ 10 | /test/dummy/tmp/ 11 | .byebug_history 12 | **/.DS_Store 13 | *.dump 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog of ActiveAnalytics 2 | 3 | ## Version 0.4.1 (2025-03-13) 4 | 5 | - Shortened index name in migration to prevent argument error (index name too long) 6 | 7 | ## Version 0.4.0 (2025-03-06) 8 | 9 | - Record browser statistics from user agent 10 | - Added `base_controller_class` configuration option to allow specifying a custom base controller for the ActiveAnalytics dashboard, 11 | enhancing flexibility in diverse application architectures. 12 | 13 | ## Version 0.3 (2023-09-15) 14 | 15 | * Queue requests to reduce the load on database writes 16 | 17 | Database writes are slow. On large trafic applications it might overload the database. 18 | The idea is to queue data into redis and to flush periodically into the database. 19 | 20 | ```ruby 21 | ActiveAnalytics.queue_request(request) # In an after_action 22 | ``` 23 | 24 | Then call from time to time : 25 | 26 | ```ruby 27 | ActiveAnalytics.flush_queue # From a cron or a job 28 | ``` 29 | 30 | * Deliver CSS and JS without asset pipeline 31 | * Reverse date range when start is after end 32 | * Add link to external page 33 | * Display views evolution against previous period 34 | * List all paths from a host referrer when available 35 | * Scope css styles with .active-analytics 36 | * Update colors to augment contrast 37 | * Add gap to separate days in chart 38 | * Add link to day on chart label 39 | * Prevent chart NaN 40 | * Add trend labels color 41 | * Remove unused css 42 | * Add environment variable ACTIVE_ANALYTICS_REDIS_URL 43 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | # Specify your gem's dependencies in active_analytics.gemspec. 5 | gemspec 6 | 7 | group :development do 8 | gem 'sqlite3' 9 | gem "redis" 10 | end 11 | 12 | gem "browser" 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | active_analytics (0.4.1) 5 | browser (>= 1.0.0) 6 | rails (>= 5.2.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (8.0.2) 12 | actionpack (= 8.0.2) 13 | activesupport (= 8.0.2) 14 | nio4r (~> 2.0) 15 | websocket-driver (>= 0.6.1) 16 | zeitwerk (~> 2.6) 17 | actionmailbox (8.0.2) 18 | actionpack (= 8.0.2) 19 | activejob (= 8.0.2) 20 | activerecord (= 8.0.2) 21 | activestorage (= 8.0.2) 22 | activesupport (= 8.0.2) 23 | mail (>= 2.8.0) 24 | actionmailer (8.0.2) 25 | actionpack (= 8.0.2) 26 | actionview (= 8.0.2) 27 | activejob (= 8.0.2) 28 | activesupport (= 8.0.2) 29 | mail (>= 2.8.0) 30 | rails-dom-testing (~> 2.2) 31 | actionpack (8.0.2) 32 | actionview (= 8.0.2) 33 | activesupport (= 8.0.2) 34 | nokogiri (>= 1.8.5) 35 | rack (>= 2.2.4) 36 | rack-session (>= 1.0.1) 37 | rack-test (>= 0.6.3) 38 | rails-dom-testing (~> 2.2) 39 | rails-html-sanitizer (~> 1.6) 40 | useragent (~> 0.16) 41 | actiontext (8.0.2) 42 | actionpack (= 8.0.2) 43 | activerecord (= 8.0.2) 44 | activestorage (= 8.0.2) 45 | activesupport (= 8.0.2) 46 | globalid (>= 0.6.0) 47 | nokogiri (>= 1.8.5) 48 | actionview (8.0.2) 49 | activesupport (= 8.0.2) 50 | builder (~> 3.1) 51 | erubi (~> 1.11) 52 | rails-dom-testing (~> 2.2) 53 | rails-html-sanitizer (~> 1.6) 54 | activejob (8.0.2) 55 | activesupport (= 8.0.2) 56 | globalid (>= 0.3.6) 57 | activemodel (8.0.2) 58 | activesupport (= 8.0.2) 59 | activerecord (8.0.2) 60 | activemodel (= 8.0.2) 61 | activesupport (= 8.0.2) 62 | timeout (>= 0.4.0) 63 | activestorage (8.0.2) 64 | actionpack (= 8.0.2) 65 | activejob (= 8.0.2) 66 | activerecord (= 8.0.2) 67 | activesupport (= 8.0.2) 68 | marcel (~> 1.0) 69 | activesupport (8.0.2) 70 | base64 71 | benchmark (>= 0.3) 72 | bigdecimal 73 | concurrent-ruby (~> 1.0, >= 1.3.1) 74 | connection_pool (>= 2.2.5) 75 | drb 76 | i18n (>= 1.6, < 2) 77 | logger (>= 1.4.2) 78 | minitest (>= 5.1) 79 | securerandom (>= 0.3) 80 | tzinfo (~> 2.0, >= 2.0.5) 81 | uri (>= 0.13.1) 82 | base64 (0.2.0) 83 | benchmark (0.4.0) 84 | bigdecimal (3.1.9) 85 | browser (6.2.0) 86 | builder (3.3.0) 87 | concurrent-ruby (1.3.5) 88 | connection_pool (2.5.0) 89 | crass (1.0.6) 90 | date (3.4.1) 91 | drb (2.2.1) 92 | erubi (1.13.1) 93 | globalid (1.2.1) 94 | activesupport (>= 6.1) 95 | i18n (1.14.7) 96 | concurrent-ruby (~> 1.0) 97 | io-console (0.8.0) 98 | irb (1.15.1) 99 | pp (>= 0.6.0) 100 | rdoc (>= 4.0.0) 101 | reline (>= 0.4.2) 102 | logger (1.6.6) 103 | loofah (2.24.0) 104 | crass (~> 1.0.2) 105 | nokogiri (>= 1.12.0) 106 | mail (2.8.1) 107 | mini_mime (>= 0.1.1) 108 | net-imap 109 | net-pop 110 | net-smtp 111 | marcel (1.0.4) 112 | mini_mime (1.1.5) 113 | mini_portile2 (2.8.8) 114 | minitest (5.25.5) 115 | net-imap (0.5.6) 116 | date 117 | net-protocol 118 | net-pop (0.1.2) 119 | net-protocol 120 | net-protocol (0.2.2) 121 | timeout 122 | net-smtp (0.5.1) 123 | net-protocol 124 | nio4r (2.7.4) 125 | nokogiri (1.18.3-x86_64-linux-gnu) 126 | racc (~> 1.4) 127 | pp (0.6.2) 128 | prettyprint 129 | prettyprint (0.2.0) 130 | psych (5.2.3) 131 | date 132 | stringio 133 | racc (1.8.1) 134 | rack (3.1.12) 135 | rack-session (2.1.0) 136 | base64 (>= 0.1.0) 137 | rack (>= 3.0.0) 138 | rack-test (2.2.0) 139 | rack (>= 1.3) 140 | rackup (2.2.1) 141 | rack (>= 3) 142 | rails (8.0.2) 143 | actioncable (= 8.0.2) 144 | actionmailbox (= 8.0.2) 145 | actionmailer (= 8.0.2) 146 | actionpack (= 8.0.2) 147 | actiontext (= 8.0.2) 148 | actionview (= 8.0.2) 149 | activejob (= 8.0.2) 150 | activemodel (= 8.0.2) 151 | activerecord (= 8.0.2) 152 | activestorage (= 8.0.2) 153 | activesupport (= 8.0.2) 154 | bundler (>= 1.15.0) 155 | railties (= 8.0.2) 156 | rails-dom-testing (2.2.0) 157 | activesupport (>= 5.0.0) 158 | minitest 159 | nokogiri (>= 1.6) 160 | rails-html-sanitizer (1.6.2) 161 | loofah (~> 2.21) 162 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) 163 | railties (8.0.2) 164 | actionpack (= 8.0.2) 165 | activesupport (= 8.0.2) 166 | irb (~> 1.13) 167 | rackup (>= 1.0.0) 168 | rake (>= 12.2) 169 | thor (~> 1.0, >= 1.2.2) 170 | zeitwerk (~> 2.6) 171 | rake (13.2.1) 172 | rdoc (6.12.0) 173 | psych (>= 4.0.0) 174 | redis (5.4.0) 175 | redis-client (>= 0.22.0) 176 | redis-client (0.24.0) 177 | connection_pool 178 | reline (0.6.0) 179 | io-console (~> 0.5) 180 | securerandom (0.4.1) 181 | sqlite3 (2.6.0) 182 | mini_portile2 (~> 2.8.0) 183 | stringio (3.1.5) 184 | thor (1.3.2) 185 | timeout (0.4.3) 186 | tzinfo (2.0.6) 187 | concurrent-ruby (~> 1.0) 188 | uri (1.0.3) 189 | useragent (0.16.11) 190 | websocket-driver (0.7.7) 191 | base64 192 | websocket-extensions (>= 0.1.0) 193 | websocket-extensions (0.1.5) 194 | zeitwerk (2.7.2) 195 | 196 | PLATFORMS 197 | ruby 198 | 199 | DEPENDENCIES 200 | active_analytics! 201 | browser 202 | redis 203 | sqlite3 204 | 205 | BUNDLED WITH 206 | 2.2.22 207 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Alexis Bernard 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. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveAnalytics 2 | 3 | active analytics logo 4 | 5 | Simple traffic analytics for the win of privacy. 6 | 7 | * NO cookies 8 | * NO JavaScript 9 | * NO third parties 10 | * NO bullshit 11 | 12 | **ActiveAnalytics** is a Rails engine directly mountable in your Ruby on Rails application. It doesn't reveal anything about specific visitors. It cannot be blocked by adblockers or other privacy-protecting extensions (and doesn't need to). 13 | 14 | **ActiveAnalytics** lets you know about: 15 | 16 | * **Sources**: What are the pages and domains that bring some traffic. 17 | * **Page views**: What are the pages that are the most viewed in your application. 18 | * **Next/previous page**: What are the entry and exit pages for a given page of your application. 19 | * **Browser statistics**: What browsers and which versions are visiting your site. 20 | 21 | active analytics logo 22 | 23 | ## Installation 24 | Add this line to your application's Gemfile: 25 | ```ruby 26 | gem 'active_analytics' 27 | ``` 28 | 29 | Then execute bundle and run the migration: 30 | ```bash 31 | bundle 32 | rails active_analytics:install:migrations 33 | rails db:migrate 34 | ``` 35 | 36 | Add the route to ActiveAnalytics dashboard at the desired endpoint: 37 | 38 | ```ruby 39 | # config/routes.rb 40 | mount ActiveAnalytics::Engine, at: "analytics" # http://localhost:3000/analytics 41 | ``` 42 | By default ActiveAnalytics will extend `ActionController::Base`, but you can specify a custom base controller for the ActiveAnalytics dashboard: 43 | 44 | ```ruby 45 | # config/initializers/active_analytics.rb 46 | Rails.application.configure do 47 | ActiveAnalytics.base_controller_class = "AdminController" 48 | end 49 | ``` 50 | 51 | 52 | The next step is to collect trafic and there is 2 options. 53 | 54 | ### Record requests synchronously 55 | 56 | This is the easiest way to start with. 57 | However it's less performant since it triggers a write into your database for each request. 58 | Your controllers have to call `ActiveAnalytics.record_request(request)` to record page views. 59 | The Rails way to achieve is to use `after_action` : 60 | 61 | ```ruby 62 | class ApplicationController < ActionController::Base 63 | after_action :record_page_view 64 | 65 | def record_page_view 66 | # This is a basic example, you might need to customize some conditions. 67 | # For most sites, it makes no sense to record anything other than HTML. 68 | if response.content_type && response.content_type.start_with?("text/html") 69 | # Add a condition to record only your canonical domain 70 | # and use a gem such as crawler_detect to skip bots. 71 | ActiveAnalytics.record_request(request) 72 | end 73 | end 74 | end 75 | ``` 76 | 77 | In case you don't want to record all page views, because each application has sensitive URLs such as password reset and so on, simply define a `skip_after_action :record_page_view` in the relevant controller. 78 | 79 | ### Queue requests asynchronously 80 | 81 | It requires more work and it's relevant if your application handle a large trafic. 82 | The idea is to queue data into Redis because it does not require the database writing to the disk on each request. 83 | First you have to set the Redis URL or connection. 84 | 85 | ```ruby 86 | # File lib/patches/active_analytics.rb or config/initializers/active_analytics.rb 87 | 88 | ActiveAnalytics.redis_url = "redis://user:password@host/1" # Default ENV["ACTIVE_ANALYTICS_REDIS_URL"] || ENV["REDIS_URL"] || "redis://localhost" 89 | 90 | # If you use special connection options you have to instantiate it yourself 91 | ActiveAnalytics.redis = Redis.new( 92 | url: ENV["REDIS_URL"], 93 | reconnect_attempts: 10, 94 | ssl_params: {verify_mode: OpenSSL::SSL::VERIFY_NONE} 95 | ) 96 | ``` 97 | 98 | Then your controllers have to call `ActiveAnalytics.queue_request(request)` to queue page views. 99 | The Rails way to achieve is to use `after_action` : 100 | 101 | ```ruby 102 | class ApplicationController < ActionController::Base 103 | after_action :queue_page_view 104 | 105 | def queue_page_view 106 | # This is a basic example, you might need to customize some conditions. 107 | # For most sites, it makes no sense to record anything other than HTML. 108 | if response.content_type && response.content_type.start_with?("text/html") 109 | # Add a condition to record only your canonical domain 110 | # and use a gem such as crawler_detect to skip bots. 111 | ActiveAnalytics.queue_request(request) 112 | end 113 | end 114 | end 115 | ``` 116 | 117 | Queued data need to be saved into the database in order to be viewable in the ActiveAnalytics dashboard. 118 | For that, call `ActiveAnalytics.flush_queue` from a cron task or a background job. 119 | 120 | It's up to you if you want to flush the queue every hour or every 10 minutes. 121 | I advise to execute the last flush of the day at 23:59. 122 | It prevents from shifting the trafic to the next day. 123 | In that case only the last minute will be shifted to the next day, even if the flush ends after midnight. 124 | This small imperfection allows a simpler implementation for now. 125 | Keep it simple ! 126 | 127 | 128 | ## Authentication and permissions 129 | 130 | ActiveAnalytics cannot guess how you handle user authentication, because it is different for all Rails applications. 131 | So you have to monkey patch `ActiveAnalytics::ApplicationController` in order to inject your own mechanism. 132 | The patch can be saved wherever you want. 133 | For example, I like to have all the patches in one place, so I put them in `lib/patches`. 134 | 135 | ```ruby 136 | # lib/patches/active_analytics.rb 137 | 138 | ActiveAnalytics::ApplicationController.class_eval do 139 | before_action :require_admin 140 | 141 | def require_admin 142 | # This example supposes there are current_user and User#admin? methods 143 | raise ActionController::RoutingError.new("Not found") unless current_user.try(:admin?) 144 | end 145 | end 146 | end 147 | ``` 148 | 149 | Then you have to require the monkey patch. 150 | Because it's loaded via require, it won't be reloaded in development. 151 | Since you are not supposed to change this file often, it should not be an issue. 152 | 153 | ```ruby 154 | # config/application.rb 155 | config.after_initialize do 156 | require "patches/active_analytics" 157 | end 158 | ``` 159 | 160 | If you use Devise, you can check the permission directly from routes.rb : 161 | 162 | ```ruby 163 | # config/routes.rb 164 | authenticate :user, -> (u) { u.admin? } do # Supposing there is a User#admin? method 165 | mount ActiveAnalytics::Engine, at: "analytics" # http://localhost:3000/analytics 166 | end 167 | ``` 168 | 169 | ## License 170 | The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 171 | 172 | Made by [Base Secrète](https://basesecrete.com). 173 | 174 | Rails developer? Check out [RoRvsWild](https://rorvswild.com), our Ruby on Rails application monitoring tool. 175 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | 3 | APP_RAKEFILE = File.expand_path("test/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 "rake/testtask" 11 | 12 | Rake::TestTask.new(:test) do |t| 13 | t.libs << 'test' 14 | t.pattern = 'test/**/*_test.rb' 15 | t.verbose = false 16 | end 17 | 18 | task default: :test 19 | -------------------------------------------------------------------------------- /active_analytics.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/active_analytics/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "active_analytics" 5 | spec.version = ActiveAnalytics::VERSION 6 | spec.authors = ["Alexis Bernard", "Antoine Marguerie"] 7 | spec.email = ["alexis@basesecrete.com", "antoine@basesecrete.com"] 8 | spec.homepage = "https://github.com/BaseSecrete/active_analytics" 9 | spec.summary = "First-party, privacy-focused traffic analytics for Ruby on Rails applications" 10 | spec.description = "NO cookies, NO JavaScript, NO third parties and NO bullshit." 11 | spec.license = "MIT" 12 | 13 | spec.metadata["allowed_push_host"] = "https://rubygems.org" 14 | spec.metadata["homepage_uri"] = spec.homepage 15 | spec.metadata["source_code_uri"] = "https://github.com/BaseSecrete/active_analytics" 16 | spec.metadata["changelog_uri"] = "https://github.com/BaseSecrete/active_analytics" 17 | 18 | spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] 19 | 20 | spec.add_dependency "rails", ">= 5.2.0" 21 | spec.add_dependency "browser", ">= 1.0.0" 22 | end 23 | -------------------------------------------------------------------------------- /app/assets/config/active_analytics_manifest.js: -------------------------------------------------------------------------------- 1 | //= link_directory ../stylesheets/active_analytics .css 2 | -------------------------------------------------------------------------------- /app/assets/images/active_analytics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/app/assets/images/active_analytics.png -------------------------------------------------------------------------------- /app/assets/images/active_analytics/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/app/assets/images/active_analytics/.keep -------------------------------------------------------------------------------- /app/assets/images/active_analytics_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/app/assets/images/active_analytics_screenshot.png -------------------------------------------------------------------------------- /app/controllers/active_analytics/application_controller.rb: -------------------------------------------------------------------------------- 1 | module ActiveAnalytics 2 | class ApplicationController < ActiveAnalytics.base_controller_class.constantize 3 | layout "active_analytics/application" 4 | 5 | helper PagesHelper 6 | helper SitesHelper 7 | helper BrowsersHelper 8 | 9 | private 10 | 11 | def from_date 12 | @from_date ||= Date.parse(params[:from]) 13 | end 14 | 15 | def to_date 16 | @to_date ||= Date.parse(params[:to]) 17 | end 18 | 19 | def require_date_range 20 | redirect_to(params.to_unsafe_hash.merge(from: to_date, to: from_date)) if from_date > to_date 21 | rescue TypeError, ArgumentError # Raised by Date.parse when invalid format 22 | redirect_to(params.to_unsafe_hash.merge(from: 7.days.ago.to_date, to: Date.today)) 23 | end 24 | 25 | def current_views_per_days 26 | ViewsPerDay.where(site: params[:site]).between_dates(from_date, to_date) 27 | end 28 | 29 | def previous_views_per_days 30 | ViewsPerDay.where(site: params[:site]).between_dates(previous_from_date, previous_to_date) 31 | end 32 | 33 | def previous_from_date 34 | from_date - (to_date - from_date) 35 | end 36 | 37 | def previous_to_date 38 | to_date - (to_date - from_date) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/controllers/active_analytics/assets_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_dependency "active_analytics/application_controller" 4 | 5 | module ActiveAnalytics 6 | class AssetsController < ApplicationController 7 | protect_from_forgery except: :show 8 | 9 | @@root = ActiveAnalytics::Engine.root.join("app/views", controller_path + "/").to_s 10 | 11 | def self.endpoints 12 | return @endpoints if @endpoints 13 | paths = Dir["#{@@root}**/*"].keep_if { |path| File.file?(path) } 14 | files = paths.map { |path| path.to_s.delete_prefix(@@root).delete_suffix(".erb") } 15 | @endpoints = files.delete_if { |str| str.start_with?("_") } 16 | end 17 | 18 | def show 19 | if self.class.endpoints.include?(params[:file]) 20 | expires_in(1.day, public: true) 21 | render_asset(params[:file]) 22 | else 23 | raise ActionController::RoutingError.new("Not found #{params[:file]}") 24 | end 25 | end 26 | 27 | private 28 | 29 | def render_asset(path) 30 | ext = File.extname(params[:file]) 31 | path_without_ext = path.delete_suffix(ext) 32 | mime_type = Mime::Type.lookup_by_extension(ext.delete_prefix(".")) 33 | render("#{controller_path}/#{path_without_ext}", mime_type: mime_type) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/controllers/active_analytics/browsers_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency "active_analytics/application_controller" 2 | 3 | module ActiveAnalytics 4 | class BrowsersController < ApplicationController 5 | def index 6 | @histogram = Histogram.new(current_browsers_per_days.order_by_date.group_by_date, from_date, to_date) 7 | @previous_histogram = Histogram.new(previous_browsers_per_days.order_by_date.group_by_date, previous_from_date, previous_to_date) 8 | @browsers = current_browsers_per_days.group_by_name.top(100) 9 | end 10 | 11 | def show 12 | @histogram = Histogram.new(current_browsers_per_days.order_by_date.group_by_date, from_date, to_date) 13 | @previous_histogram = Histogram.new(previous_browsers_per_days.order_by_date.group_by_date, previous_from_date, previous_to_date) 14 | @browsers = current_browsers_per_days.group_by_version.top(100) 15 | end 16 | 17 | private 18 | 19 | def current_browsers_per_days 20 | BrowsersPerDay.filter_by(params) 21 | end 22 | 23 | def previous_browsers_per_days 24 | BrowsersPerDay.filter_by(params.merge(from: previous_from_date, to: previous_to_date)) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/active_analytics/pages_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency "active_analytics/application_controller" 2 | 3 | module ActiveAnalytics 4 | class PagesController < ApplicationController 5 | include PagesHelper 6 | 7 | before_action :require_date_range 8 | 9 | def index 10 | @histogram = Histogram.new(current_views_per_days.order_by_date.group_by_date, from_date, to_date) 11 | @previous_histogram = Histogram.new(previous_views_per_days.order_by_date.group_by_date, previous_from_date, previous_to_date) 12 | @pages = current_views_per_days.top(100).group_by_page 13 | end 14 | 15 | def show 16 | page_scope = current_views_per_days.where(page: page_from_params) 17 | previous_page_scope = previous_views_per_days.where(page: page_from_params) 18 | @histogram = Histogram.new(page_scope.order_by_date.group_by_date, from_date, to_date) 19 | @previous_histogram = Histogram.new(previous_page_scope.order_by_date.group_by_date, previous_from_date, previous_to_date) 20 | @next_pages = current_views_per_days.where(referrer_host: params[:site], referrer_path: page_from_params).top(100).group_by_page 21 | @previous_pages = page_scope.top(100).group_by_referrer_page 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/active_analytics/referrers_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency "active_analytics/application_controller" 2 | 3 | module ActiveAnalytics 4 | class ReferrersController < ApplicationController 5 | before_action :require_date_range 6 | 7 | def index 8 | @referrers = current_views_per_days.top(100).group_by_referrer_site 9 | @histogram = Histogram.new(current_views_per_days.order_by_date.group_by_date, from_date, to_date) 10 | @previous_histogram = Histogram.new(previous_views_per_days.order_by_date.group_by_date, previous_from_date, previous_to_date) 11 | end 12 | 13 | def show 14 | referrer_host, referrer_path = params[:referrer].split("/", 2) 15 | scope = current_views_per_days.where(referrer_host: referrer_host) 16 | scope = scope.where(referrer_path: "/" + referrer_path) if referrer_path.present? 17 | previous_scope = previous_views_per_days.where(referrer_host: params[:referrer]) 18 | @histogram = Histogram.new(scope.order_by_date.group_by_date, from_date, to_date) 19 | @previous_histogram = Histogram.new(previous_scope.order_by_date.group_by_date, previous_from_date, previous_to_date) 20 | @previous_pages = scope.top(100).group_by_referrer_page 21 | @next_pages = scope.top(100).group_by_page 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/active_analytics/sites_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency "active_analytics/application_controller" 2 | 3 | module ActiveAnalytics 4 | class SitesController < ApplicationController 5 | before_action :require_date_range, only: :show 6 | 7 | def index 8 | @sites = ViewsPerDay.after(30.days.ago).order_by_totals.group_by_site 9 | redirect_to(site_path(@sites.first.host)) if @sites.size == 1 10 | end 11 | 12 | def show 13 | @histogram = Histogram.new(current_views_per_days.order_by_date.group_by_date, from_date, to_date) 14 | @previous_histogram = Histogram.new(previous_views_per_days.order_by_date.group_by_date, previous_from_date, previous_to_date) 15 | @referrers = current_views_per_days.top.group_by_referrer_site 16 | @pages = current_views_per_days.top.group_by_page 17 | @browsers = BrowsersPerDay.filter_by(params).group_by_name.top 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/helpers/active_analytics/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ActiveAnalytics 2 | module ApplicationHelper 3 | def format_view_count(number) 4 | number_with_delimiter(number.to_i) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/helpers/active_analytics/browsers_helper.rb: -------------------------------------------------------------------------------- 1 | module ActiveAnalytics 2 | module BrowsersHelper 3 | def browser_icon(browser_name) 4 | path = "browsers/#{browser_name.to_s.parameterize(separator: "_")}.svg" 5 | if AssetsController.endpoints.include?(path) 6 | image_tag(asset_url(path, host: request.host), alt: browser_name, class: "referer-favicon", width: 16, height: 16) 7 | else 8 | image_tag(asset_url("browsers/default.svg", host: request.host), alt: browser_name, class: "referer-favicon", width: 16, height: 16) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/helpers/active_analytics/pages_helper.rb: -------------------------------------------------------------------------------- 1 | module ActiveAnalytics 2 | module PagesHelper 3 | def page_to_params(name) 4 | name == "/" ? "index" : name[1..-1] 5 | end 6 | 7 | def page_from_params 8 | if params[:page] == "index" 9 | "/" 10 | elsif params[:page].present? 11 | "/#{params[:page]}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/helpers/active_analytics/referrers_helper.rb: -------------------------------------------------------------------------------- 1 | module ActiveAnalytics 2 | module ReferrersHelper 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/active_analytics/sites_helper.rb: -------------------------------------------------------------------------------- 1 | module ActiveAnalytics 2 | module SitesHelper 3 | def site_icon(host) 4 | image_tag("https://icons.duckduckgo.com/ip3/#{host}.ico", referrerpolicy: "no-referrer", class: "referer-favicon") 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/jobs/active_analytics/application_job.rb: -------------------------------------------------------------------------------- 1 | module ActiveAnalytics 2 | class ApplicationJob < ActiveJob::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/lib/active_analytics/histogram.rb: -------------------------------------------------------------------------------- 1 | module ActiveAnalytics 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 48 | -------------------------------------------------------------------------------- /app/mailers/active_analytics/application_mailer.rb: -------------------------------------------------------------------------------- 1 | module ActiveAnalytics 2 | class ApplicationMailer < ActionMailer::Base 3 | default from: 'from@example.com' 4 | layout 'mailer' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/models/active_analytics/application_record.rb: -------------------------------------------------------------------------------- 1 | module ActiveAnalytics 2 | class ApplicationRecord < ActiveRecord::Base 3 | self.abstract_class = true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/active_analytics/browsers_per_day.rb: -------------------------------------------------------------------------------- 1 | module ActiveAnalytics 2 | class BrowsersPerDay < ApplicationRecord 3 | # TODO: Deduplicate 4 | scope :top, -> (n = 10) { order_by_totals.limit(n) } 5 | scope :order_by_date, -> { order(:date) } 6 | scope :order_by_totals, -> { order(Arel.sql("SUM(total) DESC")) } 7 | scope :between_dates, -> (from, to) { where(date: from..to) } 8 | 9 | def self.append(params) 10 | total = params.delete(:total) || 1 11 | params[:site] = params[:site].downcase if params[:site] 12 | where(params).first.try(:increment!, :total, total) || create!(params.merge(total: total)) 13 | end 14 | 15 | def self.group_by_name 16 | group(:name).select("name, sum(total) AS total") 17 | end 18 | 19 | def self.group_by_version 20 | group(:name, :version).select("version, sum(total) AS total") 21 | end 22 | 23 | def self.group_by_date 24 | group(:date).select("date, sum(total) as total") 25 | end 26 | 27 | def self.filter_by(params) 28 | scope = all 29 | scope = scope.between_dates(params[:from], params[:to]) if params[:from].present? && params[:to].present? 30 | scope = scope.where(site: params[:site]) if params[:site].present? 31 | scope = scope.where(name: params[:id]) if params[:id].present? 32 | scope = scope.where(version: params[:version]) if params[:version].present? 33 | scope 34 | end 35 | 36 | def to_param 37 | name 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/models/active_analytics/views_per_day.rb: -------------------------------------------------------------------------------- 1 | module ActiveAnalytics 2 | class ViewsPerDay < ApplicationRecord 3 | validates_presence_of :site, :page, :date 4 | 5 | scope :between_dates, -> (from, to) { where("date BETWEEN ? AND ?", from, to) } 6 | scope :after, -> (date) { where("date > ?", date) } 7 | scope :order_by_totals, -> { order(Arel.sql("SUM(total) DESC")) } 8 | scope :order_by_date, -> { order(:date) } 9 | scope :top, -> (n = 10) { order_by_totals.limit(n) } 10 | 11 | class Site 12 | attr_reader :host, :total 13 | def initialize(host, total) 14 | @host, @total = host, total 15 | end 16 | end 17 | 18 | class Page 19 | attr_reader :host, :path, :total 20 | def initialize(host, path, total) 21 | @host, @path, @total = host, path, total 22 | end 23 | 24 | def url 25 | host + path 26 | end 27 | end 28 | 29 | def self.group_by_site 30 | group(:site).pluck("site, SUM(total)").map do |row| 31 | Site.new(row[0], row[1]) 32 | end 33 | end 34 | 35 | def self.group_by_page 36 | group(:site, :page).pluck("site, page, SUM(total)").map do |row| 37 | Page.new(row[0], row[1], row[2]) 38 | end 39 | end 40 | 41 | def self.group_by_referrer_site 42 | group(:referrer_host).pluck("referrer_host, SUM(total)").map do |row| 43 | Site.new(row[0], row[1]) 44 | end 45 | end 46 | 47 | def self.group_by_referrer_page 48 | group(:referrer_host, :referrer_path).pluck("referrer_host, referrer_path, SUM(total)").map do |row| 49 | Page.new(row[0], row[1], row[2]) 50 | end 51 | end 52 | 53 | def self.group_by_date 54 | group(:date).select("date, sum(total) AS total") 55 | end 56 | 57 | def self.to_histogram 58 | Histogram.new(self) 59 | end 60 | 61 | def self.append(params) 62 | total = params.delete(:total) || 1 63 | params[:site] = params[:site].downcase if params[:site] 64 | params[:referrer_path] = nil if params[:referrer_path].blank? 65 | params[:referrer_path] = params[:referrer_path].downcase if params[:referrer_path] 66 | params[:referrer_host] = params[:referrer_host].downcase if params[:referrer_host] 67 | where(params).first.try(:increment!, :total, total) || create!(params.merge(total: total)) 68 | end 69 | 70 | SLASH = "/" 71 | 72 | def self.split_referrer(referrer) 73 | return [nil, nil] if referrer.blank? 74 | if (uri = URI(referrer)).host.present? 75 | [uri.host, uri.path.presence] 76 | else 77 | strings = referrer.split(SLASH, 2) 78 | [strings[0], strings[1] ? SLASH + strings[1] : nil] 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /app/views/active_analytics/assets/_charts.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Charts.css v0.9.0 (https://ChartsCSS.org/) 3 | * Copyright 2020 Rami Yushuvaev 4 | * Licensed under MIT 5 | */ 6 | .active-analytics .charts-css { 7 | --chart-bg-color: transparent; 8 | --heading-size: 0px; 9 | --primary-axis-color: rgba(var(--color-grey-100), 1); 10 | --primary-axis-style: solid; 11 | --primary-axis-width: 1px; 12 | --secondary-axes-color: rgba(var(--color-grey-50), 1); 13 | --secondary-axes-style: solid; 14 | --secondary-axes-width: 1px; 15 | --data-axes-color: rgba(var(--color-grey-200), 1); 16 | --data-axes-style: solid; 17 | --data-axes-width: 1px; 18 | --legend-border-color: rgba(var(--color-grey-200), 1); 19 | position: relative; 20 | display: block; 21 | margin: 0; 22 | padding: 0; 23 | border: 0; 24 | } 25 | 26 | /* 27 | * Chart wrapper element 28 | */ 29 | 30 | .active-analytics .charts-css, 31 | .active-analytics .charts-css::after, 32 | .active-analytics .charts-css::before, 33 | .active-analytics .charts-css *, 34 | .active-analytics .charts-css *::after, 35 | .active-analytics .charts-css *::before { 36 | box-sizing: border-box; 37 | } 38 | 39 | /* 40 | * Reset table element 41 | */ 42 | .active-analytics table.charts-css { 43 | border-collapse: collapse; 44 | border-spacing: 0; 45 | empty-cells: show; 46 | overflow: initial; 47 | background-color: transparent; 48 | } 49 | 50 | .active-analytics table.charts-css caption, 51 | .active-analytics table.charts-css colgroup, 52 | .active-analytics table.charts-css thead, 53 | .active-analytics table.charts-css tbody, 54 | .active-analytics table.charts-css tr, 55 | .active-analytics table.charts-css th, 56 | .active-analytics table.charts-css td { 57 | display: block; 58 | margin: 0; 59 | padding: 0; 60 | border: 0; 61 | background-color: transparent; 62 | } 63 | 64 | .active-analytics table.charts-css colgroup, 65 | .active-analytics table.charts-css thead, 66 | .active-analytics table.charts-css tfoot { 67 | display: none; 68 | } 69 | 70 | 71 | /* 72 | * Chart colors 73 | */ 74 | 75 | .active-analytics .charts-css.column tbody tr td { 76 | background: rgba(var(--color-grey-100), 1); 77 | padding: 0; 78 | } 79 | 80 | /* 81 | * Chart data 82 | */ 83 | .active-analytics .charts-css.hide-data .data { 84 | opacity: 0; 85 | } 86 | 87 | .active-analytics .charts-css.show-data-on-hover .data { 88 | transition-duration: .3s; 89 | opacity: 0; 90 | } 91 | 92 | .active-analytics .charts-css.show-data-on-hover tr:hover .data { 93 | transition-duration: .3s; 94 | opacity: 1; 95 | } 96 | 97 | /* 98 | * Chart labels 99 | */ 100 | 101 | .active-analytics .charts-css.column:not(.show-labels) { 102 | --labels-size: 0; 103 | } 104 | 105 | .active-analytics .charts-css.column:not(.show-labels) tbody tr th { 106 | display: none; 107 | } 108 | 109 | .active-analytics .charts-css.column.show-labels { 110 | --labels-size: 1.5rem; 111 | } 112 | 113 | .active-analytics .charts-css.column.show-labels tbody tr th { 114 | display: flex; 115 | justify-content: var(--labels-align, center); 116 | align-items: center; 117 | flex-direction: column; 118 | } 119 | 120 | @media (max-width: 600px) { 121 | .active-analytics .charts-css.column.show-labels { 122 | --labels-size: 0; 123 | } 124 | 125 | .active-analytics .charts-css.column.show-labels tbody tr th { 126 | display: none; 127 | } 128 | } 129 | 130 | /* 131 | * Chart axes 132 | */ 133 | .active-analytics .charts-css.column.show-primary-axis:not(.reverse) tbody tr { 134 | border-block-end: var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color); 135 | } 136 | 137 | .active-analytics .charts-css.column.show-primary-axis.reverse tbody tr { 138 | border-block-start: var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color); 139 | } 140 | 141 | .active-analytics .charts-css.column.show-5-secondary-axes:not(.reverse) tbody tr { 142 | background-size: 100% 20%; 143 | background-image: linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width), transparent var(--secondary-axes-width)); 144 | } 145 | 146 | .active-analytics .charts-css.column.show-5-secondary-axes.reverse tbody tr { 147 | background-size: 100% 20%; 148 | background-image: linear-gradient(0deg, var(--secondary-axes-color) var(--secondary-axes-width), transparent var(--secondary-axes-width)); 149 | } 150 | 151 | /* 152 | * Chart tooltips 153 | */ 154 | .active-analytics .charts-css .tooltip { 155 | position: absolute; 156 | z-index: 1; 157 | bottom: 50%; 158 | left: 50%; 159 | transform: translateX(-50%); 160 | width: max-content; 161 | padding: 5px 10px; 162 | border-radius: 6px; 163 | visibility: hidden; 164 | opacity: 0; 165 | transition: opacity .3s; 166 | background-color: rgba(var(--color-grey-500), 1); 167 | color: rgba(var(--color-grey-00), 1); 168 | text-align: center; 169 | font-size: .9rem; 170 | } 171 | 172 | .active-analytics .charts-css .tooltip::after { 173 | content: ""; 174 | position: absolute; 175 | top: 100%; 176 | left: 50%; 177 | margin-left: -5px; 178 | border-width: 5px; 179 | border-style: solid; 180 | border-color: rgba(var(--color-grey-500), 1) transparent transparent; 181 | } 182 | 183 | .active-analytics .charts-css tr:hover .tooltip { 184 | visibility: visible; 185 | opacity: 1; 186 | } 187 | 188 | /* 189 | * Column Chart 190 | */ 191 | .active-analytics .charts-css.column tbody { 192 | display: flex; 193 | justify-content: space-between; 194 | align-items: stretch; 195 | width: 100%; 196 | gap: 1px; 197 | height: calc(100% - var(--heading-size)); 198 | } 199 | 200 | .active-analytics .charts-css.column tbody tr { 201 | position: relative; 202 | flex-grow: 1; 203 | flex-shrink: 1; 204 | flex-basis: 0; 205 | overflow-wrap: anywhere; 206 | display: flex; 207 | justify-content: flex-start; 208 | min-width: 0; 209 | } 210 | 211 | .active-analytics .charts-css.column tbody tr th { 212 | position: absolute; 213 | right: 0; 214 | left: 0; 215 | } 216 | 217 | .active-analytics .charts-css.column tbody tr td { 218 | display: flex; 219 | justify-content: center; 220 | width: 100%; 221 | height: calc(100% * var(--size, 1)); 222 | position: relative; 223 | } 224 | 225 | .active-analytics .charts-css.column:not(.reverse) tbody tr { 226 | align-items: flex-end; 227 | margin-block-end: var(--labels-size); 228 | } 229 | 230 | .active-analytics .charts-css.column:not(.reverse) tbody tr th { 231 | bottom: calc(-1 * var(--labels-size) - var(--primary-axis-width)); 232 | height: var(--labels-size); 233 | color: rgba(var(--color-grey-400), 1); 234 | font-weight: 400; 235 | } 236 | 237 | .active-analytics .charts-css.column:not(.reverse) tbody tr td { 238 | align-items: flex-start; 239 | } 240 | 241 | .active-analytics .charts-css.column:not(.stacked) tbody tr td { 242 | flex-grow: 1; 243 | flex-shrink: 1; 244 | flex-basis: 0; 245 | } 246 | 247 | .active-analytics .charts-css.column:not(.reverse-data) tbody { 248 | flex-direction: row; 249 | } -------------------------------------------------------------------------------- /app/views/active_analytics/assets/_style.css: -------------------------------------------------------------------------------- 1 | .active-analytics body { 2 | padding: 0; 3 | margin: 0; 4 | width: 100%; 5 | } 6 | 7 | .active-analytics body > header nav { 8 | width: 100%; 9 | max-width: 1280px; 10 | margin: 0 auto; 11 | padding: 24px; 12 | display: flex; 13 | flex-wrap: wrap; 14 | align-items: center; 15 | min-height: 88px; 16 | } 17 | 18 | .active-analytics header nav .menubutton { 19 | margin-left: auto; 20 | } 21 | 22 | .active-analytics header .logo { 23 | margin-right: 8px; 24 | } 25 | 26 | .active-analytics header .logo svg { 27 | width: 24px; 28 | height: 24px; 29 | fill: none; 30 | stroke: rgba(var(--color-blue-500), 1); 31 | stroke-width: 1; 32 | vertical-align: middle; 33 | } 34 | 35 | .active-analytics header .is-link { 36 | text-transform: none; 37 | text-decoration: none; 38 | font-size: 1rem; 39 | letter-spacing: 0; 40 | font-weight: 400; 41 | padding: 0 16px; 42 | margin: 0; 43 | min-height: 24px; 44 | color: rgba(var(--color-grey-700), 1); 45 | } 46 | 47 | .active-analytics header .is-link:hover { 48 | background: rgba(var(--color-grey-50), 1); 49 | } 50 | 51 | .active-analytics main { 52 | width: 100%; 53 | max-width: 1280px; 54 | margin: 0 auto; 55 | padding: 0 24px; 56 | } 57 | 58 | .active-analytics main h2 { 59 | word-break: break-word; 60 | display: flex; 61 | gap: var(--space); 62 | } 63 | 64 | .active-analytics section { 65 | margin-bottom: var(--space-4x); 66 | } 67 | 68 | .active-analytics .tooltip-date { 69 | color: rgba(var(--color-grey-100), 1); 70 | } 71 | 72 | .active-analytics .card h3 { 73 | display: flex; 74 | } 75 | 76 | .active-analytics .card h3 small { 77 | color: rgba(var(--color-red-500), 1); 78 | } 79 | 80 | .active-analytics .card h3 small.is-success { 81 | color: rgba(var(--color-green-500), 1); 82 | } 83 | 84 | .active-analytics .card h3 a { 85 | margin-left: auto; 86 | font-weight: 400; 87 | font-size: 1rem; 88 | } 89 | 90 | .active-analytics .card table { 91 | margin: 0 -24px -24px; 92 | width: calc(100% + 48px); 93 | } 94 | 95 | .active-analytics tr td:first-of-type { 96 | padding: 12px 0px 12px 24px; 97 | } 98 | 99 | .active-analytics tr td:last-of-type { 100 | padding: 12px 24px 12px 0; 101 | } 102 | 103 | .active-analytics td a { 104 | word-break: break-word; 105 | } 106 | 107 | .active-analytics td.number { 108 | text-align: right; 109 | font-variant-numeric: tabular-nums; 110 | } 111 | 112 | .active-analytics .referer-favicon { 113 | width: 16px; 114 | height: 16px; 115 | display: inline-block; 116 | position: relative; 117 | top: 3px; 118 | margin-right: 4px; 119 | } 120 | 121 | .active-analytics div.is-empty { 122 | display: flex; 123 | align-items: center; 124 | justify-content: center; 125 | height: calc(100% - 72px); 126 | width: 100%; 127 | color: rgba(var(--color-grey-300), 1); 128 | } 129 | 130 | .active-analytics body > footer { 131 | max-width: 1280px; 132 | margin: 48px auto 0; 133 | padding: 24px; 134 | } 135 | 136 | .active-analytics body > footer ul { 137 | margin: 0; 138 | list-style-type: none; 139 | padding: 0; 140 | } 141 | 142 | .active-analytics body > footer .card { 143 | margin: 0; 144 | background: rgba(var(--color-grey-50), 1); 145 | } 146 | 147 | .active-analytics body > footer .card p { 148 | padding: 0 0 2px; 149 | color: rgba(var(--color-grey-400), 1); 150 | } 151 | 152 | @media (max-width: 480px) { 153 | .active-analytics header nav .menubutton { 154 | margin-top: 24px; 155 | } 156 | 157 | .active-analytics [role="dialog"] { 158 | min-width: 280px; 159 | } 160 | } -------------------------------------------------------------------------------- /app/views/active_analytics/assets/application.css.erb: -------------------------------------------------------------------------------- 1 | <%= render "charts" %> 2 | <%= render "style" %> 3 | -------------------------------------------------------------------------------- /app/views/active_analytics/assets/application.js: -------------------------------------------------------------------------------- 1 | ActiveAnalytics = {} 2 | 3 | ActiveAnalytics.Header = function() { 4 | } 5 | 6 | ActiveAnalytics.Header.prototype.toggleDateRangeForm = function() { 7 | var form = this.node.querySelector("#dateRangeForm") 8 | if (form.hasAttribute("hidden")) 9 | form.removeAttribute("hidden") 10 | else 11 | form.setAttribute("hidden", "hidden") 12 | } 13 | 14 | Ariato.launchWhenDomIsReady() 15 | -------------------------------------------------------------------------------- /app/views/active_analytics/assets/ariato.css: -------------------------------------------------------------------------------- 1 | /*******************************************/ 2 | /* ariato css */ 3 | /* https://github.com/BaseSecrete/ariato */ 4 | /*******************************************/ 5 | 6 | .active-analytics { 7 | --color-blue-100: 199,231,236; 8 | --color-blue-200: 147,206,223; 9 | --color-blue-300: 51,152,207; 10 | --color-blue-400: 0,115,198; 11 | --color-blue-500: 0,67,184; 12 | --color-blue-600: 22,50,155; 13 | --color-blue-700: 23,20,98; 14 | --color-red-20: 253,249,248; 15 | --color-red-200: 233,183,173; 16 | --color-red-500: 164,0,27; 17 | --color-red-600: 123,0,15; 18 | --color-green-500: 119,187,118; 19 | --color-grey-00: 255,255,248; 20 | --color-grey-20: 253,253,246; 21 | --color-grey-35: 249,249,242; 22 | --color-grey-50: 244,243,236; 23 | --color-grey-100: 225,224,217; 24 | --color-grey-200: 195,194,187; 25 | --color-grey-300: 142,140,134; 26 | --color-grey-400: 112,109,103; 27 | --color-grey-500: 80,77,72; 28 | --color-grey-600: 59,57,51; 29 | --color-grey-700: 20,21,15; 30 | --color-black: 0,0.2913950572018906,0; 31 | --color-bg: var(--color-grey-20); 32 | --color-body: var(--color-grey-500); 33 | } 34 | 35 | @media (prefers-color-scheme: dark) { 36 | .active-analytics { 37 | --color-blue-100: 34,44,64; 38 | --color-blue-200: 57,73,106; 39 | --color-blue-300: 82,105,152; 40 | --color-blue-400: 108,138,200; 41 | --color-blue-500: 135,173,250; 42 | --color-blue-600: 145,191,252; 43 | --color-blue-700: 161,208,252; 44 | --color-red-20: 47,27,24; 45 | --color-red-200: 103,62,59; 46 | --color-red-500: 244,146,140; 47 | --color-red-600: 248,165,157; 48 | --color-green-500: 119,187,118; 49 | --color-grey-00: 14,18,26; 50 | --color-grey-20: 16,20,28; 51 | --color-grey-35: 21,25,34; 52 | --color-grey-50: 24,28,36; 53 | --color-grey-100: 40,44,54; 54 | --color-grey-200: 69,74,84; 55 | --color-grey-300: 100,106,116; 56 | --color-grey-400: 133,139,150; 57 | --color-grey-500: 168,174,186; 58 | --color-grey-600: 180,189,200; 59 | --color-grey-700: 193,204,214; 60 | --color-grey-800: 206,219,228; 61 | --color-grey-900: 219,234,241; 62 | 63 | --color-black: 0,0,0; 64 | --color-bg: var(--color-grey-20); 65 | --color-body: var(--color-grey-500); 66 | } 67 | } 68 | 69 | .active-analytics body { 70 | background: rgba(var(--color-bg), 1); 71 | color: rgba(var(--color-body), 1); 72 | } 73 | 74 | .active-analytics :focus { 75 | outline: 2px solid rgba(var(--color-blue-200), 1); 76 | } 77 | 78 | /* design_tokens/font.css */ 79 | .active-analytics { 80 | --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 81 | --base-font-size: var(--space-2x); 82 | --font-weight-400: 400; 83 | --font-weight-700: 700; 84 | --ratio: 1.2; 85 | --font-size-30: calc(var(--base-font-size) / var(--ratio) / var(--ratio)); /* -2 */ 86 | --font-size-40: calc(var(--base-font-size) / var(--ratio)); /* -1 */ 87 | --font-size-50: var(--base-font-size); 88 | --font-size-60: calc(var(--base-font-size) * var(--ratio)); /* +1 */ 89 | --font-size-70: calc(var(--base-font-size) * var(--ratio) * var(--ratio)); /* +2 */ 90 | --font-size-80: calc(var(--base-font-size) * var(--ratio) * var(--ratio) * var(--ratio)); /* +4 */ 91 | --font-size-90: calc(var(--base-font-size) * var(--ratio) * var(--ratio) * var(--ratio) * var(--ratio)); /* +5 */ 92 | --font-size-100: calc(var(--base-font-size) * var(--ratio) * var(--ratio) * var(--ratio) * var(--ratio) * var(--ratio)); /* +6 */ 93 | --font-size-110: calc(var(--base-font-size) * var(--ratio) * var(--ratio) * var(--ratio) * var(--ratio) * var(--ratio) * var(--ratio)); /* +7 */ 94 | } 95 | 96 | .active-analytics, 97 | .active-analytics body { 98 | font-size: var(--base-font-size); 99 | } 100 | 101 | .active-analytics body { 102 | font-family: var(--font-sans); 103 | font-weight: var(--font-weight-400); 104 | line-height: var(--space-3x); 105 | } 106 | 107 | /* design_tokens/shadow.css */ 108 | .active-analytics { 109 | --box-shadow-s: 110 | 0 1px 1px 0 rgba(var(--color-black), 0.05), 111 | 0 1px 2px 0 rgba(var(--color-black), 0.05); 112 | --box-shadow-l: 113 | 0 0px 2px 0 rgba(var(--color-black), 0.05), 114 | 0 1px 2px 0 rgba(var(--color-black), 0.05), 115 | 0 2px 4px -1px rgba(var(--color-black), 0.05), 116 | 0 4px 8px -2px rgba(var(--color-black), 0.05), 117 | 0 8px 16px -4px rgba(var(--color-black), 0.05), 118 | 0 16px 32px -8px rgba(var(--color-black), 0.05); 119 | --box-inset-shadow-s: 0 1px 1px 0 rgba(var(--color-black), 0.05) inset; 120 | --box-inset-shadow-m: 0 1px 1px 0px rgba(var(--color-black), 0.05) inset, 121 | 0 2px 2px 1px rgba(var(--color-black), 0.05) inset; 122 | --text-shadow-s: 0 1px 1px rgba(var(--color-black), 0.05); 123 | } 124 | 125 | /* design_tokens/shape.css */ 126 | .active-analytics { 127 | --border-radius-s: calc(var(--space) * 0.25); 128 | --border-radius-m: var(--space-1-2); 129 | } 130 | 131 | /* design_tokens/space.css */ 132 | *, 133 | *::before, 134 | *::after { 135 | box-sizing: border-box; 136 | } 137 | 138 | .active-analytics { 139 | --space: 0.5rem; 140 | --space-1-4: calc(var(--space) / 4); 141 | --space-1-2: calc(var(--space) / 2); 142 | --space-2x: calc(var(--space) * 2); 143 | --space-3x: calc(var(--space) * 3); 144 | --space-4x: calc(var(--space) * 4); 145 | --space-5x: calc(var(--space) * 5); 146 | --space-6x: calc(var(--space) * 6); 147 | --space-7x: calc(var(--space) * 7); 148 | } 149 | 150 | /* elements */ 151 | 152 | /* text */ 153 | .active-analytics ::-moz-selection { 154 | background: rgba(var(--color-blue-100), 1); 155 | text-shadow: none; 156 | } 157 | 158 | .active-analytics ::selection { 159 | background: rgba(var(--color-blue-100), 1); 160 | text-shadow: none; 161 | } 162 | 163 | .active-analytics p { 164 | margin: 0; 165 | padding: calc(var(--space) - 2px) 0 calc(var(--space-2x) + 2px); 166 | } 167 | 168 | .active-analytics p.is-large { 169 | font-size: var(--font-size-70); 170 | line-height: var(--space-4x); 171 | padding: var(--space) 0 var(--space-2x); 172 | } 173 | 174 | .active-analytics p + :not(p) { 175 | margin-top: var(--space-3x); 176 | } 177 | 178 | .active-analytics h1, 179 | .active-analytics .is-h1 { 180 | display: inline-block; 181 | margin: 0; 182 | padding: calc(var(--space-2x) + 2px) 0 calc(var(--space) - 2px); 183 | color: rgba(var(--color-grey-700), 1); 184 | font-size: var(--font-size-110); 185 | font-weight: 700; 186 | letter-spacing: -0.05rem; 187 | line-height: var(--space-7x); 188 | text-transform: none; 189 | } 190 | 191 | .active-analytics h2, 192 | .active-analytics .is-h2 { 193 | margin: 0; 194 | padding: calc(var(--space) + 1px) 0 calc(var(--space-2x) - 1px); 195 | color: rgba(var(--color-grey-700), 1); 196 | font-size: var(--font-size-100); 197 | font-weight: 700; 198 | letter-spacing: -0.05rem; 199 | line-height: var(--space-6x); 200 | text-transform: none; 201 | } 202 | 203 | .active-analytics * + h2, 204 | .active-analytics * + .is-h2 { 205 | margin-top: var(--space-3x); 206 | } 207 | 208 | .active-analytics h3, 209 | .active-analytics .is-h3 { 210 | margin: 0; 211 | padding: calc(var(--space) - 3px) 0 calc(var(--space-2x) + 3px); 212 | color: rgba(var(--color-grey-700), 1); 213 | font-size: var(--font-size-60); 214 | font-weight: 700; 215 | letter-spacing: 0; 216 | line-height: var(--space-3x); 217 | text-transform: none; 218 | } 219 | 220 | .active-analytics small { 221 | font-size: var(--font-size-30); 222 | font-weight: 400; 223 | text-transform: none; 224 | color: rgba(var(--color-grey-400), 1); 225 | } 226 | 227 | /**** BUTTONS *****/ 228 | .active-analytics button, 229 | .active-analytics input[type="submit"], 230 | .active-analytics input[type="button"], 231 | .active-analytics input[type="reset"], 232 | .active-analytics [role="button"], 233 | .active-analytics :any-link[role="button"] { 234 | /* Display */ 235 | display: inline-flex; 236 | width: auto; 237 | cursor: pointer; 238 | align-items: center; 239 | justify-content: center; 240 | align-self: start; 241 | transition: 0.2s all ease-in-out; 242 | background-color: transparent; 243 | min-height: var(--space-5x); 244 | margin: 0; 245 | padding: var(--space) calc(var(--space) * 2); 246 | border: none; 247 | border-radius: var(--border-radius-s, 0); 248 | box-shadow: 0 0 0 1px rgba(var(--color-grey-200), 1) inset, 249 | var(--box-shadow-s); 250 | outline: 0; 251 | color: rgba(var(--color-grey-700), 1); 252 | font: inherit; 253 | font-family: inherit; 254 | font-size: var(--font-size-40); 255 | font-weight: 700; 256 | letter-spacing: 0.05rem; 257 | line-height: var(--space-3x); 258 | text-align: center; 259 | text-decoration: none; 260 | text-transform: uppercase; 261 | text-shadow: var(--text-shadow-s); 262 | } 263 | 264 | .active-analytics button:hover, 265 | .active-analytics input[type="submit"]:hover, 266 | .active-analytics input[type="button"]:hover, 267 | .active-analytics input[type="reset"]:hover, 268 | .active-analytics [role="button"]:hover, 269 | .active-analytics :any-link[role="button"]:hover, 270 | .active-analytics button:active, 271 | .active-analytics input[type="submit"]:active, 272 | .active-analytics input[type="button"]:active, 273 | .active-analytics input[type="reset"]:active, 274 | [role="button"]:active, 275 | :any-link[role="button"]:active { 276 | color: rgba(var(--color-grey-800), 1); 277 | box-shadow: 0 0 0 1px rgba(var(--color-grey-300), 1) inset, 278 | var(--box-shadow-s); 279 | } 280 | 281 | .active-analytics button:focus, 282 | .active-analytics input[type="submit"]:focus, 283 | .active-analytics input[type="button"]:focus, 284 | .active-analytics input[type="reset"]:focus, 285 | .active-analytics [role="button"]:focus, 286 | .active-analytics :any-link[role="button"]:focus { 287 | outline: 2px solid rgba(var(--color-blue-200), 1); 288 | border: 0; 289 | color: rgba(var(--color-grey-900), 1); 290 | z-index: 1; 291 | } 292 | 293 | .active-analytics button[aria-expanded="true"], 294 | .active-analytics button[type="reset"][aria-expanded="true"], 295 | .active-analytics input[type="submit"][aria-expanded="true"], 296 | .active-analytics input[type="button"][aria-expanded="true"], 297 | .active-analytics input[type="reset"][aria-expanded="true"], 298 | .active-analytics [role="button"][aria-expanded="true"], 299 | .active-analytics :any-link[role="button"][aria-expanded="true"] { 300 | background: rgba(var(--color-grey-100), 1); 301 | color: rgba(var(--color-grey-600), 1); 302 | box-shadow: 0 0 0 1px rgba(var(--color-grey-200), 1) inset, 303 | var(--box-inset-shadow-m); 304 | } 305 | 306 | .active-analytics button[type="reset"], 307 | .active-analytics input[type="reset"], 308 | .active-analytics button.is-reset, 309 | .active-analytics input[type="submit"].is-reset, 310 | .active-analytics input[type="button"].is-reset, 311 | .active-analytics [role="button"].is-reset, 312 | .active-analytics :any-link[role="button"].is-reset { 313 | color: rgba(var(--color-red-500), 1); 314 | } 315 | 316 | .active-analytics button[type="reset"]:hover, 317 | .active-analytics input[type="reset"]:hover, 318 | .active-analytics button[type="reset"]:hover, 319 | .active-analytics input[type="reset"]:hover, 320 | .active-analytics button.is-reset:hover, 321 | .active-analytics input[type="submit"].is-reset:hover, 322 | .active-analytics input[type="button"].is-reset:hover, 323 | .active-analytics [role="button"].is-reset:hover, 324 | .active-analytics :any-link[role="button"].is-reset:hover, 325 | .active-analytics button[type="reset"]:active, 326 | .active-analytics input[type="reset"]:active, 327 | .active-analytics button.is-reset:active, 328 | .active-analytics input[type="submit"].is-reset:active, 329 | .active-analytics input[type="button"].is-reset:active, 330 | .active-analytics [role="button"].is-reset:active, 331 | .active-analytics :any-link[role="button"].is-reset:active { 332 | background: rgba(var(--color-red-500), 1); 333 | color: rgba(var(--color-red-20), 1); 334 | box-shadow: 0 0 0 1px rgba(var(--color-red-600), 1) inset, 335 | var(--box-shadow-s); 336 | } 337 | 338 | .active-analytics button[type="reset"]:focus, 339 | .active-analytics input[type="reset"]:focus, 340 | .active-analytics button.is-reset:focus, 341 | .active-analytics input[type="submit"].is-reset:focus, 342 | .active-analytics input[type="button"].is-reset:focus, 343 | .active-analytics [role="button"].is-reset:focus, 344 | .active-analytics :any-link[role="button"].is-reset:focus { 345 | background: rgba(var(--color-red-500), 1); 346 | color: rgba(var(--color-red-20), 1); 347 | outline: 2px solid rgba(var(--color-red-200), 1); 348 | } 349 | 350 | .active-analytics button.is-link, 351 | .active-analytics button[type="reset"].is-link, 352 | .active-analytics input[type="submit"].is-link, 353 | .active-analytics input[type="button"].is-link, 354 | .active-analytics input[type="reset"].is-link, 355 | .active-analytics [role="button"].is-link, 356 | .active-analytics :any-link[role="button"].is-link { 357 | background: transparent; 358 | color: rgba(var(--color-grey-500), 1); 359 | text-decoration: underline; 360 | border-color: transparent; 361 | box-shadow: none !important; 362 | } 363 | 364 | .active-analytics button.is-link:hover, 365 | .active-analytics button[type="reset"].is-link:hover, 366 | .active-analytics input[type="submit"].is-link:hover, 367 | .active-analytics input[type="button"].is-link:hover, 368 | .active-analytics input[type="reset"].is-link:hover, 369 | .active-analytics [role="button"].is-link:hover, 370 | .active-analytics :any-link[role="button"].is-link:hover, 371 | .active-analytics button.is-link:active, 372 | .active-analytics button[type="reset"].is-link:active, 373 | .active-analytics input[type="submit"].is-link:active, 374 | .active-analytics input[type="button"].is-link:active, 375 | .active-analytics input[type="reset"].is-link:active, 376 | .active-analytics [role="button"].is-link:active, 377 | .active-analytics :any-link[role="button"].is-link:active { 378 | background: transparent; 379 | color: rgba(var(--color-grey-700), 1); 380 | box-shadow: none; 381 | } 382 | 383 | .active-analytics button.is-link:focus, 384 | .active-analytics button[type="reset"].is-link:focus, 385 | .active-analytics input[type="submit"].is-link:focus, 386 | .active-analytics input[type="button"].is-link:focus, 387 | .active-analytics input[type="reset"].is-link:focus, 388 | .active-analytics [role="button"].is-link:focus, 389 | .active-analytics :any-link[role="button"].is-link:focus { 390 | color: rgba(var(--color-grey-700), 1); 391 | } 392 | 393 | /* FORMS */ 394 | .active-analytics form { 395 | overflow: visible; 396 | } 397 | 398 | .active-analytics form > * + * { 399 | margin-top: var(--space-3x, 1em); 400 | } 401 | 402 | .active-analytics form *[hidden] { 403 | margin: 0; 404 | } 405 | 406 | .active-analytics label { 407 | display: block; 408 | line-height: var(--space-3x); 409 | padding: 0; 410 | text-align: left; 411 | text-transform: uppercase; 412 | font-size: var(--font-size-30); 413 | font-weight: 400; 414 | letter-spacing: 0.1rem; 415 | color: rgba(var(--color-grey-500), 1); 416 | -webkit-user-select: none; 417 | -moz-user-select: none; 418 | -ms-user-select: none; 419 | } 420 | 421 | .active-analytics input { 422 | display: block; 423 | width: 100%; 424 | background-color: rgba(var(--color-grey-00), 1); 425 | min-height: var(--space-5x); 426 | padding: calc(var(--space) - 1px) calc(var(--space-2x) - 1px); 427 | margin: 0; 428 | border: 1px solid rgba(var(--color-grey-50), 1); 429 | border-radius: var(--border-radius-s, 0); 430 | outline: 0 none; 431 | box-shadow: var(--box-inset-shadow-s); 432 | color: rgba(var(--color-grey-600), 1); 433 | font-family: inherit; 434 | font-size: var(--base-font-size); 435 | line-height: var(--space-3x); 436 | } 437 | 438 | .active-analytics input:focus{ 439 | outline: 0 none; 440 | border-color: rgba(var(--color-blue-400), 1); 441 | box-shadow: 442 | 0 0 0 2px rgba(var(--color-blue-200), 1), 443 | var(--box-shadow-s); 444 | } 445 | 446 | .active-analytics label + input { 447 | margin-top: 0; 448 | } 449 | 450 | .active-analytics ::placeholder { 451 | color: rgba(var(--color-grey-300), 1); 452 | } 453 | 454 | .active-analytics a:any-link { 455 | color: rgba(var(--color-blue-500, inherit), 1); 456 | text-decoration: underline; 457 | } 458 | 459 | .active-analytics a:any-link:hover { 460 | color: rgba(var(--color-blue-600), 1); 461 | } 462 | 463 | .active-analytics a:any-link > * + * { 464 | margin-left: var(--space); 465 | text-decoration: none; 466 | } 467 | 468 | .active-analytics a:any-link[target="_blank"] { 469 | padding-right: 16px; 470 | position: relative; 471 | display: inline-block; 472 | } 473 | 474 | /* Arrow to signify external links */ 475 | .active-analytics a:any-link[target="_blank"]::after, 476 | .active-analytics a:any-link[target="_blank"]::before { 477 | content: ""; 478 | display: block; 479 | position: absolute; 480 | right: 0; 481 | } 482 | 483 | .active-analytics a:any-link[target="_blank"]::before { 484 | background: currentColor; 485 | transform: rotate(-45deg); 486 | top: 50%; 487 | height: 2px; 488 | width: 12px; 489 | } 490 | 491 | .active-analytics a:any-link[target="_blank"]::after { 492 | width: 8px; 493 | height: 8px; 494 | border-right: 2px solid; 495 | border-top: 2px solid; 496 | top: calc(50% - 5px); 497 | } 498 | 499 | .active-analytics table { 500 | width: 100%; 501 | padding: 0; 502 | border-spacing: 0; 503 | } 504 | 505 | .active-analytics th { 506 | text-transform: uppercase; 507 | font-weight: var(--font-weight-700); 508 | font-size: calc(var(--base-font-size) / var(--ratio) / var(--ratio)); 509 | letter-spacing: 0.03em; 510 | transform: translateY(2px); 511 | padding: 12px 24px; 512 | text-align: left; 513 | color: rgba(var(--color-grey-500), 1); 514 | } 515 | 516 | .active-analytics tbody tr { 517 | box-shadow: 0 -1px 0 0 rgba(var(--color-grey-50), 1) inset; 518 | } 519 | 520 | .active-analytics tbody tr:hover { 521 | background: rgba(var(--color-grey-50), 1); 522 | } 523 | 524 | .active-analytics td { 525 | color: rgba(var(--color-grey-500), 1); 526 | padding: 12px 24px; 527 | } 528 | 529 | /* components/breadcrumb.css */ 530 | 531 | .active-analytics .breadcrumb ol { 532 | margin: 0; 533 | padding-left: 0; 534 | list-style: none; 535 | } 536 | 537 | .active-analytics .breadcrumb li { 538 | display: inline; 539 | } 540 | 541 | .active-analytics .breadcrumb li + li::before { 542 | content: ''; 543 | display: inline-block; 544 | margin: 0 var(--space); 545 | transform: rotate(20deg); 546 | border-right: 1px solid rgba(var(--color-grey-400), 1); 547 | height: 0.75em; 548 | } 549 | 550 | .active-analytics .breadcrumb li:last-child { 551 | color: rgba(var(--color-grey-700), 1); 552 | font-weight: var(--font-weight-700); 553 | } 554 | 555 | /* components/dialog.css */ 556 | 557 | .active-analytics .dialog-backdrop { 558 | height: 100vh; 559 | width: 100%; 560 | position: fixed; 561 | top: 0; 562 | left: 0; 563 | background: rgba(var(--color-grey-300), .7); 564 | z-index: var(--z-40, 90); 565 | animation: backdrop 0.1s cubic-bezier(0.165, 0.840, 0.440, 1.000); 566 | display: flex; 567 | align-items: center; 568 | justify-content: center; 569 | } 570 | 571 | @supports (-webkit-backdrop-filter: none) or (backdrop-filter: none) { 572 | .active-analytics .dialog-backdrop { 573 | -webkit-backdrop-filter: blur(4px); 574 | backdrop-filter: blur(4px); 575 | background: rgba(var(--color-grey-300), .7); 576 | } 577 | } 578 | 579 | /* Modale */ 580 | 581 | .active-analytics [role="dialog"] { 582 | position: relative; 583 | padding: var(--space-3x); 584 | background-color: rgba(var(--color-bg), 1); 585 | border: 1px solid rgba(var(--color-grey-35), 1); 586 | border-radius: var(--border-radius-m, 0); 587 | min-width: 480px; 588 | max-width: 100%; 589 | max-height: 100vh; 590 | overflow-y: auto; 591 | animation: modale 0.5s cubic-bezier(0.165, 0.840, 0.440, 1.000); 592 | } 593 | 594 | .active-analytics [role="dialog"] > header { 595 | width: calc(100% + var(--space-6x)); 596 | display: flex; 597 | align-items: center; 598 | border-radius: var(--border-radius-m, 0) var(--border-radius-m, 0) 0 0; 599 | margin: calc(var(--space-3x) * -1) calc(var(--space-3x) * -1) calc(var(--space-3x) * 1); 600 | padding: calc(var(--space) * 1.5 - 1px) calc(var(--space-3x) - 1px) calc(var(--space) * 1.5 - 1px); 601 | border-bottom: 1px solid rgba(var(--color-grey-50), 1); 602 | color: rgba(var(--color-grey-500), 1); 603 | } 604 | 605 | .active-analytics [role="dialog"] > header button { 606 | margin-left: auto; 607 | } 608 | 609 | .active-analytics [role="dialog"] > footer { 610 | background: rgba(var(--color-grey-50), 1); 611 | width: calc(100% + var(--space-6x)); 612 | border-radius: 0 0 var(--border-radius-m, 0) var(--border-radius-m, 0); 613 | margin: var(--space-3x) calc(var(--space-3x) * -1) calc(var(--space-3x) * -1); 614 | padding: calc(var(--space) * 1.5 - 1px) calc(var(--space-3x) - 1px) calc(var(--space) * 1.5 - 1px); 615 | border-top: 1px solid rgba(var(--color-grey-50), 1); 616 | } 617 | 618 | @media screen and (min-width: 640px) { 619 | .active-analytics [role="dialog"] { 620 | min-width: 640px; 621 | min-height: auto; 622 | box-shadow: var(--box-shadow-l); 623 | background-color: rgba(var(--color-bg), 1); 624 | } 625 | } 626 | 627 | @keyframes modale { 628 | 0% { 629 | top: -48px; 630 | opacity: 0; 631 | } 632 | 100% { 633 | top: 0; 634 | opacity: 1; 635 | } 636 | } 637 | 638 | @keyframes backdrop { 639 | 0% { 640 | opacity: 0; 641 | } 642 | 100% { 643 | opacity: 1; 644 | } 645 | } 646 | /* components/grid_auto.css */ 647 | 648 | .active-analytics .grid-auto { 649 | --gap: var(--space-3x); 650 | --col-min-width: calc(var(--space) * 38); 651 | display: grid; 652 | grid-gap: var(--gap, 0); 653 | grid-template-columns: repeat(auto-fit, minmax(var(--col-min-width), 1fr)); 654 | width: 100%; 655 | padding: 0; 656 | } 657 | 658 | .active-analytics .grid-auto > * { 659 | list-style-type: none; 660 | margin: 0; 661 | } 662 | 663 | .active-analytics .grid-auto * + *, 664 | .active-analytics .grid-auto * + .card { 665 | margin-top: 0; 666 | } 667 | 668 | .active-analytics .grid-auto + * { 669 | margin-top: var(--space-3x); 670 | } 671 | 672 | /* components/group.css */ 673 | 674 | .active-analytics [role="group"] { 675 | display: flex; 676 | align-items: flex-end; 677 | border-radius: var(--border-radius-s, 0); 678 | border-radius: 4px; 679 | max-width: 100%; 680 | } 681 | 682 | .active-analytics [role="group"] > * { 683 | border-radius: 0; 684 | margin: auto 0 0; 685 | } 686 | 687 | .active-analytics [role="group"] > *:first-child { 688 | border-radius: var(--border-radius-s, 0) 0 0 var(--border-radius-s, 0); 689 | } 690 | 691 | .active-analytics [role="group"] > *:last-child { 692 | border-radius: 0 var(--border-radius-s, 0) var(--border-radius-s, 0) 0; 693 | } 694 | 695 | .active-analytics [role="group"] > .input { 696 | margin: auto var(--space) 0 0; 697 | } 698 | 699 | .active-analytics [role="group"] > .input:last-child { 700 | margin: auto 0 0 0; 701 | } 702 | 703 | .active-analytics [role="group"] > label { 704 | margin: 0; 705 | } 706 | 707 | .active-analytics [role="group"].is-block { 708 | width: 100%; 709 | } 710 | 711 | .active-analytics [role="group"].is-block > * { 712 | flex: 1; 713 | } 714 | 715 | /* components/card.css */ 716 | .active-analytics .card { 717 | background: rgba(var(--color-grey-20), 1); 718 | border: 0; 719 | border-radius: var(--border-radius-s, 0); 720 | box-shadow: 0 0 0 1px inset rgba(var(--color-grey-50), 1), var(--box-shadow-s); 721 | padding: var(--space-3x); 722 | list-style-type: none; 723 | position: relative; 724 | overflow: visible; 725 | } 726 | 727 | .active-analytics .card > header { 728 | display: flex; 729 | align-items: center; 730 | min-height: var(--space-6x); 731 | width: calc(100% + var(--space-6x)); 732 | border-radius: var(--border-radius-m, 0) var(--border-radius-m, 0) 0 0; 733 | padding: var(--space) var(--space-3x); 734 | margin: calc(var(--space-3x) * -1) calc(var(--space-3x) * -1) calc(var(--space-3x) * 1); 735 | box-shadow: 0 1px 0 0 rgba(var(--color-grey-50), 1); 736 | color: rgba(var(--color-grey-500), 1); 737 | } 738 | 739 | .active-analytics .card > header > * { 740 | margin: 0 var(--space-1-2); 741 | padding: 0; 742 | text-transform: none; 743 | letter-spacing: 0; 744 | font-size: var(--font-size-50); 745 | line-height: var(--space-3x); 746 | } 747 | 748 | .active-analytics .card > footer { 749 | background: rgba(var(--color-grey-50), 1); 750 | box-shadow: 0 -1px 0 0 rgba(var(--color-grey-100), .8); 751 | border-radius: 0 0 var(--border-radius-m, 0) var(--border-radius-m, 0); 752 | margin: var(--space-3x) calc(var(--space-3x) * -1) calc(var(--space-3x) * -1); 753 | padding: var(--space-2x) calc(var(--space-3x) - 1px); 754 | width: calc(100% + var(--space-6x)); 755 | } 756 | 757 | /* components/menu.css */ 758 | 759 | .active-analytics [role="menu"] { 760 | text-align: left; 761 | color: rgba(var(--color-grey-500), 1); 762 | position: relative; 763 | list-style-type: none; 764 | margin: 0; 765 | padding: 6px 0 18px; 766 | text-transform: none; 767 | font-size: var(--font-size-50); 768 | letter-spacing: 0.03em; 769 | } 770 | 771 | .active-analytics li[role="menuitem"] { 772 | display: flex; 773 | flex-direction: column; 774 | } 775 | 776 | .active-analytics a[role="menuitem"]:not(.button) { 777 | padding: 0 24px; 778 | margin: 0; 779 | font-size: inherit; 780 | text-decoration: none; 781 | color: rgba(var(--color-grey-700), 1); 782 | display: flex; 783 | align-items: center; 784 | } 785 | 786 | .active-analytics a[role="menuitem"]:not(.button):hover { 787 | color: rgba(var(--color-grey-800), 1); 788 | } 789 | 790 | .active-analytics a[role="menuitem"]:not(.button)[aria-current="true"] { 791 | background: rgba(var(--color-grey-100), 1); 792 | color: rgba(var(--color-blue-700), 1); 793 | } 794 | 795 | .active-analytics .menubutton { 796 | position: relative; 797 | display: inline-block; 798 | } 799 | 800 | .active-analytics .menubutton > button, 801 | .active-analytics .menubutton > input[type="submit"], 802 | .active-analytics .menubutton > input[type="button"], 803 | .active-analytics .menubutton > input[type="reset"], 804 | .active-analytics .menubutton > [role="button"], 805 | .active-analytics .menubutton > :any-link[role="button"] { 806 | position: relative; 807 | margin: 0; 808 | } 809 | 810 | .active-analytics .menubutton > button:after, 811 | .active-analytics .menubutton > input[type="submit"]:after, 812 | .active-analytics .menubutton > input[type="button"]:after, 813 | .active-analytics .menubutton > input[type="reset"]:after, 814 | .active-analytics .menubutton > [role="button"]:after, 815 | .active-analytics .menubutton > :any-link[role="button"]:after { 816 | content: "…"; 817 | margin-left: var(--space); 818 | } 819 | 820 | .active-analytics .menubutton > [role="menu"] { 821 | display: none; 822 | position: absolute; 823 | z-index: 1000; 824 | top: var(--space-7x); 825 | left: 0; 826 | margin: 0; 827 | padding: var(--space, 0) 0; 828 | border-radius: var(--border-radius-m, 0); 829 | width: auto; 830 | background: rgba(var(--color-grey-00), 1); 831 | box-shadow: var(--box-shadow-l); 832 | border: 1px solid rgba(var(--color-grey-50), 1); 833 | } 834 | 835 | .active-analytics .menubutton > [role="menu"]:after, 836 | .active-analytics .menubutton > [role="menu"]:before { 837 | bottom: 100%; 838 | left: var(--space-3x); 839 | border: solid transparent; 840 | content: ""; 841 | height: 0; 842 | width: 0; 843 | position: absolute; 844 | pointer-events: none; 845 | } 846 | 847 | .active-analytics .menubutton > [role="menu"]:after { 848 | border-color: transparent; 849 | border-bottom-color: rgba(var(--color-grey-00), 1); 850 | border-width: 6px; 851 | margin-left: -6px; 852 | } 853 | 854 | .active-analytics .menubutton > [role="menu"]:before { 855 | border-color: transparent; 856 | border-bottom-color: rgba(var(--color-grey-50), 1); 857 | border-width: 8px; 858 | margin-left: -8px; 859 | } 860 | 861 | .active-analytics .menubutton > [role="menu"] [role="menuitem"] { 862 | display: block; 863 | padding: 0px 16px; 864 | background: rgba(var(--color-grey-00), 1); 865 | } 866 | 867 | .active-analytics .menubutton > [role="menu"] [role="menuitem"]:hover, 868 | .active-analytics .menubutton > [role="menu"] [role="menuitem"]:focus { 869 | background: rgba(var(--color-grey-50), 1); 870 | color: rgba(var(--color-grey-700), 1); 871 | } 872 | 873 | .active-analytics *[hidden=true] { 874 | display: none; 875 | } -------------------------------------------------------------------------------- /app/views/active_analytics/assets/ariato.js: -------------------------------------------------------------------------------- 1 | Ariato = {} 2 | 3 | Ariato.launchWhenDomIsReady = function(root) { 4 | if (document.readyState != "loading") { 5 | Ariato.launch() 6 | Ariato.launch(document, "aria-roledescription") 7 | Ariato.launch(document, "data-ariato") 8 | } 9 | else 10 | document.addEventListener("DOMContentLoaded", function() { Ariato.launchWhenDomIsReady(root) } ) 11 | } 12 | 13 | Ariato.launch = function(root, attribute, parent) { 14 | attribute || (attribute = "role") 15 | var elements = (root || document).querySelectorAll("[" + attribute + "]") 16 | for (var i = 0; i < elements.length; i++) 17 | Ariato.start(elements[i], attribute, parent) 18 | } 19 | 20 | Ariato.mount = function() { 21 | } 22 | 23 | Ariato.start = function(element, attribute, parent) { 24 | var names = element.getAttribute(attribute).split(" ") 25 | for (var i = 0; i < names.length; i++) { 26 | var name = names[i].charAt(0).toUpperCase() + names[i].slice(1) // Capitalize 27 | var func = Ariato.stringToFunction("Ariato." + name) || Ariato.stringToFunction(name) 28 | if (func instanceof Function) 29 | Ariato.instanciate(func, element, parent) 30 | } 31 | } 32 | 33 | Ariato.instanciate = function(func, element, parent) { 34 | try { 35 | controller = Object.create(func.prototype) 36 | controller.parent = parent 37 | controller.node = element 38 | Ariato.initialize(controller, element) 39 | func.call(controller, element) 40 | } catch (ex) { 41 | console.error(ex) 42 | } 43 | } 44 | 45 | Ariato.stringToFunction = function(fullName) { 46 | var func = window, names = fullName.split(".") 47 | for (var i = 0; i < names.length; i++) 48 | if (!(func = func[names[i]])) 49 | return null 50 | return func 51 | } 52 | 53 | Ariato.initialize = function(controller, container) { 54 | Ariato.listenEvents(container, controller) 55 | Ariato.assignRoles(container, controller) 56 | } 57 | 58 | Ariato.listenEvents = function(root, controller) { 59 | var elements = root.querySelectorAll("[data-event]") 60 | for (var i = 0; i < elements.length; i++) { 61 | elements[i].getAttribute("data-event").split(" ").forEach(function(eventAndAction) { 62 | var array = eventAndAction.split("->") 63 | Ariato.listenEvent(controller, elements[i], array[0], array[1]) 64 | }) 65 | } 66 | } 67 | 68 | Ariato.listenEvent = function(controller, element, event, action) { 69 | if (controller[action] instanceof Function) 70 | element.addEventListener(event, controller[action].bind(controller)) 71 | } 72 | 73 | Ariato.findRoles = function(container) { 74 | var roles = {}, elements = container.querySelectorAll("[data-role]") 75 | for (var i = 0; i < elements.length; i++) { 76 | var name = elements[i].getAttribute("data-role") 77 | roles[name] ? roles[name].push(elements[i]) : roles[name] = [elements[i]] 78 | } 79 | return roles 80 | } 81 | 82 | Ariato.assignRoles = function(container, controller) { 83 | controller.roles = Ariato.findRoles(container) 84 | for (var name in controller.roles) 85 | if (controller.roles[name].length == 1) 86 | controller[name] = controller.roles[name][0] 87 | } 88 | 89 | Ariato.Dialog = function(node) { 90 | node.setAttribute("hidden", true) 91 | node.addEventListener("open", this.open.bind(this)) 92 | node.addEventListener("close", this.close.bind(this)) 93 | node.addEventListener("keydown", this.keydown.bind(this)) 94 | } 95 | 96 | Ariato.Dialog.open = function(elementOrId) { 97 | var dialog = elementOrId instanceof Element ? elementOrId : document.getElementById(elementOrId) 98 | dialog && dialog.dispatchEvent(new CustomEvent("open")) 99 | } 100 | 101 | Ariato.Dialog.close = function(button) { 102 | var dialog = Ariato.Dialog.current() 103 | if (dialog && dialog.node.contains(button)) 104 | dialog.close() 105 | } 106 | 107 | Ariato.Dialog.closeCurrent = function() { 108 | var dialog = Ariato.Dialog.current() 109 | dialog && dialog.close() 110 | } 111 | 112 | Ariato.Dialog.replace = function(elementOrId) { 113 | Ariato.Dialog.closeCurrent() 114 | Ariato.Dialog.open(elementOrId) 115 | } 116 | 117 | Ariato.Dialog.close = function(button) { 118 | var dialog = Ariato.Dialog.current() 119 | if (dialog && dialog.node.contains(button)) 120 | dialog.close() 121 | } 122 | 123 | Ariato.Dialog.list = [] 124 | 125 | Ariato.Dialog.current = function() { 126 | return this.list[this.list.length - 1] 127 | } 128 | 129 | Ariato.Dialog.prototype.open = function(event) { 130 | Ariato.Dialog.list.push(this) 131 | document.addEventListener("focus", this.bindedLimitFocusScope = this.limitFocusScope.bind(this), true) 132 | this.initiator = document.activeElement 133 | this.node.removeAttribute("hidden") 134 | 135 | this.lockScrolling() 136 | this.createBackdrop() 137 | this.createFocusStoppers() 138 | this.focusFirstDescendant(this.node) 139 | } 140 | 141 | Ariato.Dialog.prototype.close = function(event) { 142 | document.removeEventListener("focus", this.bindedLimitFocusScope, true) 143 | this.node.setAttribute("hidden", true) 144 | this.removeFocusStoppers() 145 | this.removeBackdrop() 146 | this.unlockScrolling() 147 | this.initiator.focus() 148 | Ariato.Dialog.list.pop() 149 | } 150 | 151 | Ariato.Dialog.prototype.keydown = function(event) { 152 | if (event.key == "Escape") 153 | this.close() 154 | } 155 | 156 | Ariato.Dialog.prototype.focusFirstDescendant = function(parent) { 157 | var focusable = ["A", "BUTTON", "INPUT", "SELECT", "TEXTAREA"] 158 | 159 | for (var i = 0; i < parent.children.length; i++) { 160 | var child = parent.children[i] 161 | if (focusable.indexOf(child.nodeName) != -1 && !child.disabled && child.type != "hidden") { 162 | child.focus() 163 | return child 164 | } 165 | else { 166 | var focus = this.focusFirstDescendant(child) 167 | if (focus) return focus 168 | } 169 | } 170 | } 171 | 172 | Ariato.Dialog.prototype.limitFocusScope = function(event) { 173 | if (this == Ariato.Dialog.current()) 174 | if (!this.node.contains(event.target)) 175 | this.focusFirstDescendant(this.node) 176 | } 177 | 178 | Ariato.Dialog.prototype.lockScrolling = function() { 179 | document.body.style.position = "fixed"; 180 | document.body.style.top = "-" + window.scrollY + "px"; 181 | } 182 | 183 | Ariato.Dialog.prototype.unlockScrolling = function() { 184 | var scrollY = document.body.style.top 185 | document.body.style.position = "" 186 | document.body.style.top = "" 187 | window.scrollTo(0, parseInt(scrollY || "0") * -1) 188 | } 189 | 190 | Ariato.Dialog.prototype.createFocusStoppers = function() { 191 | this.node.parentNode.insertBefore(this.focusStopper1 = document.createElement("div"), this.node) 192 | this.focusStopper1.tabIndex = 0 193 | 194 | this.node.parentNode.insertBefore(this.focusStopper2 = document.createElement("div"), this.node.nextSibling) 195 | this.focusStopper2.tabIndex = 0 196 | } 197 | 198 | Ariato.Dialog.prototype.removeFocusStoppers = function() { 199 | this.focusStopper1 && this.focusStopper1.parentNode.removeChild(this.focusStopper1) 200 | this.focusStopper2 && this.focusStopper2.parentNode.removeChild(this.focusStopper2) 201 | } 202 | 203 | Ariato.Dialog.prototype.createBackdrop = function() { 204 | this.backdrop = document.createElement("div") 205 | this.backdrop.classList.add("dialog-backdrop") 206 | this.node.parentNode.insertBefore(this.backdrop, this.node) 207 | this.backdrop.appendChild(this.node) 208 | } 209 | 210 | Ariato.Dialog.prototype.removeBackdrop = function() { 211 | this.backdrop.parentNode.insertBefore(this.node, this.backdrop) 212 | this.backdrop.parentNode.removeChild(this.backdrop) 213 | this.backdrop = null 214 | } 215 | 216 | Ariato.Alertdialog = Ariato.Dialog 217 | 218 | Ariato.MenuButton = function(node) { 219 | this.node = this.button = node 220 | this.menu = document.getElementById(this.button.getAttribute("aria-controls")) 221 | 222 | this.menu.addEventListener("keydown", this.keydown.bind(this)) 223 | this.button.addEventListener("keydown", this.keydown.bind(this)) 224 | 225 | this.button.addEventListener("click", this.clicked.bind(this)) 226 | window.addEventListener("click", this.windowClicked.bind(this), true) 227 | } 228 | 229 | Ariato.MenuButton.prototype.clicked = function(event) { 230 | this.node.getAttribute("aria-expanded") == "true" ? this.close() : this.open() 231 | } 232 | 233 | Ariato.MenuButton.prototype.windowClicked = function() { 234 | if (!this.node.contains(event.target) && this.node.getAttribute("aria-expanded") == "true") 235 | this.close() 236 | } 237 | 238 | Ariato.MenuButton.prototype.open = function() { 239 | this.button.setAttribute("aria-expanded", "true") 240 | this.menu.style.display = "block" 241 | } 242 | 243 | Ariato.MenuButton.prototype.close = function() { 244 | this.button.setAttribute("aria-expanded", "false") 245 | this.menu.style.display = null 246 | } 247 | 248 | Ariato.MenuButton.prototype.keydown = function(event) { 249 | switch(event.key) { 250 | case "Escape": 251 | this.close() 252 | break 253 | case "ArrowDown": 254 | event.preventDefault() 255 | this.focusNextItem() 256 | break 257 | case "ArrowUp": 258 | event.preventDefault() 259 | this.focusPreviousItem() 260 | break 261 | case "Tab": 262 | this.close() 263 | case "Home": 264 | case "PageUp": 265 | event.preventDefault() 266 | this.items()[0].focus() 267 | break 268 | case "End": 269 | case "PageDown": 270 | event.preventDefault() 271 | var items = this.items() 272 | items[items.length-1].focus() 273 | break 274 | } 275 | } 276 | 277 | Ariato.MenuButton.prototype.items = function() { 278 | return this.menu.querySelectorAll("[role=menuitem]") 279 | } 280 | 281 | Ariato.MenuButton.prototype.currentItem = function() { 282 | return this.menu.querySelector("[role=menuitem]:focus") 283 | } 284 | 285 | Ariato.MenuButton.prototype.nextItem = function() { 286 | var items = this.items() 287 | var current = this.currentItem() 288 | if (!current) return items[0] 289 | for (var i = 0; i < items.length; i++) { 290 | if (items[i] == current) 291 | return items[i+1] 292 | } 293 | } 294 | 295 | Ariato.MenuButton.prototype.previousItem = function() { 296 | var items = this.items() 297 | var current = this.currentItem() 298 | if (!current) return items[0] 299 | for (var i = 0; i < items.length; i++) { 300 | if (items[i] == current) 301 | return items[i-1] 302 | } 303 | } 304 | 305 | Ariato.MenuButton.prototype.focusNextItem = function() { 306 | var item = this.nextItem() 307 | item && item.focus() 308 | } 309 | 310 | Ariato.MenuButton.prototype.focusPreviousItem = function() { 311 | var item = this.previousItem() 312 | item && item.focus() 313 | } 314 | 315 | Ariato.Menu = function(node) { 316 | var button = this.labelledBy() 317 | button && new Ariato.MenuButton(button) 318 | } 319 | 320 | Ariato.Menu.prototype.labelledBy = function() { 321 | return document.getElementById(this.node.getAttribute("aria-labelledby")) 322 | } -------------------------------------------------------------------------------- /app/views/active_analytics/assets/browsers/arc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/active_analytics/assets/browsers/brave.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/active_analytics/assets/browsers/chrome.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/views/active_analytics/assets/browsers/default.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/views/active_analytics/assets/browsers/firefox.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/views/active_analytics/assets/browsers/internet_explorer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/active_analytics/assets/browsers/microsoft_edge.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/views/active_analytics/assets/browsers/opera.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/views/active_analytics/assets/browsers/safari.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/active_analytics/assets/browsers/samsung_internet.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/active_analytics/assets/browsers/vivaldi.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/active_analytics/assets/browsers/yandex.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/active_analytics/browsers/_table.html.erb: -------------------------------------------------------------------------------- 1 | <% if browsers.empty? %> 2 |
3 | no data 4 |
5 | <% else %> 6 | 7 | <% for browser in browsers %> 8 | 9 | 13 | 14 | 15 | <% end %> 16 |
10 | <%= browser_icon(browser.name)%> 11 | <%= link_to browser.name, browser_path(site: params[:site], id: browser, from: params[:from], to: params[:to]) %> 12 | <%= format_view_count browser.total %>
17 | <% end %> 18 | -------------------------------------------------------------------------------- /app/views/active_analytics/browsers/_version_table.html.erb: -------------------------------------------------------------------------------- 1 | <% if browsers.empty? %> 2 |
3 | no data 4 |
5 | <% else %> 6 | 7 | <% for browser in browsers %> 8 | 9 | 12 | 13 | 14 | <% end %> 15 |
10 | <%= link_to browser.version, browser_path(site: params[:site], id: params[:id], version: browser.version, from: params[:from], to: params[:to]) %> 11 | <%= format_view_count browser.total %>
16 | <% end %> 17 | -------------------------------------------------------------------------------- /app/views/active_analytics/browsers/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render "/active_analytics/sites/histogram_header", histogram: @histogram, previous_histogram: @previous_histogram %> 3 | <%= render "/active_analytics/sites/histogram", histogram: @histogram %> 4 |
5 | 6 |
7 |

Browsers

8 | <%= render "/active_analytics/browsers/table", browsers: @browsers %> 9 |
10 | -------------------------------------------------------------------------------- /app/views/active_analytics/browsers/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | Browser: <%= params[:id] %> 4 |

5 |
6 | 7 |
8 | <%= render "/active_analytics/sites/histogram_header", histogram: @histogram, previous_histogram: @previous_histogram %> 9 | <%= render "/active_analytics/sites/histogram", histogram: @histogram %> 10 |
11 | 12 | 18 | -------------------------------------------------------------------------------- /app/views/active_analytics/pages/_table.html.erb: -------------------------------------------------------------------------------- 1 | <% if pages.empty? %> 2 |
3 | no data 4 |
5 | <% else %> 6 | 7 | <% for page in pages %> 8 | 9 | 18 | 19 | 20 | <% end %> 21 |
10 | <% if page.path.present? %> 11 | <%= link_to page.path, page_path(site: page.host, page: page_to_params(page.path), from: params[:from], to: params[:to]) %> 12 | <% else %> 13 | <%= site_icon page.host %> 14 | <%= link_to page.host, site_path(site: page.host, from: params[:from], to: params[:to]) %> 15 | (page not provided ?) 16 | <% end %> 17 | <%= format_view_count page.total %>
22 | <% end %> 23 | -------------------------------------------------------------------------------- /app/views/active_analytics/pages/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render "/active_analytics/sites/histogram_header", histogram: @histogram, previous_histogram: @previous_histogram %> 3 | <%= render "/active_analytics/sites/histogram", histogram: @histogram %> 4 |
5 | 6 |
7 |

Pages

8 | <%= render "/active_analytics/pages/table", pages: @pages %> 9 |
10 | -------------------------------------------------------------------------------- /app/views/active_analytics/pages/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | <%= page_from_params %> 4 | <%= link_to "", File.join("https://", params[:site], page_from_params), target: "_blank" %> 5 |

6 |
7 | 8 |
9 | <%= render "/active_analytics/sites/histogram_header", histogram: @histogram, previous_histogram: @previous_histogram %> 10 | <%= render "/active_analytics/sites/histogram", histogram: @histogram %> 11 |
12 | 13 | 26 | -------------------------------------------------------------------------------- /app/views/active_analytics/referrers/_table.html.erb: -------------------------------------------------------------------------------- 1 | <% if referrers.empty? %> 2 |
3 | no data 4 |
5 | <% else %> 6 | 7 | <% for referrer in referrers %> 8 | 9 | 24 | 25 | 26 | <% end %> 27 |
10 | <% if referrer.try(:path) %> 11 | <%= site_icon referrer.host %> 12 | <% if referrer.host == params[:site] %> 13 | <%= link_to referrer.path, page_path(site: referrer.host, page: page_to_params(referrer.path), from: params[:from], to: params[:to]) %> 14 | <% else %> 15 | <%= link_to referrer.url, referrer_path(site: params[:site], referrer: referrer.url.chomp("/"), from: params[:from], to: params[:to]) %> 16 | <% end %> 17 | <% elsif referrer.host %> 18 | <%= site_icon referrer.host %> 19 | <%= link_to referrer.host, referrer_path(site: params[:site], referrer: referrer.host, from: params[:from], to: params[:to]) %> 20 | <% else %> 21 | (None or direct) 22 | <% end %> 23 | <%= format_view_count referrer.total %>
28 | <% end %> 29 | -------------------------------------------------------------------------------- /app/views/active_analytics/referrers/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render "/active_analytics/sites/histogram_header", histogram: @histogram, previous_histogram: @previous_histogram %> 3 | <%= render "/active_analytics/sites/histogram", histogram: @histogram %> 4 |
5 | 6 |
7 |

Sources

8 | <%= render "table", referrers: @referrers %> 9 |
10 | -------------------------------------------------------------------------------- /app/views/active_analytics/referrers/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | Source: <%= params[:referrer] %> 4 | <%= link_to "", File.join("https://", params[:referrer]), target: "_blank" %> 5 |

6 |
7 | 8 |
9 | <%= render "/active_analytics/sites/histogram_header", histogram: @histogram, previous_histogram: @previous_histogram %> 10 | <%= render "/active_analytics/sites/histogram", histogram: @histogram %> 11 |
12 | 23 | -------------------------------------------------------------------------------- /app/views/active_analytics/sites/_histogram.html.erb: -------------------------------------------------------------------------------- 1 | <% if histogram.bars.count.between?(2, 367) %> 2 | " style="height: calc(var(--space-2x) * 8);"> 3 | 4 | <% for bar in histogram.bars %> 5 | 6 | 13 | 17 | 18 | 19 | <% end %> 20 | 21 |
7 | <% if bar.height > 0 %> 8 | <%= link_to bar.label.day, url_for(params.merge(from: bar.label.to_date, to: bar.label.to_date).to_unsafe_hash) %> 9 | <% else %> 10 | <%= bar.label.day %> 11 | <% end %> 12 | 14 | <%= bar.value.to_i %> 15 | <%= l bar.label, format: :long%>
<%= format_view_count bar.value %> views
16 |
22 | <% end %> 23 | -------------------------------------------------------------------------------- /app/views/active_analytics/sites/_histogram_header.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <%= format_view_count current_total = histogram.total %> views 3 | <% if (previous_total = previous_histogram.total) > 0 %> 4 | <% if (diff = current_total - previous_total) > 0 %> 5 |  +<%= number_to_percentage diff * 100.0 / previous_total, precision: 0 %> 6 | <% elsif (diff = current_total - previous_total) < 0 %> 7 |  <%= number_to_percentage diff * 100.0 / previous_total, precision: 0 %> 8 | <% end %> 9 | <% end %> 10 |

11 | -------------------------------------------------------------------------------- /app/views/active_analytics/sites/index.html.erb: -------------------------------------------------------------------------------- 1 |

Site views in the last 30 days

2 |
3 | 4 | 5 | <% for site in @sites %> 6 | 7 | 8 | 9 | 10 | <% end %> 11 |
<%= link_to site.host, site_path(site: site.host) %><%= format_view_count site.total %>
12 |
-------------------------------------------------------------------------------- /app/views/active_analytics/sites/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render "/active_analytics/sites/histogram_header", histogram: @histogram, previous_histogram: @previous_histogram %> 3 | <%= render "/active_analytics/sites/histogram", histogram: @histogram %> 4 |
5 | 6 | 22 | -------------------------------------------------------------------------------- /app/views/layouts/active_analytics/_footer.html.erb: -------------------------------------------------------------------------------- 1 | 35 | 36 | 49 | -------------------------------------------------------------------------------- /app/views/layouts/active_analytics/_header.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 49 |
-------------------------------------------------------------------------------- /app/views/layouts/active_analytics/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Active analytics 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= tag.link rel: "stylesheet", href: asset_path(:ariato, format: :css) %> 10 | <%= tag.link rel: "stylesheet", href: asset_path(:application, format: :css) %> 11 | <%= tag.script "", src: asset_path(:ariato, format: :js) %> 12 | <%= tag.script "", src: asset_path(:application, format: :js) %> 13 | 14 | 15 | 16 | <%= render "/layouts/active_analytics/header" %> 17 |
18 | <%= yield %> 19 |
20 | <%= render "/layouts/active_analytics/footer" %> 21 | 22 | 23 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path('..', __dir__) 6 | ENGINE_PATH = File.expand_path('../lib/active_analytics/engine', __dir__) 7 | APP_PATH = File.expand_path('../test/dummy/config/application', __dir__) 8 | 9 | # Set up gems listed in the Gemfile. 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 11 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) 12 | 13 | require "rails/all" 14 | require "rails/engine/commands" 15 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | ActiveAnalytics::Engine.routes.draw do 2 | get "/assets/*file", to: "assets#show", constraints: {file: /.+/}, as: :asset 3 | 4 | get "/:site", to: "sites#show", as: :site, constraints: {site: /[^\/]+/} 5 | 6 | # Referrers 7 | get "/:site/referrers", to: "referrers#index", constraints: {site: /[^\/]+/}, as: :referrers 8 | get "/:site/referrers/*referrer", to: "referrers#show", as: :referrer, constraints: {site: /[^\/]+/, referrer: /.+/} 9 | 10 | # Browsers 11 | get "/:site/browsers", to: "browsers#index", constraints: {site: /[^\/]+/}, as: :browsers 12 | get "/:site/browsers/:id", to: "browsers#show", constraints: {site: /[^\/]+/}, as: :browser 13 | 14 | # Pages 15 | get "/:site/pages", to: "pages#index", constraints: {site: /[^\/]+/}, as: :pages 16 | get "/:site/*page", to: "pages#show", as: :page, constraints: {site: /[^\/]+/} 17 | 18 | root to: "sites#index", as: :active_analytics 19 | end 20 | -------------------------------------------------------------------------------- /db/migrate/20210303094108_create_active_analytics_views_per_days.rb: -------------------------------------------------------------------------------- 1 | class CreateActiveAnalyticsViewsPerDays < ActiveRecord::Migration[5.2] 2 | def up 3 | create_table :active_analytics_views_per_days do |t| 4 | t.string :site, null: false 5 | t.string :page, null: false 6 | t.date :date, null: false 7 | t.bigint :total, null: false, default: 1 8 | t.string :referrer_host 9 | t.string :referrer_path 10 | t.timestamps 11 | end 12 | add_index :active_analytics_views_per_days, [:date, :site, :page] 13 | add_index :active_analytics_views_per_days, [:date, :site, :referrer_host, :referrer_path], 14 | name: 'index_views_per_days_on_date_site_referrer_host_referrer_path' 15 | end 16 | 17 | def down 18 | drop_table :active_analytics_views_per_days 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /db/migrate/20240823150626_create_active_analytics_browsers_per_days.rb: -------------------------------------------------------------------------------- 1 | class CreateActiveAnalyticsBrowsersPerDays < ActiveRecord::Migration[7.1] 2 | def change 3 | create_table :active_analytics_browsers_per_days do |t| 4 | t.string :site, null: false 5 | t.string :name, null: false 6 | t.string :version, null: false 7 | t.date :date, null: false 8 | t.bigint :total, null: false, default: 1 9 | t.timestamps 10 | end 11 | add_index :active_analytics_browsers_per_days, [:date, :site, :name, :version] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/active_analytics.rb: -------------------------------------------------------------------------------- 1 | require "active_analytics/version" 2 | require "active_analytics/engine" 3 | require "browser" 4 | 5 | module ActiveAnalytics 6 | mattr_accessor :base_controller_class, default: "ActionController::Base" 7 | 8 | def self.redis_url=(string) 9 | @redis_url = string 10 | end 11 | 12 | def self.redis_url 13 | @redis_url ||= ENV["ACTIVE_ANALYTICS_REDIS_URL"] || ENV["REDIS_URL"] || "redis://localhost" 14 | end 15 | 16 | def self.redis=(connection) 17 | @redis = connection 18 | end 19 | 20 | def self.redis 21 | @redis ||= Redis.new(url: redis_url) 22 | end 23 | 24 | def self.record_request(request) 25 | params = { 26 | site: request.host, 27 | page: request.path, 28 | date: Date.today, 29 | } 30 | if request.referrer.present? 31 | params[:referrer_host], params[:referrer_path] = ViewsPerDay.split_referrer(request.referrer) 32 | end 33 | ViewsPerDay.append(params) 34 | 35 | browser = Browser.new(request.headers["User-Agent"]) 36 | BrowsersPerDay.append(site: request.host, date: Date.today, name: browser.name, version: browser.version) 37 | rescue => ex 38 | if Rails.env.development? || Rails.env.test? 39 | raise ex 40 | else 41 | Rails.logger.error(ex.inspect) 42 | Rails.logger.error(ex.backtrace.join("\n")) 43 | end 44 | end 45 | 46 | SEPARATOR = "|" 47 | 48 | PAGE_QUEUE = "ActiveAnalytics::PageQueue" 49 | BROWSER_QUEUE = "ActiveAnalytics::BrowserQueue" 50 | 51 | OLD_PAGE_QUEUE = "ActiveAnalytics::OldPageQueue" 52 | OLD_BROWSER_QUEUE = "ActiveAnalytics::BrowserQueue" 53 | 54 | def self.queue_request(request) 55 | queue_request_page(request) 56 | queue_request_browser(request) 57 | end 58 | 59 | def self.queue_request_page(request) 60 | keys = [request.host, request.path] 61 | if request.referrer.present? 62 | keys.concat(ViewsPerDay.split_referrer(request.referrer)) 63 | end 64 | redis.hincrby(PAGE_QUEUE, keys.join(SEPARATOR).downcase, 1) 65 | end 66 | 67 | def self.queue_request_browser(request) 68 | browser = Browser.new(request.headers["User-Agent"]) 69 | keys = [request.host.downcase, browser.name, browser.version] 70 | redis.hincrby(BROWSER_QUEUE, keys.join(SEPARATOR), 1) 71 | end 72 | 73 | def self.flush_queue 74 | flush_page_queue 75 | flush_browser_queue 76 | end 77 | 78 | def self.flush_page_queue 79 | return if !redis.exists?(PAGE_QUEUE) 80 | date = Date.today 81 | redis.rename(PAGE_QUEUE, OLD_PAGE_QUEUE) 82 | redis.hscan_each(OLD_PAGE_QUEUE) do |key, count| 83 | site, page, referrer_host, referrer_path = key.split(SEPARATOR) 84 | ViewsPerDay.append(date: date, site: site, page: page, referrer_host: referrer_host, referrer_path: referrer_path, total: count.to_i) 85 | end 86 | redis.del(OLD_PAGE_QUEUE) 87 | end 88 | 89 | def self.flush_browser_queue 90 | return if !redis.exists?(BROWSER_QUEUE) 91 | date = Date.today 92 | redis.rename(BROWSER_QUEUE, OLD_BROWSER_QUEUE) 93 | redis.hscan_each(OLD_BROWSER_QUEUE) do |key, count| 94 | site, name, version = key.split(SEPARATOR) 95 | BrowsersPerDay.append(date: date, site: site, name: name, version: version, total: count.to_i) 96 | end 97 | redis.del(OLD_BROWSER_QUEUE) 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/active_analytics/engine.rb: -------------------------------------------------------------------------------- 1 | module ActiveAnalytics 2 | class Engine < ::Rails::Engine 3 | isolate_namespace ActiveAnalytics 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/active_analytics/version.rb: -------------------------------------------------------------------------------- 1 | module ActiveAnalytics 2 | VERSION = '0.4.1' 3 | end 4 | -------------------------------------------------------------------------------- /lib/tasks/active_analytics_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :active_analytics do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /test/active_analytics_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "redis" 3 | 4 | class ActiveAnalyticsTest < ActiveSupport::TestCase 5 | Request = Struct.new(:host, :path, :referrer, :headers) 6 | 7 | def test_record_request_with_referrer 8 | req1, req2, req3, req4 = sample_requests 9 | 10 | assert_difference("ActiveAnalytics::ViewsPerDay.count") { ActiveAnalytics.record_request(req1) } 11 | assert_no_difference("ActiveAnalytics::ViewsPerDay.count") { ActiveAnalytics.record_request(req2) } 12 | assert_equal(2, ActiveAnalytics::ViewsPerDay.last.total) 13 | assert_difference("ActiveAnalytics::ViewsPerDay.count") { ActiveAnalytics.record_request(req3) } 14 | assert_difference("ActiveAnalytics::ViewsPerDay.count") { ActiveAnalytics.record_request(req4) } 15 | end 16 | 17 | def test_record_request_without_referrer 18 | req1 = Request.new("SITE.TEST", "PAGE", "", {}) 19 | req2 = Request.new("SITE.TEST", "PAGE", nil, {}) 20 | assert_difference("ActiveAnalytics::ViewsPerDay.count") { ActiveAnalytics.record_request(req1) } 21 | assert_no_difference("ActiveAnalytics::ViewsPerDay.count") { ActiveAnalytics.record_request(req2) } 22 | assert_equal(2, ActiveAnalytics::ViewsPerDay.last.total) 23 | end 24 | 25 | def test_record_request_with_user_agent 26 | req1, req2 = sample_requests 27 | assert_difference("ActiveAnalytics::BrowsersPerDay.count") { ActiveAnalytics.record_request(req1) } 28 | assert_no_difference("ActiveAnalytics::BrowsersPerDay.count") { ActiveAnalytics.record_request(req2) } 29 | assert_equal(2, ActiveAnalytics::BrowsersPerDay.last.total) 30 | end 31 | 32 | def test_queue_request_and_flush_queue 33 | req1, req2, req3, req4 = sample_requests 34 | ActiveAnalytics.redis.del(ActiveAnalytics::PAGE_QUEUE) 35 | ActiveAnalytics.redis.del(ActiveAnalytics::OLD_PAGE_QUEUE) 36 | ActiveAnalytics.redis.del(ActiveAnalytics::BROWSER_QUEUE) 37 | ActiveAnalytics.redis.del(ActiveAnalytics::OLD_BROWSER_QUEUE) 38 | 39 | ActiveAnalytics.queue_request(req1) 40 | ActiveAnalytics.queue_request(req2) 41 | ActiveAnalytics.queue_request(req3) 42 | ActiveAnalytics.queue_request(req4) 43 | 44 | assert_equal(3, ActiveAnalytics.redis.hlen("ActiveAnalytics::PageQueue"), ActiveAnalytics.redis.hgetall("ActiveAnalytics::PageQueue")) 45 | assert_equal(1, ActiveAnalytics.redis.hlen("ActiveAnalytics::BrowserQueue"), ActiveAnalytics.redis.hgetall("ActiveAnalytics::BrowserQueue")) 46 | 47 | assert_difference("ActiveAnalytics::BrowsersPerDay.count", 1) do 48 | assert_difference("ActiveAnalytics::ViewsPerDay.count", 3) do 49 | ActiveAnalytics.flush_queue 50 | end 51 | end 52 | 53 | assert_equal(2, ActiveAnalytics::ViewsPerDay.where(site: "site.test", page: "page", referrer_host: "site.test", referrer_path: "/previous_page").first.total) 54 | assert_equal(4, ActiveAnalytics::BrowsersPerDay.where(site: "site.test", name: "Firefox", version: "128").first.total) 55 | 56 | assert_equal(0, ActiveAnalytics.redis.exists("ActiveAnalytics::PageQueue")) 57 | assert_equal(0, ActiveAnalytics.redis.exists("ActiveAnalytics::OldPageQueue")) 58 | 59 | assert_equal(0, ActiveAnalytics.redis.exists("ActiveAnalytics::BrowserQueue")) 60 | assert_equal(0, ActiveAnalytics.redis.exists("ActiveAnalytics::OldBrowserQueue")) 61 | 62 | assert_no_difference("ActiveAnalytics::ViewsPerDay.sum(:total)") do 63 | assert_no_difference("ActiveAnalytics::ViewsPerDay.sum(:total)") do 64 | ActiveAnalytics.flush_queue 65 | end 66 | end 67 | 68 | ActiveAnalytics.queue_request(req1) 69 | assert_difference("ActiveAnalytics::BrowsersPerDay.sum(:total)", 1) do 70 | assert_difference("ActiveAnalytics::ViewsPerDay.sum(:total)", 1) do 71 | assert_no_difference("ActiveAnalytics::BrowsersPerDay.count") do 72 | assert_no_difference("ActiveAnalytics::ViewsPerDay.count") do 73 | ActiveAnalytics.flush_queue 74 | end 75 | end 76 | end 77 | end 78 | assert_equal(3, ActiveAnalytics::ViewsPerDay.where(site: "site.test", page: "page", referrer_host: "site.test", referrer_path: "/previous_page").first.total) 79 | end 80 | 81 | private 82 | 83 | def sample_requests 84 | [ 85 | Request.new("site.test", "page", "http://site.test/previous_page", sample_headers), 86 | Request.new("SITE.TEST", "page", "http://SITE.TEST/previous_page", sample_headers), 87 | Request.new("site.test", "page", "http://site.test/", sample_headers), 88 | Request.new("site.test", "page", "http://site.test", sample_headers), 89 | ] 90 | end 91 | 92 | def sample_headers 93 | {"User-Agent" => "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"} 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/controllers/active_analytics/browsers_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module ActiveAnalytics 4 | class BrowsersControllerTest < ActionDispatch::IntegrationTest 5 | include Engine.routes.url_helpers 6 | 7 | # test "the truth" do 8 | # assert true 9 | # end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/controllers/active_analytics/pages_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module ActiveAnalytics 4 | class PagesControllerTest < ActionDispatch::IntegrationTest 5 | include Engine.routes.url_helpers 6 | 7 | # test "the truth" do 8 | # assert true 9 | # end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/controllers/active_analytics/referrers_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module ActiveAnalytics 4 | class ReferrersControllerTest < ActionDispatch::IntegrationTest 5 | include Engine.routes.url_helpers 6 | 7 | # test "the truth" do 8 | # assert true 9 | # end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/controllers/active_analytics/sites_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module ActiveAnalytics 4 | class SitesControllerTest < ActionDispatch::IntegrationTest 5 | include Engine.routes.url_helpers 6 | 7 | # test "the truth" do 8 | # assert true 9 | # end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | //= link active_analytics_manifest.js 4 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/test/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /test/dummy/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/test/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/packs/application.js: -------------------------------------------------------------------------------- 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 rails-ujs 14 | //= require activestorage 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /test/dummy/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/test/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= stylesheet_link_tag 'application', media: 'all' %> 10 | 11 | 12 | 13 | <%= yield %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /test/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /test/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /test/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?('config/database.yml') 22 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! 'bin/rails db:prepare' 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! 'bin/rails log:clear tmp:clear' 30 | 31 | puts "\n== Restarting application server ==" 32 | system! 'bin/rails restart' 33 | end 34 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | require "active_analytics" 9 | 10 | module Dummy 11 | class Application < Rails::Application 12 | config.load_defaults Rails::VERSION::STRING.to_f 13 | 14 | # Configuration for the application, engines, and railties goes here. 15 | # 16 | # These settings can be overridden in specific environments using the files 17 | # in config/environments, which are processed later. 18 | # 19 | # config.time_zone = "Central Time (US & Canada)" 20 | # config.eager_load_paths << Rails.root.join("extras") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) 3 | 4 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) 5 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) 6 | -------------------------------------------------------------------------------- /test/dummy/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: dummy_production 11 | -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable/disable caching. By default caching is disabled. 18 | # Run rails dev:cache to toggle caching. 19 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 20 | config.action_controller.perform_caching = true 21 | config.action_controller.enable_fragment_cache_logging = true 22 | 23 | config.cache_store = :memory_store 24 | config.public_file_server.headers = { 25 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 26 | } 27 | else 28 | config.action_controller.perform_caching = false 29 | 30 | config.cache_store = :null_store 31 | end 32 | 33 | # Store uploaded files on the local file system (see config/storage.yml for options). 34 | config.active_storage.service = :local 35 | 36 | # Don't care if the mailer can't send. 37 | config.action_mailer.raise_delivery_errors = false 38 | 39 | config.action_mailer.perform_caching = false 40 | 41 | # Print deprecation notices to the Rails logger. 42 | config.active_support.deprecation = :log 43 | 44 | # Raise exceptions for disallowed deprecations. 45 | config.active_support.disallowed_deprecation = :raise 46 | 47 | # Tell Active Support which deprecation messages to disallow. 48 | config.active_support.disallowed_deprecation_warnings = [] 49 | 50 | # Raise an error on page load if there are pending migrations. 51 | config.active_record.migration_error = :page_load 52 | 53 | # Highlight code that triggered database queries in logs. 54 | config.active_record.verbose_query_logs = true 55 | 56 | # Debug mode disables concatenation and preprocessing of assets. 57 | # This option may cause significant delays in view rendering with a large 58 | # number of complex assets. 59 | # config.assets.debug = true 60 | 61 | # Suppress logger output for asset requests. 62 | # config.assets.quiet = true 63 | 64 | # Raises error for missing translations. 65 | # config.i18n.raise_on_missing_translations = true 66 | 67 | # Annotate rendered view with file names. 68 | # config.action_view.annotate_rendered_view_with_filenames = true 69 | 70 | # Use an evented file watcher to asynchronously detect changes in source code, 71 | # routes, locales, etc. This feature depends on the listen gem. 72 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker 73 | 74 | # Uncomment if you wish to allow Action Cable access from any origin. 75 | # config.action_cable.disable_request_forgery_protection = true 76 | end 77 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | # config.require_master_key = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress CSS using a preprocessor. 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 34 | # config.asset_host = 'http://assets.example.com' 35 | 36 | # Specifies the header that your server uses for sending files. 37 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 38 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 39 | 40 | # Store uploaded files on the local file system (see config/storage.yml for options). 41 | config.active_storage.service = :local 42 | 43 | # Mount Action Cable outside main process or domain. 44 | # config.action_cable.mount_path = nil 45 | # config.action_cable.url = 'wss://example.com/cable' 46 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 47 | 48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 49 | # config.force_ssl = true 50 | 51 | # Include generic and useful information about system operation, but avoid logging too much 52 | # information to avoid inadvertent exposure of personally identifiable information (PII). 53 | config.log_level = :info 54 | 55 | # Prepend all log lines with the following tags. 56 | config.log_tags = [ :request_id ] 57 | 58 | # Use a different cache store in production. 59 | # config.cache_store = :mem_cache_store 60 | 61 | # Use a real queuing backend for Active Job (and separate queues per environment). 62 | # config.active_job.queue_adapter = :resque 63 | # config.active_job.queue_name_prefix = "dummy_production" 64 | 65 | config.action_mailer.perform_caching = false 66 | 67 | # Ignore bad email addresses and do not raise email delivery errors. 68 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 69 | # config.action_mailer.raise_delivery_errors = false 70 | 71 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 72 | # the I18n.default_locale when a translation cannot be found). 73 | config.i18n.fallbacks = true 74 | 75 | # Send deprecation notices to registered listeners. 76 | config.active_support.deprecation = :notify 77 | 78 | # Log disallowed deprecations. 79 | config.active_support.disallowed_deprecation = :log 80 | 81 | # Tell Active Support which deprecation messages to disallow. 82 | config.active_support.disallowed_deprecation_warnings = [] 83 | 84 | # Use default logging formatter so that PID and timestamp are not suppressed. 85 | config.log_formatter = ::Logger::Formatter.new 86 | 87 | # Use a different logger for distributed setups. 88 | # require "syslog/logger" 89 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 90 | 91 | if ENV["RAILS_LOG_TO_STDOUT"].present? 92 | logger = ActiveSupport::Logger.new(STDOUT) 93 | logger.formatter = config.log_formatter 94 | config.logger = ActiveSupport::TaggedLogging.new(logger) 95 | end 96 | 97 | # Do not dump schema after migrations. 98 | config.active_record.dump_schema_after_migration = false 99 | 100 | # Inserts middleware to perform automatic connection switching. 101 | # The `database_selector` hash is used to pass options to the DatabaseSelector 102 | # middleware. The `delay` is used to determine how long to wait after a write 103 | # to send a subsequent read to the primary. 104 | # 105 | # The `database_resolver` class is used by the middleware to determine which 106 | # database is appropriate to use based on the time delay. 107 | # 108 | # The `database_resolver_context` class is used by the middleware to set 109 | # timestamps for the last write to the primary. The resolver uses the context 110 | # class timestamps to determine how long to wait before reading from the 111 | # replica. 112 | # 113 | # By default Rails will store a last write timestamp in the session. The 114 | # DatabaseSelector middleware is designed as such you can define your own 115 | # strategy for connection switching and pass that into the middleware through 116 | # these configuration options. 117 | # config.active_record.database_selector = { delay: 2.seconds } 118 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver 119 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session 120 | end 121 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | config.cache_classes = true 12 | 13 | # Do not eager load code on boot. This avoids loading your whole application 14 | # just for the purpose of running a single test. If you are using a tool that 15 | # preloads Rails for running tests, you may have to set it to true. 16 | config.eager_load = false 17 | 18 | # Configure public file server for tests with Cache-Control for performance. 19 | config.public_file_server.enabled = true 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 22 | } 23 | 24 | # Show full error reports and disable caching. 25 | config.consider_all_requests_local = true 26 | config.action_controller.perform_caching = false 27 | config.cache_store = :null_store 28 | 29 | # Raise exceptions instead of rendering exception templates. 30 | config.action_dispatch.show_exceptions = false 31 | 32 | # Disable request forgery protection in test environment. 33 | config.action_controller.allow_forgery_protection = false 34 | 35 | # Store uploaded files on the local file system in a temporary directory. 36 | config.active_storage.service = :test 37 | 38 | config.action_mailer.perform_caching = false 39 | 40 | # Tell Action Mailer not to deliver emails to the real world. 41 | # The :test delivery method accumulates sent emails in the 42 | # ActionMailer::Base.deliveries array. 43 | config.action_mailer.delivery_method = :test 44 | 45 | # Print deprecation notices to the stderr. 46 | config.active_support.deprecation = :stderr 47 | 48 | # Raise exceptions for disallowed deprecations. 49 | config.active_support.disallowed_deprecation = :raise 50 | 51 | # Tell Active Support which deprecation messages to disallow. 52 | config.active_support.disallowed_deprecation_warnings = [] 53 | 54 | # Raises error for missing translations. 55 | # config.i18n.raise_on_missing_translations = true 56 | 57 | # Annotate rendered view with file names. 58 | # config.action_view.annotate_rendered_view_with_filenames = true 59 | end 60 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | # Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 13 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] 9 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | 19 | # If you are using UJS then enable automatic nonce generation 20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 21 | 22 | # Set the nonce only to specific directives 23 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 24 | 25 | # Report CSP violations to a specified URI 26 | # For further information see the following documentation: 27 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 28 | # Rails.application.config.content_security_policy_report_only = true 29 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [ 5 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 6 | ] 7 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /test/dummy/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 12 | # terminating a worker in development environments. 13 | # 14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 15 | 16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 17 | # 18 | port ENV.fetch("PORT") { 3000 } 19 | 20 | # Specifies the `environment` that Puma will run in. 21 | # 22 | environment ENV.fetch("RAILS_ENV") { "development" } 23 | 24 | # Specifies the `pidfile` that Puma will use. 25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 26 | 27 | # Specifies the number of `workers` to boot in clustered mode. 28 | # Workers are forked web server processes. If using threads and workers together 29 | # the concurrency of the application would be max `threads` * `workers`. 30 | # Workers do not work on JRuby or Windows (both of which do not support 31 | # processes). 32 | # 33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 34 | 35 | # Use the `preload_app!` method when specifying a `workers` number. 36 | # This directive tells Puma to first boot the application and load code 37 | # before forking the application. This takes advantage of Copy On Write 38 | # process behavior so workers use less memory. 39 | # 40 | # preload_app! 41 | 42 | # Allow puma to be restarted by `rails restart` command. 43 | plugin :tmp_restart 44 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | mount ActiveAnalytics::Engine => "/analytics" 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /test/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[8.0].define(version: 2024_08_23_150626) do 14 | create_table "active_analytics_browsers_per_days", force: :cascade do |t| 15 | t.string "site", null: false 16 | t.string "name", null: false 17 | t.string "version", null: false 18 | t.date "date", null: false 19 | t.bigint "total", default: 1, null: false 20 | t.datetime "created_at", null: false 21 | t.datetime "updated_at", null: false 22 | end 23 | 24 | create_table "active_analytics_views_per_days", force: :cascade do |t| 25 | t.string "site", null: false 26 | t.string "page", null: false 27 | t.date "date", null: false 28 | t.bigint "total", default: 1, null: false 29 | t.string "referrer_host" 30 | t.string "referrer_path" 31 | t.datetime "created_at", precision: nil, null: false 32 | t.datetime "updated_at", precision: nil, null: false 33 | t.index ["date", "site", "page"], name: "index_active_analytics_views_per_days_on_date_and_site_and_page" 34 | t.index ["date", "site", "referrer_host", "referrer_path"], name: "index_views_per_days_on_date_site_referrer_host_referrer_path" 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/test/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /test/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/test/dummy/log/.keep -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /test/dummy/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/test/dummy/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /test/dummy/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/test/dummy/public/apple-touch-icon.png -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/test/dummy/public/favicon.ico -------------------------------------------------------------------------------- /test/fixtures/active_analytics/browsers_per_days.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/test/fixtures/active_analytics/browsers_per_days.yml -------------------------------------------------------------------------------- /test/fixtures/active_analytics/views_per_days.yml: -------------------------------------------------------------------------------- 1 | rorvswild: 2 | site: rorvswild.com 3 | page: / 4 | date: <%= Date.today %> 5 | total: 1 6 | -------------------------------------------------------------------------------- /test/integration/navigation_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class NavigationTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/active_analytics/browsers_per_day_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module ActiveAnalytics 4 | class BrowsersPerDayTest < ActiveSupport::TestCase 5 | def test_append 6 | assert_difference("BrowsersPerDay.count") do 7 | BrowsersPerDay.append(site: "site.example", date: Date.today, name: "Firefox", version: 123) 8 | end 9 | 10 | assert_equal("Firefox", (browser = BrowsersPerDay.last).name) 11 | assert_equal("123", browser.version) 12 | 13 | assert_no_difference("BrowsersPerDay.count") do 14 | browser = BrowsersPerDay.append(site: "SITE.EXAMPLE", date: Date.today, name: "Firefox", version: 123) 15 | assert_equal(2, browser.total) 16 | end 17 | 18 | assert_difference("BrowsersPerDay.count") do 19 | BrowsersPerDay.append(site: "site.example", date: Date.today, name: "Firefox", version: 456) 20 | end 21 | 22 | assert_equal("Firefox", (browser = BrowsersPerDay.last).name) 23 | assert_equal("456", browser.version) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/models/active_analytics/views_per_day_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module ActiveAnalytics 4 | class ViewsPerDayTest < ActiveSupport::TestCase 5 | def test_histogram 6 | scope = ViewsPerDay.where(site: "rorvswild.com").order_by_date.group_by_date 7 | histogram = Histogram.new(scope, Date.yesterday, Date.tomorrow) 8 | 9 | assert_equal(3, histogram.bars.size) 10 | 11 | assert_equal(Date.yesterday, histogram.bars[0].label) 12 | assert_equal(0, histogram.bars[0].value) 13 | 14 | assert_equal(Date.today, histogram.bars[1].label) 15 | assert_equal(1, histogram.bars[1].value) 16 | 17 | assert_equal(Date.tomorrow, histogram.bars[2].label) 18 | assert_equal(0, histogram.bars[2].value) 19 | end 20 | 21 | def test_split_referrer 22 | assert_equal([nil, nil], ViewsPerDay.split_referrer(nil)) 23 | assert_equal([nil, nil], ViewsPerDay.split_referrer("")) 24 | assert_equal(["domain.test", nil], ViewsPerDay.split_referrer("domain.test")) 25 | assert_equal(["domain.test", "/1"], ViewsPerDay.split_referrer("domain.test/1")) 26 | assert_equal(["domain.test", "/1/2"], ViewsPerDay.split_referrer("domain.test/1/2")) 27 | assert_equal(["domain.test", nil], ViewsPerDay.split_referrer("https://domain.test")) 28 | assert_equal(["domain.test", "/"], ViewsPerDay.split_referrer("https://domain.test/")) 29 | assert_equal(["domain.test", "/1"], ViewsPerDay.split_referrer("https://domain.test/1")) 30 | assert_equal(["domain.test", "/1/2"], ViewsPerDay.split_referrer("https://domain.test/1/2")) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require_relative "../test/dummy/config/environment" 5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] 6 | ActiveRecord::Migrator.migrations_paths << File.expand_path('../db/migrate', __dir__) 7 | require "rails/test_help" 8 | 9 | ActiveSupport::TestCase.fixture_paths << File.expand_path("fixtures", __dir__) 10 | ActionDispatch::IntegrationTest.fixture_paths << File.expand_path("fixtures", __dir__) 11 | ActiveSupport::TestCase.fixture_paths << File.expand_path("fixtures/files", __dir__) 12 | ActiveSupport::TestCase.fixtures :all 13 | --------------------------------------------------------------------------------