├── log └── .keep ├── storage └── .keep ├── tmp ├── .keep └── pids │ └── .keep ├── vendor └── .keep ├── lib ├── assets │ └── .keep └── tasks │ ├── .keep │ └── auto_annotate_models.rake ├── public ├── favicon.ico ├── apple-touch-icon.png ├── apple-touch-icon-precomposed.png ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── test ├── helpers │ └── .keep ├── mailers │ ├── .keep │ ├── verification_mailer_test.rb │ └── previews │ │ └── verification_mailer_preview.rb ├── models │ ├── .keep │ ├── hello_test.rb │ ├── site_member_test.rb │ ├── measurement_protocol_test.rb │ ├── shared_link_test.rb │ ├── goal_test.rb │ ├── user_test.rb │ ├── site_test.rb │ └── event_test.rb ├── system │ └── .keep ├── controllers │ ├── .keep │ ├── oses_controller_test.rb │ ├── sites_controller_test.rb │ ├── browsers_controller_test.rb │ ├── countries_controller_test.rb │ ├── sessions_controller_test.rb │ ├── settings_controller_test.rb │ ├── device_types_controller_test.rb │ ├── location_urls_controller_test.rb │ ├── registrations_controller_test.rb │ ├── site_connections_controller_test.rb │ ├── traffic_campaigns_controller_test.rb │ ├── traffic_mediums_controller_test.rb │ ├── traffic_sources_controller_test.rb │ └── measurement_protocols_controller_test.rb ├── fixtures │ ├── .keep │ ├── files │ │ └── .keep │ ├── hellos.yml │ ├── site_members.yml │ ├── measurement_protocols.yml │ ├── shared_links.yml │ ├── goals.yml │ ├── users.yml │ ├── sites.yml │ └── events.yml ├── integration │ └── .keep ├── jobs │ └── hello_job_test.rb ├── application_system_test_case.rb ├── channels │ └── application_cable │ │ └── connection_test.rb └── test_helper.rb ├── app ├── assets │ ├── images │ │ └── .keep │ ├── javascripts │ │ ├── channels │ │ │ └── .keep │ │ ├── cable.js │ │ └── application.js │ ├── config │ │ └── manifest.js │ └── stylesheets │ │ └── application.css ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── application_hyper_record.rb │ ├── site_member.rb │ ├── shared_link.rb │ ├── growth_rate.rb │ ├── goal.rb │ ├── measurement_protocol.rb │ ├── user.rb │ ├── site_connection.rb │ ├── buffer_queue.rb │ └── site.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── measurement_protocols_controller.rb │ ├── site_connections_controller.rb │ ├── oses_controller.rb │ ├── browsers_controller.rb │ ├── countries_controller.rb │ ├── settings_controller.rb │ ├── device_types_controller.rb │ ├── traffic_mediums_controller.rb │ ├── traffic_sources_controller.rb │ ├── traffic_campaigns_controller.rb │ ├── location_urls_controller.rb │ ├── referrer_sources_controller.rb │ ├── registrations_controller.rb │ ├── sessions_controller.rb │ └── application_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ ├── _flash.html.erb │ │ ├── application.html.erb │ │ ├── flash │ │ │ ├── _success.html.erb │ │ │ ├── _info.html.erb │ │ │ └── _error.html.erb │ │ ├── auth.html.erb │ │ └── detail.html.erb │ ├── shared │ │ ├── _sql.html.erb │ │ ├── _growth_rate.html.erb │ │ ├── devices │ │ │ ├── _nav.html.erb │ │ │ ├── _os.html.erb │ │ │ ├── _browser.html.erb │ │ │ └── _device_type.html.erb │ │ ├── sources │ │ │ ├── _nav.html.erb │ │ │ ├── _traffic_medium.html.erb │ │ │ ├── _traffic_source.html.erb │ │ │ ├── _traffic_campaign.html.erb │ │ │ └── _referrer_source.html.erb │ │ ├── _date_filter.html.erb │ │ ├── _steps.html.erb │ │ ├── _summary.html.erb │ │ └── _top_page.html.erb │ ├── verification_mailer │ │ └── verify.html.erb │ ├── registrations │ │ └── verification.html.erb │ ├── oses │ │ └── index.html.erb │ ├── browsers │ │ └── index.html.erb │ ├── device_types │ │ └── index.html.erb │ ├── countries │ │ └── index.html.erb │ ├── traffic_mediums │ │ └── index.html.erb │ ├── traffic_sources │ │ └── index.html.erb │ ├── traffic_campaigns │ │ └── index.html.erb │ ├── location_urls │ │ └── index.html.erb │ ├── settings │ │ ├── _nav.html.erb │ │ └── visibility.html.erb │ ├── sites │ │ ├── debug.html.erb │ │ └── index.html.erb │ ├── referrer_sources │ │ └── index.html.erb │ └── sessions │ │ └── new.html.erb ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── mailers │ ├── application_mailer.rb │ └── verification_mailer.rb ├── javascript │ ├── channels │ │ ├── index.js │ │ └── consumer.js │ ├── controllers │ │ ├── flash_controller.js │ │ ├── index.js │ │ └── hello_controller.js │ ├── stylesheets │ │ ├── loader.css │ │ ├── tooltip.css │ │ └── modal.css │ └── packs │ │ └── application.js └── jobs │ ├── application_job.rb │ └── mp_event_job.rb ├── .browserslistrc ├── .ruby-version ├── c.sh ├── diagrams ├── imgs │ ├── ga.png │ ├── rails.png │ ├── redis.png │ ├── chartio.png │ ├── sidekiq.png │ ├── metabase.png │ ├── openresty.png │ ├── postgresql.png │ └── TimescaleDB.png ├── scale_queue.png ├── scale_collector.png ├── simple_architecture.png ├── simple_architecture.py ├── scale_queue.py └── scale_collector.py ├── bin ├── rake ├── bundle ├── rails ├── yarn ├── webpack ├── webpack-dev-server ├── update └── setup ├── config ├── webpack │ ├── environment.js │ ├── test.js │ ├── production.js │ └── development.js ├── boot.rb ├── environment.rb ├── initializers │ ├── mime_types.rb │ ├── filter_parameter_logging.rb │ ├── redis.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── backtrace_silencers.rb │ ├── sql_formatter.rb │ ├── sidekiq.rb │ ├── default_host.rb │ ├── referer_source_detector.rb │ ├── wrap_parameters.rb │ ├── assets.rb │ ├── ipdb.rb │ ├── inflections.rb │ ├── tech_detector.rb │ ├── content_security_policy.rb │ ├── new_framework_defaults_5_2.rb │ └── traffic_detector.rb ├── cable.yml ├── credentials.yml.enc ├── sidekiq.yml ├── database.yml ├── locales │ └── en.yml ├── storage.yml ├── routes.rb ├── application.rb ├── puma.rb ├── environments │ ├── test.rb │ └── development.rb └── webpacker.yml ├── docker ├── dockerfiles │ ├── webpack.Dockerfile │ ├── rails.Dockerfile │ └── openresty.Dockerfile └── entrypoints │ ├── helpers │ └── pg_database_url.sh │ ├── webpack.sh │ └── rails.sh ├── db ├── migrate │ ├── 20210202211704_add_domain_for_site.rb │ ├── 20210130194542_add_email_verified_on_users.rb │ ├── 20210311142921_remove_username_from_site_connection.rb │ ├── 20210130223112_add_email_verification_token_on_users.rb │ ├── 20210211144851_use_uuid_for_site_id.rb │ ├── 20210115103047_create_shared_links.rb │ ├── 20210110200847_create_users.rb │ ├── 20210115101634_create_site_members.rb │ ├── 20210404121858_create_measurement_protocols.rb │ ├── 20210115100815_create_goals.rb │ ├── 20210311122231_add_site_connection.rb │ └── 20210110190350_create_sites.rb ├── hyper_migrate │ ├── 20210311173212_revoke_pg_roles.rb │ ├── 20210312171927_add_request_params.rb │ ├── 20210311151807_add_policy.rb │ ├── 20210226015643_add_traffic_stuff.rb │ ├── 20210226091913_drop_useless_column.rb │ ├── 20210311071834_default_role_for_user.rb │ ├── 20210311175013_revoke_public.rb │ └── 20210213182643_create_events.rb └── seeds.rb ├── config.ru ├── env.md ├── docker.md ├── Rakefile ├── .dockerignore ├── .env.example.docker ├── third_party_tracker.md ├── package.json ├── measurement_protocol_test.sh ├── benchmark └── siege ├── .gitignore ├── postcss.config.js ├── tailwind.config.js ├── test_request.sh ├── deploy └── nginx.conf ├── babel.config.js ├── .github └── workflows │ └── ci.yml └── Gemfile /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/pids/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.0.0 2 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /c.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | bundle exec rubocop -A 3 | -------------------------------------------------------------------------------- /diagrams/imgs/ga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperCable/hypercable/HEAD/diagrams/imgs/ga.png -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | -------------------------------------------------------------------------------- /diagrams/imgs/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperCable/hypercable/HEAD/diagrams/imgs/rails.png -------------------------------------------------------------------------------- /diagrams/imgs/redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperCable/hypercable/HEAD/diagrams/imgs/redis.png -------------------------------------------------------------------------------- /diagrams/imgs/chartio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperCable/hypercable/HEAD/diagrams/imgs/chartio.png -------------------------------------------------------------------------------- /diagrams/imgs/sidekiq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperCable/hypercable/HEAD/diagrams/imgs/sidekiq.png -------------------------------------------------------------------------------- /diagrams/scale_queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperCable/hypercable/HEAD/diagrams/scale_queue.png -------------------------------------------------------------------------------- /diagrams/imgs/metabase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperCable/hypercable/HEAD/diagrams/imgs/metabase.png -------------------------------------------------------------------------------- /diagrams/imgs/openresty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperCable/hypercable/HEAD/diagrams/imgs/openresty.png -------------------------------------------------------------------------------- /diagrams/imgs/postgresql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperCable/hypercable/HEAD/diagrams/imgs/postgresql.png -------------------------------------------------------------------------------- /diagrams/scale_collector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperCable/hypercable/HEAD/diagrams/scale_collector.png -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /diagrams/imgs/TimescaleDB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperCable/hypercable/HEAD/diagrams/imgs/TimescaleDB.png -------------------------------------------------------------------------------- /config/webpack/environment.js: -------------------------------------------------------------------------------- 1 | const { environment } = require('@rails/webpacker') 2 | 3 | module.exports = environment 4 | -------------------------------------------------------------------------------- /diagrams/simple_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperCable/hypercable/HEAD/diagrams/simple_architecture.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /app/views/shared/_sql.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |     <%= simple_format @sql %>
4 |   
5 |
-------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docker/dockerfiles/webpack.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hypercable:development 2 | 3 | RUN chmod +x docker/entrypoints/webpack.sh 4 | 5 | EXPOSE 3035 6 | CMD ["bin/webpack-dev-server"] -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Channel < ActionCable::Channel::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Connection < ActionCable::Connection::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20210202211704_add_domain_for_site.rb: -------------------------------------------------------------------------------- 1 | class AddDomainForSite < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :sites, :domain, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 4 | 5 | require "bundler/setup" # Set up gems listed in the Gemfile. 6 | -------------------------------------------------------------------------------- /docker/dockerfiles/rails.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hypercable:development 2 | 3 | RUN chmod +x docker/entrypoints/rails.sh 4 | 5 | EXPOSE 3000 6 | CMD ["rails", "server", "-b", "0.0.0.0", "-p", "3000"] -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require_relative "config/environment" 6 | 7 | run Rails.application 8 | -------------------------------------------------------------------------------- /config/webpack/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /config/webpack/production.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require_relative "application" 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /config/webpack/development.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /db/hyper_migrate/20210311173212_revoke_pg_roles.rb: -------------------------------------------------------------------------------- 1 | class RevokePgRoles < ActiveRecord::Migration[6.1] 2 | def change 3 | execute("REVOKE SELECT ON pg_catalog.pg_roles FROM readonly;") 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/hyper_migrate/20210312171927_add_request_params.rb: -------------------------------------------------------------------------------- 1 | class AddRequestParams < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :events, :request_params, :jsonb, default: '{}' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/jobs/hello_job_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class HelloJobTest < ActiveJob::TestCase 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | connects_to database: { writing: :main, reading: :main } 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20210130194542_add_email_verified_on_users.rb: -------------------------------------------------------------------------------- 1 | class AddEmailVerifiedOnUsers < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :users, :email_verified, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20210311142921_remove_username_from_site_connection.rb: -------------------------------------------------------------------------------- 1 | class RemoveUsernameFromSiteConnection < ActiveRecord::Migration[6.1] 2 | def change 3 | remove_column :site_connections, :username 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/application_hyper_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationHyperRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | connects_to database: { writing: :hyper, reading: :hyper } 6 | end 7 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationMailer < ActionMailer::Base 4 | default from: ENV["DEFAULT_MAIL_FROM"] || "Hypercable " 5 | layout "mailer" 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new mime types for use in respond_to blocks: 5 | # Mime::Type.register "text/richtext", :rtf 6 | -------------------------------------------------------------------------------- /env.md: -------------------------------------------------------------------------------- 1 | ## ENV list & desc 2 | 3 | 4 | | name | desc | example value | 5 | | :-----| :---- | :---- | 6 | | HOST | host for web | hypercable.plus | 7 | | COLLECTOR | host for collector | collector.hypercable.plus | 8 | 9 | 10 | todo -------------------------------------------------------------------------------- /test/mailers/verification_mailer_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class VerificationMailerTest < ActionMailer::TestCase 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 6 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400] 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/oses_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class OsesControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/controllers/sites_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class SitesControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /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: hypercable_web_production 11 | -------------------------------------------------------------------------------- /db/hyper_migrate/20210311151807_add_policy.rb: -------------------------------------------------------------------------------- 1 | class AddPolicy < ActiveRecord::Migration[6.1] 2 | def change 3 | execute(<<~SQL) 4 | create policy event_access on events for select using (site_id = current_user::text) 5 | SQL 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20210130223112_add_email_verification_token_on_users.rb: -------------------------------------------------------------------------------- 1 | class AddEmailVerificationTokenOnUsers < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :users, :email_verification_token, :string, index: {unique: true} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/controllers/browsers_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class BrowsersControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/controllers/countries_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class CountriesControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/controllers/sessions_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class SessionsControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/controllers/settings_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class SettingsControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /app/javascript/channels/index.js: -------------------------------------------------------------------------------- 1 | // Load all the channels within this directory and all subdirectories. 2 | // Channel files must be named *_channel.js. 3 | 4 | const channels = require.context('.', true, /_channel\.js$/) 5 | channels.keys().forEach(channels) 6 | -------------------------------------------------------------------------------- /test/controllers/device_types_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class DeviceTypesControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /docker.md: -------------------------------------------------------------------------------- 1 | ## dev 2 | 3 | * docker-compose build 4 | 5 | ## start 6 | 7 | * docker-compose up 8 | 9 | ## migration 10 | 11 | * docker-compose run rails rake db:migrate 12 | 13 | ## debug 14 | 15 | * using binding.pry 16 | * docker-compose attach xxx 17 | -------------------------------------------------------------------------------- /test/controllers/location_urls_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class LocationUrlsControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/controllers/registrations_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class RegistrationsControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/controllers/site_connections_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class SiteConnectionsControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/controllers/traffic_campaigns_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class TrafficCampaignsControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/controllers/traffic_mediums_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class TrafficMediumsControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/controllers/traffic_sources_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class TrafficSourcesControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/controllers/measurement_protocols_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class MeasurementProtocolsControllerTest < ActionDispatch::IntegrationTest 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require_relative "config/application" 7 | 8 | Rails.application.load_tasks 9 | -------------------------------------------------------------------------------- /app/mailers/verification_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class VerificationMailer < ApplicationMailer 4 | def verify 5 | @user = params[:user] 6 | mail(to: @user.email, subject: "Finish setting up your Hypercable account: verify your email address") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Configure sensitive parameters which will be filtered from the log file. 6 | Rails.application.config.filter_parameters += [:password] 7 | -------------------------------------------------------------------------------- /config/initializers/redis.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RedisClient = Redis.new(driver: :hiredis, url: ENV["SAAS_REDIS_URL"] || ENV["REDIS_URL"] || "redis://localhost:6379/0") 4 | 5 | MeasurementProtocol.all.each do |mp| 6 | RedisClient.set(mp.api_secret, mp.site.uuid) 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20210211144851_use_uuid_for_site_id.rb: -------------------------------------------------------------------------------- 1 | class UseUuidForSiteId < ActiveRecord::Migration[6.1] 2 | def change 3 | enable_extension 'pgcrypto' 4 | add_column :sites, :uuid, :uuid, default: "gen_random_uuid()", null: false 5 | add_index :sites, :uuid, unique: true 6 | end 7 | end -------------------------------------------------------------------------------- /db/hyper_migrate/20210226015643_add_traffic_stuff.rb: -------------------------------------------------------------------------------- 1 | class AddTrafficStuff < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :events, :traffic_campaign, :string 4 | add_column :events, :traffic_medium, :string 5 | add_column :events, :traffic_source, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/javascript/controllers/flash_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = [ "container" ] 5 | 6 | close(){ 7 | const element = this.containerTarget; 8 | element.setAttribute("style", "display:none"); 9 | } 10 | } -------------------------------------------------------------------------------- /db/migrate/20210115103047_create_shared_links.rb: -------------------------------------------------------------------------------- 1 | class CreateSharedLinks < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :shared_links do |t| 4 | t.bigint :site_id, index: true 5 | t.string :slug, index: {unique: true} 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | .env 3 | .env.* 4 | .git 5 | .gitignore 6 | docker-compose.* 7 | docker/Dockerfile 8 | docker/dockerfiles 9 | log 10 | storage 11 | public/system 12 | tmp 13 | .codeclimate.yml 14 | public/assets 15 | public/packs 16 | node_modules 17 | vendor/bundle 18 | .DS_Store 19 | *.swp 20 | *~ 21 | -------------------------------------------------------------------------------- /app/javascript/channels/consumer.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command. 3 | 4 | import { createConsumer } from "@rails/actioncable" 5 | 6 | export default createConsumer() 7 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/mailers/previews/verification_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Preview all emails at http://localhost:3000/rails/mailers/varification_mailer 4 | class VerificationMailerPreview < ActionMailer::Preview 5 | def verify 6 | VerificationMailer.with(user: User.last).verify 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20210110200847_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :users do |t| 4 | t.string :email, null: false 5 | t.string :password_digest 6 | t.string :remember_token, index: {unique: true} 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20210115101634_create_site_members.rb: -------------------------------------------------------------------------------- 1 | class CreateSiteMembers < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :site_members do |t| 4 | t.bigint :site_id 5 | t.bigint :user_id 6 | t.timestamps 7 | t.index [:site_id, :user_id], unique: true 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20210404121858_create_measurement_protocols.rb: -------------------------------------------------------------------------------- 1 | class CreateMeasurementProtocols < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :measurement_protocols do |t| 4 | t.bigint :site_id, index: true 5 | t.string :api_secret, null: false 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # ActiveSupport::Reloader.to_prepare do 5 | # ApplicationController.renderer.defaults.merge!( 6 | # http_host: 'example.org', 7 | # https: false 8 | # ) 9 | # end 10 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Specify a serializer for the signed and encrypted cookie jars. 6 | # Valid options are :json, :marshal, and :hybrid. 7 | Rails.application.config.action_dispatch.cookies_serializer = :json 8 | -------------------------------------------------------------------------------- /db/migrate/20210115100815_create_goals.rb: -------------------------------------------------------------------------------- 1 | class CreateGoals < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :goals do |t| 4 | t.bigint :user_id, index: true 5 | t.bigint :site_id, index: true 6 | t.string :event_name 7 | t.string :path 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20210311122231_add_site_connection.rb: -------------------------------------------------------------------------------- 1 | class AddSiteConnection < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :site_connections do |t| 4 | t.bigint :site_id, index: true 5 | t.string :username, null: false 6 | t.string :password, null: false 7 | t.timestamps 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | begin 5 | exec "yarnpkg", *ARGV 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationJob < ActiveJob::Base 4 | # Automatically retry jobs that encountered a deadlock 5 | # retry_on ActiveRecord::Deadlocked 6 | 7 | # Most jobs are safe to ignore if the underlying records are no longer available 8 | # discard_on ActiveJob::DeserializationError 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20210110190350_create_sites.rb: -------------------------------------------------------------------------------- 1 | class CreateSites < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :sites do |t| 4 | t.bigint :user_id, index: true 5 | t.string :tracking_id, index: {unique: true} 6 | t.string :timezone 7 | t.boolean :public, default: false 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/channels/application_cable/connection_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase 6 | # test "connects with cookies" do 7 | # cookies.signed[:user_id] = 42 8 | # 9 | # connect 10 | # 11 | # assert_equal connection.user_id, "42" 12 | # end 13 | end 14 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /db/hyper_migrate/20210226091913_drop_useless_column.rb: -------------------------------------------------------------------------------- 1 | class DropUselessColumn < ActiveRecord::Migration[6.1] 2 | def change 3 | remove_column :events, :non_interaction_hit 4 | remove_column :events, :utm_source 5 | remove_column :events, :utm_medium 6 | remove_column :events, :utm_campaign 7 | remove_column :events, :utm_term 8 | remove_column :events, :utm_content 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/shared/_growth_rate.html.erb: -------------------------------------------------------------------------------- 1 | <% if growth.na? %> 2 | 〰 N/A 3 | <% elsif growth.positive?%> 4 | <%= number_to_percentage growth.growth_rate, precision: 0 %> 5 | <% else %> 6 | <%= number_to_percentage growth.growth_rate, precision: 0 %> 7 | <% end %> -------------------------------------------------------------------------------- /db/hyper_migrate/20210311071834_default_role_for_user.rb: -------------------------------------------------------------------------------- 1 | class DefaultRoleForUser < ActiveRecord::Migration[6.1] 2 | def change 3 | current_db_name = connection.current_database 4 | execute("CREATE ROLE readonly;") 5 | execute("GRANT USAGE ON SCHEMA public TO readonly;") 6 | execute("GRANT CONNECT ON DATABASE #{current_db_name} TO readonly;") 7 | execute("GRANT SELECT ON events TO readonly;") 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require_tree ./channels 7 | 8 | (function() { 9 | this.App || (this.App = {}); 10 | 11 | App.cable = ActionCable.createConsumer(); 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /docker/entrypoints/helpers/pg_database_url.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'uri' 3 | 4 | # Let DATABASE_URL env take presedence over individual connection params. 5 | if !ENV['MAIN_DATABASE_URL'].nil? && ENV['MAIN_DATABASE_URL'] != '' 6 | uri = URI(ENV['MAIN_DATABASE_URL']) 7 | puts "export POSTGRES_HOST=#{uri.host} POSTGRES_PORT=#{uri.port || 5432} POSTGRES_USERNAME=#{uri.user}" 8 | else 9 | puts "export POSTGRES_PORT=5432" 10 | end -------------------------------------------------------------------------------- /docker/entrypoints/webpack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | rm -rf /app/tmp/pids/server.pid 5 | rm -rf /app/tmp/cache/* 6 | 7 | yarn install --check-files 8 | 9 | echo "Waiting for yarn and bundle integrity to match lockfiles...." 10 | YARN="yarn check --integrity" 11 | BUNDLE="bundle check" 12 | 13 | until $YARN && $BUNDLE 14 | do 15 | sleep 2; 16 | done 17 | 18 | echo "Ready to run webpack development server." 19 | 20 | exec "$@" 21 | -------------------------------------------------------------------------------- /test/models/hello_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: hellos 6 | # 7 | # id :bigint not null, primary key 8 | # name :string 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | require "test_helper" 13 | 14 | class HelloTest < ActiveSupport::TestCase 15 | # test "the truth" do 16 | # assert true 17 | # end 18 | end 19 | -------------------------------------------------------------------------------- /app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | // Load all the controllers within this directory and all subdirectories. 2 | // Controller files must be named *_controller.js. 3 | 4 | import { Application } from "stimulus" 5 | import { definitionsFromContext } from "stimulus/webpack-helpers" 6 | 7 | const application = Application.start() 8 | const context = require.context("controllers", true, /_controller\.js$/) 9 | application.load(definitionsFromContext(context)) 10 | 11 | -------------------------------------------------------------------------------- /app/views/layouts/_flash.html.erb: -------------------------------------------------------------------------------- 1 | <% if flash.any? %> 2 |
3 |
4 | <% flash.each do |key, value| %> 5 | <% if %w[error info success].include?(key) && value.present? %> 6 |
7 | <%= render "layouts/flash/#{key}" %> 8 |
9 | <% end %> 10 | <% end %> 11 |
12 |
13 | <% end %> -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | 9WLdER2sdZpEaZNgnFR6PUk+2TKMdqvyTVBrDEaBAZuYS5g9WmC2VRxmJMepCNq6RWt+YJXJyJjR+AXsrV1u80gJbxbTRqEZZWdF2K2sM6X+ixNCvV/isH5kq4PFGctu24U1X3mCzdFC80WQcCUbEhKg4CrupOhlrbsamr4LOjxiva9wkwhGeNP1FTgtM3DwYJFeOXkI1oOJYr8UD4LA6ODolO7C+SxnFqyrMSN/eJ2IQ4aj2LAZw/fU8Jvax53CsMaBSIx8XTDRcfxKlBoTyEu+PIq9ijvp3qxemW8w0AY9nf9D+QyDjO+bpgJPJlt6GEhEtYc58NsGSb8cIskjywTuOqjmd14w9XP/lg7wIyOn6O1gQczqbf9De0xkciSjuD/hibJeB5n/ywCTOzXctv2VejwaXo/9xAgR--CDZevZ+xv9KxeEnv--DNQwaQxFynsbfwOc7zYEJA== -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 5 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 6 | 7 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 8 | # Rails.backtrace_cleaner.remove_silencers! 9 | -------------------------------------------------------------------------------- /config/initializers/sql_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "digest" 4 | 5 | class SqlFormatter 6 | TMP_PATH = Rails.root.join("tmp") 7 | 8 | attr_reader :relation 9 | def initialize(relation) 10 | @relation = relation 11 | end 12 | 13 | def call 14 | sql = relation.to_sql 15 | path = File.join(TMP_PATH, Digest::MD5.hexdigest(sql)) + ".sql" 16 | File.open(path, "w+") { |file| file.write sql } 17 | `pg_format #{path}` 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV["RAILS_ENV"] ||= "test" 4 | require_relative "../config/environment" 5 | require "rails/test_help" 6 | 7 | class ActiveSupport::TestCase 8 | # Run tests in parallel with specified workers 9 | parallelize(workers: :number_of_processors) 10 | 11 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 12 | fixtures :all 13 | 14 | # Add more helper methods to be used by all tests here... 15 | end 16 | -------------------------------------------------------------------------------- /bin/webpack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/webpack_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::WebpackRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /config/initializers/sidekiq.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "sidekiq/worker_killer" 4 | 5 | Sidekiq.configure_server do |config| 6 | config.redis = { driver: :hiredis, url: ENV["REDIS_URL"] || "redis://localhost:6379/0" } 7 | config.server_middleware do |chain| 8 | # chain.add Sidekiq::WorkerKiller, max_rss: 880 9 | end 10 | end 11 | 12 | Sidekiq.configure_client do |config| 13 | config.redis = { driver: :hiredis, url: ENV["REDIS_URL"] || "redis://localhost:6379/0" } 14 | end 15 | -------------------------------------------------------------------------------- /bin/webpack-dev-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/dev_server_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::DevServerRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /config/initializers/default_host.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | default = if Rails.env.development? 4 | "localhost:3000" 5 | elsif Rails.env.test? 6 | "localhost:3000" 7 | elsif Rails.env.production? 8 | "hypercable.plus" 9 | end 10 | Rails.application.routes.default_url_options[:host] = ENV["HOST"] || default 11 | 12 | # NOTICE: http://lulalala.logdown.com/posts/5835445-rails-many-default-url-options 13 | Rails.application.routes.default_url_options[:protocol] = "https" if Rails.env.production? 14 | -------------------------------------------------------------------------------- /.env.example.docker: -------------------------------------------------------------------------------- 1 | RAILS_ENV=development 2 | SKIP_TEST_DATABASE=true 3 | HYPER_DATABASE_URL=postgres://postgres:password@tsdb:5432/tsdb_dev 4 | REDIS_URL=redis://redis:6379 5 | MAIN_DATABASE_URL=postgres://postgres:password@postgres:5432/hypercable_dev 6 | REDIS_HOST=redis 7 | REDIS_PORT=6379 8 | 9 | GEOIPUPDATE_ACCOUNT_ID= 10 | GEOIPUPDATE_LICENSE_KEY= 11 | GEOIPUPDATE_EDITION_IDS="GeoLite2-ASN GeoLite2-City GeoLite2-Country" 12 | GEOIPUPDATE_FREQUENCY=72 13 | 14 | HOST=localhost:3333 15 | COLLECTOR_HOST=localhost:8000 16 | -------------------------------------------------------------------------------- /app/models/site_member.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: site_members 6 | # 7 | # id :bigint not null, primary key 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # site_id :bigint 11 | # user_id :bigint 12 | # 13 | # Indexes 14 | # 15 | # index_site_members_on_site_id_and_user_id (site_id,user_id) UNIQUE 16 | # 17 | class SiteMember < ApplicationRecord 18 | belongs_to :user 19 | belongs_to :site 20 | end 21 | -------------------------------------------------------------------------------- /config/initializers/referer_source_detector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RefererSourceDetector 4 | CACHE = LruRedux::Cache.new(1000) 5 | 6 | def self.detect(referer, cache = true) 7 | if cache 8 | CACHE.getset(referer) { self.detect_without_cache(referer) } 9 | else 10 | self.detect_without_cache(ua) 11 | end 12 | end 13 | 14 | def self.detect_without_cache(referer) 15 | return nil if referer.blank? 16 | uri = URI.parse(referer) rescue nil 17 | uri&.host 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/shared_link.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: shared_links 6 | # 7 | # id :bigint not null, primary key 8 | # slug :string 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # site_id :bigint 12 | # 13 | # Indexes 14 | # 15 | # index_shared_links_on_site_id (site_id) 16 | # index_shared_links_on_slug (slug) UNIQUE 17 | # 18 | class SharedLink < ApplicationRecord 19 | belongs_to :site 20 | end 21 | -------------------------------------------------------------------------------- /app/models/growth_rate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class GrowthRate 4 | attr_reader :base_data, :new_data 5 | 6 | def initialize(base_data, new_data) 7 | @base_data = base_data.to_f 8 | @new_data = new_data.to_f 9 | end 10 | 11 | def growth_value 12 | new_data - base_data 13 | end 14 | 15 | def growth_rate 16 | (growth_value / base_data) * 100 17 | end 18 | 19 | def na? 20 | return true if base_data.zero? 21 | false 22 | end 23 | 24 | def positive? 25 | growth_value > 0 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/javascript/controllers/hello_controller.js: -------------------------------------------------------------------------------- 1 | // Visit The Stimulus Handbook for more details 2 | // https://stimulusjs.org/handbook/introduction 3 | // 4 | // This example controller works with specially annotated HTML like: 5 | // 6 | //
7 | //

8 | //
9 | 10 | import { Controller } from "stimulus" 11 | 12 | export default class extends Controller { 13 | static targets = [ "output" ] 14 | 15 | connect() { 16 | this.outputTarget.textContent = 'Hello, Stimulus!' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/hellos.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: hellos 4 | # 5 | # id :bigint not null, primary key 6 | # name :string 7 | # created_at :datetime not null 8 | # updated_at :datetime not null 9 | # 10 | 11 | # This model initially had no columns defined. If you add columns to the 12 | # model remove the '{}' from the fixture names and add the columns immediately 13 | # below each fixture, per the syntax in the comments below 14 | # 15 | one: {} 16 | # column: value 17 | # 18 | two: {} 19 | # column: value 20 | -------------------------------------------------------------------------------- /app/models/goal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: goals 6 | # 7 | # id :bigint not null, primary key 8 | # event_name :string 9 | # path :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # site_id :bigint 13 | # user_id :bigint 14 | # 15 | # Indexes 16 | # 17 | # index_goals_on_site_id (site_id) 18 | # index_goals_on_user_id (user_id) 19 | # 20 | class Goal < ApplicationRecord 21 | belongs_to :site 22 | belongs_to :user 23 | end 24 | -------------------------------------------------------------------------------- /app/views/verification_mailer/verify.html.erb: -------------------------------------------------------------------------------- 1 |

Hi there,

2 | 3 |

Your Hypercable account is almost ready!

4 | 5 |

Click below to verify your email and activate your account.

6 | 7 |
8 | 9 | Verify my Email 10 | 11 |
-------------------------------------------------------------------------------- /db/hyper_migrate/20210311175013_revoke_public.rb: -------------------------------------------------------------------------------- 1 | class RevokePublic < ActiveRecord::Migration[6.1] 2 | def change 3 | current_db_name = connection.current_database 4 | execute("REVOKE SELECT ON pg_catalog.pg_roles FROM public;") 5 | execute("REVOKE SELECT ON pg_catalog.pg_roles FROM readonly;") 6 | 7 | execute("REVOKE SELECT ON pg_catalog.pg_authid FROM public;") 8 | execute("REVOKE SELECT ON pg_catalog.pg_authid FROM readonly;") 9 | 10 | execute("revoke create on schema public from PUBLIC;") 11 | execute("REVOKE ALL ON DATABASE #{current_db_name} FROM PUBLIC;") 12 | end 13 | end -------------------------------------------------------------------------------- /test/models/site_member_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: site_members 6 | # 7 | # id :bigint not null, primary key 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # site_id :bigint 11 | # user_id :bigint 12 | # 13 | # Indexes 14 | # 15 | # index_site_members_on_site_id_and_user_id (site_id,user_id) UNIQUE 16 | # 17 | require "test_helper" 18 | 19 | class SiteMemberTest < ActiveSupport::TestCase 20 | # test "the truth" do 21 | # assert true 22 | # end 23 | end 24 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # This file contains settings for ActionController::ParamsWrapper which 6 | # is enabled by default. 7 | 8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 9 | ActiveSupport.on_load(:action_controller) do 10 | wrap_parameters format: [:json] 11 | end 12 | 13 | # To enable root element in JSON for ActiveRecord objects. 14 | # ActiveSupport.on_load(:active_record) do 15 | # self.include_root_in_json = true 16 | # end 17 | -------------------------------------------------------------------------------- /docker/dockerfiles/openresty.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openresty/openresty:alpine-fat 2 | 3 | RUN apk add git 4 | 5 | EXPOSE 8000 6 | ENV REDIS_HOST redis 7 | ENV REDIS_PORT 6379 8 | RUN echo $REDIS_HOST 9 | RUN /usr/local/openresty/luajit/bin/luarocks install lua-resty-reqargs 10 | RUN /usr/local/openresty/luajit/bin/luarocks install lua-resty-jit-uuid 11 | RUN /usr/local/openresty/luajit/bin/luarocks install jsonschema 12 | ADD nginx.conf /usr/local/openresty/nginx/conf/nginx.conf 13 | ADD debug_mp.lua /usr/local/openresty/nginx/debug_mp.lua 14 | #RUN echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' > /etc/nsswitch.conf -------------------------------------------------------------------------------- /test/models/measurement_protocol_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: measurement_protocols 6 | # 7 | # id :bigint not null, primary key 8 | # api_secret :string not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # site_id :bigint 12 | # 13 | # Indexes 14 | # 15 | # index_measurement_protocols_on_site_id (site_id) 16 | # 17 | require "test_helper" 18 | 19 | class MeasurementProtocolTest < ActiveSupport::TestCase 20 | # test "the truth" do 21 | # assert true 22 | # end 23 | end 24 | -------------------------------------------------------------------------------- /test/models/shared_link_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: shared_links 6 | # 7 | # id :bigint not null, primary key 8 | # slug :string 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # site_id :bigint 12 | # 13 | # Indexes 14 | # 15 | # index_shared_links_on_site_id (site_id) 16 | # index_shared_links_on_slug (slug) UNIQUE 17 | # 18 | require "test_helper" 19 | 20 | class SharedLinkTest < ActiveSupport::TestCase 21 | # test "the truth" do 22 | # assert true 23 | # end 24 | end 25 | -------------------------------------------------------------------------------- /test/models/goal_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: goals 6 | # 7 | # id :bigint not null, primary key 8 | # event_name :string 9 | # path :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # site_id :bigint 13 | # user_id :bigint 14 | # 15 | # Indexes 16 | # 17 | # index_goals_on_site_id (site_id) 18 | # index_goals_on_user_id (user_id) 19 | # 20 | require "test_helper" 21 | 22 | class GoalTest < ActiveSupport::TestCase 23 | # test "the truth" do 24 | # assert true 25 | # end 26 | end 27 | -------------------------------------------------------------------------------- /app/controllers/measurement_protocols_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class MeasurementProtocolsController < ApplicationController 4 | before_action :get_site 5 | 6 | def create 7 | @mp = @site.measurement_protocols.create 8 | redirect_to site_measurement_protocols_path(@site) 9 | end 10 | 11 | def destroy 12 | @mp = @site.measurement_protocols.find(params[:id]) 13 | @mp.destroy 14 | redirect_to site_measurement_protocols_path(@site) 15 | end 16 | 17 | def index 18 | @mps = @site.measurement_protocols 19 | end 20 | 21 | def get_site 22 | @site = current_user.sites.find_by!(uuid: params[:site_id]) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/site_connections_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SiteConnectionsController < ApplicationController 4 | before_action :get_site 5 | 6 | def create 7 | @site_connection = @site.site_connections.create 8 | redirect_to site_site_connections_path(@site) 9 | end 10 | 11 | def destroy 12 | @site_connection = @site.site_connections.find(params[:id]) 13 | @site_connection.destroy 14 | redirect_to site_site_connections_path(@site) 15 | end 16 | 17 | def index 18 | @site_connections = @site.site_connections 19 | end 20 | 21 | def get_site 22 | @site = current_user.sites.find_by!(uuid: params[:site_id]) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /third_party_tracker.md: -------------------------------------------------------------------------------- 1 | ## Google Analytics 2 | 3 | ``` 4 | 5 | 20 | ``` 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hypercable_web", 3 | "private": true, 4 | "dependencies": { 5 | "@rails/actioncable": "^6.0.0", 6 | "@rails/activestorage": "^6.0.0", 7 | "@rails/ujs": "^6.0.0", 8 | "@rails/webpacker": "4.2.2", 9 | "@tailwindcss/aspect-ratio": "^0.2.0", 10 | "@tailwindcss/forms": "^0.2.1", 11 | "@tailwindcss/typography": "^0.4.0", 12 | "alpinejs": "^2.8.0", 13 | "chart.js": "^2.9.4", 14 | "chartkick": "^3.2.1", 15 | "stimulus": "^1.1.1", 16 | "tailwindcss": "npm:@tailwindcss/postcss7-compat", 17 | "turbolinks": "^5.2.0" 18 | }, 19 | "version": "0.1.0", 20 | "devDependencies": { 21 | "webpack-dev-server": "^3.11.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/site_members.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: site_members 4 | # 5 | # id :bigint not null, primary key 6 | # created_at :datetime not null 7 | # updated_at :datetime not null 8 | # site_id :bigint 9 | # user_id :bigint 10 | # 11 | # Indexes 12 | # 13 | # index_site_members_on_site_id_and_user_id (site_id,user_id) UNIQUE 14 | # 15 | 16 | # This model initially had no columns defined. If you add columns to the 17 | # model remove the '{}' from the fixture names and add the columns immediately 18 | # below each fixture, per the syntax in the comments below 19 | # 20 | one: {} 21 | # column: value 22 | # 23 | two: {} 24 | # column: value 25 | -------------------------------------------------------------------------------- /config/sidekiq.yml: -------------------------------------------------------------------------------- 1 | # Sample configuration file for Sidekiq. 2 | # Options here can still be overridden by cmd line args. 3 | # Place this file at config/sidekiq.yml and Sidekiq will 4 | # pick it up automatically. 5 | --- 6 | :verbose: true 7 | :concurrency: 10 8 | :timeout: 25 9 | 10 | # Sidekiq will run this file through ERB when reading it so you can 11 | # even put in dynamic logic, like a host-specific queue. 12 | # http://www.mikeperham.com/2013/11/13/advanced-sidekiq-host-specific-queues/ 13 | :queues: 14 | - critical 15 | - default 16 | - low 17 | 18 | # you can override concurrency based on environment 19 | production: 20 | :concurrency: <%= ENV['SIDEKIQ_CONCURRENCY'] || 25 %> 21 | staging: 22 | :concurrency: 15 23 | -------------------------------------------------------------------------------- /test/fixtures/measurement_protocols.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: measurement_protocols 4 | # 5 | # id :bigint not null, primary key 6 | # api_secret :string not null 7 | # created_at :datetime not null 8 | # updated_at :datetime not null 9 | # site_id :bigint 10 | # 11 | # Indexes 12 | # 13 | # index_measurement_protocols_on_site_id (site_id) 14 | # 15 | 16 | # This model initially had no columns defined. If you add columns to the 17 | # model remove the '{}' from the fixture names and add the columns immediately 18 | # below each fixture, per the syntax in the comments below 19 | # 20 | one: {} 21 | # column: value 22 | # 23 | two: {} 24 | # column: value 25 | -------------------------------------------------------------------------------- /test/fixtures/shared_links.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: shared_links 4 | # 5 | # id :bigint not null, primary key 6 | # slug :string 7 | # created_at :datetime not null 8 | # updated_at :datetime not null 9 | # site_id :bigint 10 | # 11 | # Indexes 12 | # 13 | # index_shared_links_on_site_id (site_id) 14 | # index_shared_links_on_slug (slug) UNIQUE 15 | # 16 | 17 | # This model initially had no columns defined. If you add columns to the 18 | # model remove the '{}' from the fixture names and add the columns immediately 19 | # below each fixture, per the syntax in the comments below 20 | # 21 | one: {} 22 | # column: value 23 | # 24 | two: {} 25 | # column: value 26 | -------------------------------------------------------------------------------- /app/controllers/oses_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class OsesController < ApplicationController 4 | layout "detail" 5 | 6 | def index 7 | @site = current_user.sites.find_by!(uuid: params[:site_id]) 8 | 9 | base = QueryBuilder.call(Hyper::Event, @site, params).result 10 | current_scope = QueryBuilder.call(Hyper::Event, @site, period: "realtime").result 11 | @current_visitors_count = current_scope.distinct.count(:client_id) 12 | @top_oses = base 13 | .where(event_name: "page_view") 14 | .select("os, count(distinct client_id) as visitors_count, count(*) as count, count(distinct session_id) as sessions_count") 15 | .group("site_id, os").order("2 desc").limit(100) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/views/shared/devices/_nav.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Top Devices

3 | 8 |
-------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Version of your assets, change this if you want to expire all your assets. 6 | Rails.application.config.assets.version = "1.0" 7 | 8 | # Add additional assets to the asset load path. 9 | # Rails.application.config.assets.paths << Emoji.images_path 10 | # Add Yarn node_modules folder to the asset load path. 11 | Rails.application.config.assets.paths << Rails.root.join("node_modules") 12 | 13 | # Precompile additional assets. 14 | # application.js, application.css, and all non-JS/CSS in the app/assets 15 | # folder are already added. 16 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 17 | -------------------------------------------------------------------------------- /test/fixtures/goals.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: goals 4 | # 5 | # id :bigint not null, primary key 6 | # event_name :string 7 | # path :string 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # site_id :bigint 11 | # user_id :bigint 12 | # 13 | # Indexes 14 | # 15 | # index_goals_on_site_id (site_id) 16 | # index_goals_on_user_id (user_id) 17 | # 18 | 19 | # This model initially had no columns defined. If you add columns to the 20 | # model remove the '{}' from the fixture names and add the columns immediately 21 | # below each fixture, per the syntax in the comments below 22 | # 23 | one: {} 24 | # column: value 25 | # 26 | two: {} 27 | # column: value 28 | -------------------------------------------------------------------------------- /app/controllers/browsers_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class BrowsersController < ApplicationController 4 | layout "detail" 5 | 6 | def index 7 | @site = current_user.sites.find_by!(uuid: params[:site_id]) 8 | 9 | base = QueryBuilder.call(Hyper::Event, @site, params).result 10 | current_scope = QueryBuilder.call(Hyper::Event, @site, period: "realtime").result 11 | @current_visitors_count = current_scope.distinct.count(:client_id) 12 | @top_browsers = base 13 | .where(event_name: "page_view") 14 | .select("browser, count(distinct client_id) as visitors_count, count(*) as count, count(distinct session_id) as sessions_count") 15 | .group("site_id, browser").order("2 desc").limit(100) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/countries_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CountriesController < ApplicationController 4 | layout "detail" 5 | 6 | def index 7 | @site = current_user.sites.find_by!(uuid: params[:site_id]) 8 | 9 | base = QueryBuilder.call(Hyper::Event, @site, params).result 10 | current_scope = QueryBuilder.call(Hyper::Event, @site, period: "realtime").result 11 | @current_visitors_count = current_scope.distinct.count(:client_id) 12 | @top_countries = base 13 | .where(event_name: "page_view") 14 | .select("country, count(distinct client_id) as visitors_count, count(*) as count, count(distinct session_id) as sessions_count") 15 | .group("site_id, country").order("2 desc").limit(100) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/settings_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SettingsController < ApplicationController 4 | before_action :get_site 5 | 6 | def general 7 | end 8 | 9 | def update_general 10 | @site.update(params.require(:site).permit(:timezone, :domain, :tracking_id)) 11 | if @site.valid? 12 | flash[:success] = "Site update successfully." 13 | redirect_back fallback_location: general_site_settings_path(@site) 14 | else 15 | flash[:error] = "Site update failed" 16 | render "general" 17 | end 18 | end 19 | 20 | def visibility 21 | end 22 | 23 | def update_visibility 24 | end 25 | 26 | def get_site 27 | @site = current_user.sites.find_by!(uuid: params[:site_id]) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /config/initializers/ipdb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class IPDB 4 | CACHE = LruRedux::Cache.new(10000) 5 | begin 6 | if Rails.env.production? 7 | MMDB = GeoIP2Compat.new("/usr/share/GeoIP/GeoLite2-City.mmdb") 8 | else 9 | MMDB = GeoIP2Compat.new("/usr/local/var/GeoIP/GeoLite2-City.mmdb") 10 | end 11 | rescue GeoIP2Compat::Error 12 | puts "run 'geoipupdate -d /usr/local/var/GeoIP' locally." 13 | end 14 | 15 | def self.get_from_mmdb(ip) 16 | MMDB.lookup(ip) || {} 17 | rescue GeoIP2Compat::Error 18 | {} 19 | end 20 | 21 | def self.get(ip, cache = true) 22 | if cache 23 | CACHE.getset(ip) { self.get_from_mmdb(ip) } 24 | else 25 | self.get_from_mmdb(ip) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new inflection rules using the following format. Inflections 5 | # are locale specific, and you may define rules for as many different 6 | # locales as you wish. All of these examples are active by default: 7 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 8 | # inflect.plural /^(ox)$/i, '\1en' 9 | # inflect.singular /^(ox)en/i, '\1' 10 | # inflect.irregular 'person', 'people' 11 | # inflect.uncountable %w( fish sheep ) 12 | # end 13 | 14 | # These inflection rules are supported but not enabled by default: 15 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 16 | # inflect.acronym 'RESTful' 17 | # end 18 | -------------------------------------------------------------------------------- /app/controllers/device_types_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DeviceTypesController < ApplicationController 4 | layout "detail" 5 | 6 | def index 7 | @site = current_user.sites.find_by!(uuid: params[:site_id]) 8 | 9 | base = QueryBuilder.call(Hyper::Event, @site, params).result 10 | current_scope = QueryBuilder.call(Hyper::Event, @site, period: "realtime").result 11 | @current_visitors_count = current_scope.distinct.count(:client_id) 12 | @top_device_types = base 13 | .where(event_name: "page_view") 14 | .select("device_type, count(distinct client_id) as visitors_count, count(*) as count, count(distinct session_id) as sessions_count") 15 | .group("site_id, device_type").order("2 desc").limit(100) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/models/measurement_protocol.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: measurement_protocols 6 | # 7 | # id :bigint not null, primary key 8 | # api_secret :string not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # site_id :bigint 12 | # 13 | # Indexes 14 | # 15 | # index_measurement_protocols_on_site_id (site_id) 16 | # 17 | class MeasurementProtocol < ApplicationRecord 18 | belongs_to :site 19 | 20 | before_create do 21 | self.api_secret = SecureRandom.hex(20) 22 | end 23 | 24 | after_create do 25 | RedisClient.set(api_secret, site.uuid) 26 | end 27 | 28 | before_destroy do 29 | RedisClient.del(api_secret) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /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, or any plugin's 6 | * 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 | -------------------------------------------------------------------------------- /app/controllers/traffic_mediums_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TrafficMediumsController < ApplicationController 4 | layout "detail" 5 | 6 | def index 7 | @site = current_user.sites.find_by!(uuid: params[:site_id]) 8 | 9 | base = QueryBuilder.call(Hyper::Event, @site, params).result 10 | current_scope = QueryBuilder.call(Hyper::Event, @site, period: "realtime").result 11 | @current_visitors_count = current_scope.distinct.count(:client_id) 12 | @top_traffic_mediums = base 13 | .where(event_name: "page_view") 14 | .select("traffic_medium, count(distinct client_id) as visitors_count, count(*) as count, count(distinct session_id) as sessions_count") 15 | .group("site_id, traffic_medium").order("2 desc").limit(100) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/traffic_sources_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TrafficSourcesController < ApplicationController 4 | layout "detail" 5 | 6 | def index 7 | @site = current_user.sites.find_by!(uuid: params[:site_id]) 8 | 9 | base = QueryBuilder.call(Hyper::Event, @site, params).result 10 | current_scope = QueryBuilder.call(Hyper::Event, @site, period: "realtime").result 11 | @current_visitors_count = current_scope.distinct.count(:client_id) 12 | @top_traffic_sources = base 13 | .where(event_name: "page_view") 14 | .select("traffic_source, count(distinct client_id) as visitors_count, count(*) as count, count(distinct session_id) as sessions_count") 15 | .group("site_id, traffic_source").order("2 desc").limit(100) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: users 6 | # 7 | # id :bigint not null, primary key 8 | # email :string not null 9 | # email_verification_token :string 10 | # email_verified :boolean default(FALSE) 11 | # password_digest :string 12 | # remember_token :string 13 | # created_at :datetime not null 14 | # updated_at :datetime not null 15 | # 16 | # Indexes 17 | # 18 | # index_users_on_remember_token (remember_token) UNIQUE 19 | # 20 | require "test_helper" 21 | 22 | class UserTest < ActiveSupport::TestCase 23 | # test "the truth" do 24 | # assert true 25 | # end 26 | end 27 | -------------------------------------------------------------------------------- /app/controllers/traffic_campaigns_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TrafficCampaignsController < ApplicationController 4 | layout "detail" 5 | 6 | def index 7 | @site = current_user.sites.find_by!(uuid: params[:site_id]) 8 | 9 | base = QueryBuilder.call(Hyper::Event, @site, params).result 10 | current_scope = QueryBuilder.call(Hyper::Event, @site, period: "realtime").result 11 | @current_visitors_count = current_scope.distinct.count(:client_id) 12 | @top_traffic_campaigns = base 13 | .where(event_name: "page_view") 14 | .select("traffic_campaign, count(distinct client_id) as visitors_count, count(*) as count, count(distinct session_id) as sessions_count") 15 | .group("site_id, traffic_campaign").order("2 desc").limit(100) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/assets/javascripts/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, or any plugin's 5 | // 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 turbolinks 16 | //= require_tree . 17 | -------------------------------------------------------------------------------- /test/models/site_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: sites 6 | # 7 | # id :bigint not null, primary key 8 | # domain :string 9 | # public :boolean default(FALSE) 10 | # timezone :string 11 | # uuid :uuid not null 12 | # created_at :datetime not null 13 | # updated_at :datetime not null 14 | # tracking_id :string 15 | # user_id :bigint 16 | # 17 | # Indexes 18 | # 19 | # index_sites_on_tracking_id (tracking_id) UNIQUE 20 | # index_sites_on_user_id (user_id) 21 | # index_sites_on_uuid (uuid) UNIQUE 22 | # 23 | require "test_helper" 24 | 25 | class SiteTest < ActiveSupport::TestCase 26 | # test "the truth" do 27 | # assert true 28 | # end 29 | end 30 | -------------------------------------------------------------------------------- /docker/entrypoints/rails.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -x 4 | 5 | # Remove a potentially pre-existing server.pid for Rails. 6 | rm -rf /app/tmp/pids/server.pid 7 | rm -rf /app/tmp/cache/* 8 | 9 | echo "Waiting for postgres to become ready...." 10 | 11 | # Let DATABASE_URL env take presedence over individual connection params. 12 | # This is done to avoid printing the DATABASE_URL in the logs 13 | $(docker/entrypoints/helpers/pg_database_url.sh) 14 | PG_READY="pg_isready -h $POSTGRES_HOST -p $POSTGRES_PORT -U $POSTGRES_USERNAME" 15 | 16 | until $PG_READY 17 | do 18 | sleep 2; 19 | done 20 | 21 | echo "Database ready to accept connections." 22 | 23 | bundle install 24 | 25 | BUNDLE="bundle check" 26 | 27 | until $BUNDLE 28 | do 29 | sleep 2; 30 | done 31 | 32 | # Execute the main process of the container 33 | exec "$@" 34 | -------------------------------------------------------------------------------- /app/controllers/location_urls_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class LocationUrlsController < ApplicationController 4 | layout "detail" 5 | 6 | def index 7 | @site = current_user.sites.find_by!(uuid: params[:site_id]) 8 | 9 | base = QueryBuilder.call(Hyper::Event, @site, params).result 10 | current_scope = QueryBuilder.call(Hyper::Event, @site, period: "realtime").result 11 | @current_visitors_count = current_scope.distinct.count(:client_id) 12 | @top_location_urls = base 13 | .where(event_name: "page_view") 14 | .select("location_url, count(distinct client_id) as visitors_count, count(*) as count, count(distinct session_id) as sessions_count") 15 | .group("site_id, location_url").order("2 desc").limit(100) 16 | @sql = SqlFormatter.new(@top_location_urls).call 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/controllers/referrer_sources_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ReferrerSourcesController < ApplicationController 4 | layout "detail" 5 | 6 | def index 7 | @site = current_user.sites.find_by!(uuid: params[:site_id]) 8 | 9 | base = QueryBuilder.call(Hyper::Event, @site, params).result 10 | current_scope = QueryBuilder.call(Hyper::Event, @site, period: "realtime").result 11 | @current_visitors_count = current_scope.distinct.count(:client_id) 12 | @top_referrer_sources = base 13 | .where(event_name: "page_view") 14 | .where("traffic_medium != '(none)'") 15 | .select("referrer_source, count(distinct client_id) as visitors_count, count(*) as count, count(distinct session_id) as sessions_count") 16 | .group("site_id, referrer_source").order("2 desc").limit(100) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /measurement_protocol_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | curl -H "Content-Type: application/json" "localhost:8000/e63f4903-293c-408c-807f-63b49a2d7376/debug/mp/collect?measurement_id=55&api_secret=7164faca075113e13d8f8ce0d2c7da537d2f588d" -X POST --data '{"events": [], "client_id": 1, "user_properties": {}}' 3 | 4 | curl -v -H "Content-Type: application/json" "localhost:8000/e63f4903-293c-408c-807f-63b49a2d7376/mp/collect?measurement_id=55&api_secret=7164faca075113e13d8f8ce0d2c7da537d2f588d" -X POST --data '{"client_id":"client_id","events":[{"name":"add_shipping_info","params":{"coupon":"SUMMER_FUN","currency":"USD","items":[{"item_id":"SKU_12345","item_name":"jeggings","coupon":"SUMMER_FUN","discount":2.22,"affiliation":"Google Store","item_brand":"Gucci","item_category":"pants","item_variant":"Black","price":9.99,"currency":"USD"}],"shipping_tier":"Ground","value":7.77}}]}' 5 | 6 | -------------------------------------------------------------------------------- /app/views/registrations/verification.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Success!

3 |
4 | We've sent an activation link to <%= current_user.email %>. Please click on the link to activate your account. 5 |
6 |
7 | Didn't receive an email? 8 |
9 |
10 | Please check your spam folder and contact hoooopo@gmail.com if the problem persists 11 |
12 |
13 | <% if Rails.env.development? %> 14 | <%= link_to 'Verify email for development env', verify_registrations_path(token: current_user.email_verification_token) %> 15 | <% end %> 16 |
17 |
-------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :bigint not null, primary key 6 | # email :string not null 7 | # email_verification_token :string 8 | # email_verified :boolean default(FALSE) 9 | # password_digest :string 10 | # remember_token :string 11 | # created_at :datetime not null 12 | # updated_at :datetime not null 13 | # 14 | # Indexes 15 | # 16 | # index_users_on_remember_token (remember_token) UNIQUE 17 | # 18 | 19 | # This model initially had no columns defined. If you add columns to the 20 | # model remove the '{}' from the fixture names and add the columns immediately 21 | # below each fixture, per the syntax in the comments below 22 | # 23 | one: {} 24 | # column: value 25 | # 26 | two: {} 27 | # column: value 28 | -------------------------------------------------------------------------------- /benchmark/siege: -------------------------------------------------------------------------------- 1 | siege -R <(echo connection = keep-alive) -c50 -b -t 20S 'http://10.40.96.5:8000/c7f4edce-58c3-4917-8f18-a2ea6c1b93dc/g/collect?en=page_view&v=2&tid=G-JEX4JP2G1E>m=2oe161&_p=1322479532&sr=1440x900&ul=zh-cn&cid=1162070685.1609784219&dl=https%3A%2F%2Fhypercable.github.io%2Fsite%2F%3Fto%3Dget-start&dr=https%3A%2F%2Fhypercable.github.io%2Fsite%2F%3Fto%3Dlearn-more&dt=ga%20test&sid=1611145231&sct=34&seg=1&_s=1' \ 2 | -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36' \ 3 | -H 'content-type: text/plain;charset=UTF-8' \ 4 | -H 'accept: */*' \ 5 | -H 'origin: https://hypercable.github.io' \ 6 | -H 'referer: https://hypercable.github.io/' \ 7 | -H 'accept-language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7' \ 8 | -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36' -------------------------------------------------------------------------------- /config/initializers/tech_detector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TechDetector 4 | CACHE = LruRedux::Cache.new(10000) 5 | 6 | def self.detect(ua, cache = true) 7 | if cache 8 | CACHE.getset(ua) { self.detect_without_cache(ua) } 9 | else 10 | self.detect_without_cache(ua) 11 | end 12 | end 13 | 14 | def self.detect_without_cache(ua) 15 | client = DeviceDetector.new(ua) 16 | device_type = 17 | case client.device_type 18 | when "smartphone" 19 | "Mobile" 20 | when "tv" 21 | "TV" 22 | else 23 | client.device_type.try(:titleize) 24 | end 25 | 26 | { 27 | browser: client.name || "unknown", 28 | os: client.os_name || "unknown", 29 | device_type: device_type || "unknown", 30 | is_bot: client.bot? || !client.known? || client.name == "Headless Chrome" 31 | } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/fixtures/sites.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: sites 4 | # 5 | # id :bigint not null, primary key 6 | # domain :string 7 | # public :boolean default(FALSE) 8 | # timezone :string 9 | # uuid :uuid not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # tracking_id :string 13 | # user_id :bigint 14 | # 15 | # Indexes 16 | # 17 | # index_sites_on_tracking_id (tracking_id) UNIQUE 18 | # index_sites_on_user_id (user_id) 19 | # index_sites_on_uuid (uuid) UNIQUE 20 | # 21 | 22 | # This model initially had no columns defined. If you add columns to the 23 | # model remove the '{}' from the fixture names and add the columns immediately 24 | # below each fixture, per the syntax in the comments below 25 | # 26 | one: {} 27 | # column: value 28 | # 29 | two: {} 30 | # column: value 31 | -------------------------------------------------------------------------------- /diagrams/simple_architecture.py: -------------------------------------------------------------------------------- 1 | from diagrams import Cluster, Diagram 2 | from diagrams.custom import Custom 3 | 4 | with Diagram("simple_architecture", show=False): 5 | ga = Custom("Google Analytics v4", "./imgs/ga.png") 6 | redis = Custom("redis", "./imgs/redis.png") 7 | sidekiq = Custom("sidekiq", "./imgs/sidekiq.png") 8 | openresty = Custom("openresty", "./imgs/openresty.png") 9 | rails = Custom("rails", "./imgs/rails.png") 10 | postgresql = Custom("postgresql", "./imgs/postgresql.png") 11 | timescaledb = Custom("tsdb", "./imgs/TimescaleDB.png") 12 | 13 | 14 | with Cluster("Self-Service BI"): 15 | metabase = Custom("metabase", "./imgs/metabase.png") 16 | chartio = Custom("chartio", "./imgs/chartio.png") 17 | bi = [metabase, chartio] 18 | 19 | rails >> [postgresql, timescaledb] 20 | ga >> openresty >> redis >> sidekiq >> timescaledb 21 | timescaledb >> bi 22 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: unicode 4 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 25 } %> 5 | 6 | development: 7 | main: 8 | <<: *default 9 | url: <%= ENV['MAIN_DATABASE_URL'] %> 10 | migrations_paths: db/migrate 11 | hyper: 12 | <<: *default 13 | url: <%= ENV['HYPER_DATABASE_URL'] %> 14 | migrations_paths: db/hyper_migrate 15 | 16 | test: 17 | main: 18 | <<: *default 19 | url: <%= ENV['MAIN_DATABASE_URL'] %> 20 | migrations_paths: db/migrate 21 | hyper: 22 | <<: *default 23 | url: <%= ENV['HYPER_DATABASE_URL'] %> 24 | migrations_paths: db/hyper_migrate 25 | 26 | production: 27 | main: 28 | <<: *default 29 | url: <%= ENV['MAIN_DATABASE_URL'] %> 30 | migrations_paths: db/migrate 31 | hyper: 32 | <<: *default 33 | url: <%= ENV['HYPER_DATABASE_URL'] %> 34 | migrations_paths: db/hyper_migrate 35 | pool: 25 36 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a way to update your development environment automatically. 14 | # Add necessary update steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | puts "\n== Updating database ==" 24 | system! 'bin/rails db:migrate' 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! 'bin/rails log:clear tmp:clear' 28 | 29 | puts "\n== Restarting application server ==" 30 | system! 'bin/rails restart' 31 | end 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | .env 10 | .env.production 11 | 12 | # Ignore all logfiles and tempfiles. 13 | /log/* 14 | /tmp/* 15 | !/log/.keep 16 | !/tmp/.keep 17 | 18 | # Ignore pidfiles, but keep the directory. 19 | /tmp/pids/* 20 | !/tmp/pids/ 21 | !/tmp/pids/.keep 22 | 23 | # Ignore uploaded files in development. 24 | /storage/* 25 | !/storage/.keep 26 | 27 | /public/assets 28 | .byebug_history 29 | 30 | # Ignore master key for decrypting credentials and more. 31 | /config/master.key 32 | 33 | /public/packs 34 | /public/packs-test 35 | /node_modules 36 | /yarn-error.log 37 | yarn-debug.log* 38 | .yarn-integrity 39 | -------------------------------------------------------------------------------- /app/views/shared/sources/_nav.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Top Traffics

3 | 9 |
-------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/loader.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2021 Plausible. */ 2 | .loading { 3 | width: 50px; 4 | height: 50px; 5 | animation: loaderFadein .2s ease-in; 6 | } 7 | 8 | .loading.sm { 9 | width: 25px; 10 | height: 25px; 11 | } 12 | 13 | .loading div { 14 | display: inline-block; 15 | width: 50px; 16 | height: 50px; 17 | border: 3px solid #dae1e7; 18 | border-radius: 50%; 19 | border-top-color: #606f7b; 20 | animation: spin 1s ease-in-out infinite; 21 | -webkit-animation: spin 1s ease-in-out infinite; 22 | } 23 | 24 | .dark .loading div { 25 | border: 3px solid #606f7b; 26 | border-top-color: #dae1e7; 27 | } 28 | 29 | .loading.sm div { 30 | width: 25px; 31 | height: 25px; 32 | } 33 | 34 | 35 | @keyframes spin { 36 | to { -webkit-transform: rotate(360deg); } 37 | } 38 | @-webkit-keyframes spin { 39 | to { -webkit-transform: rotate(360deg); } 40 | } 41 | 42 | @keyframes loaderFadein { 43 | 0% { opacity: 0; } 44 | 50% { opacity: 0; } 45 | 100% { opacity: 1; } 46 | } 47 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/tooltip.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2021 Plausible. */ 2 | [tooltip]{ 3 | position:relative; 4 | display:inline-block; 5 | } 6 | 7 | [tooltip]::before { 8 | transition: .3s; 9 | content: ""; 10 | position: absolute; 11 | top:-6px; 12 | left:50%; 13 | transform: translateX(-50%); 14 | border-width: 4px 6px 0 6px; 15 | border-style: solid; 16 | border-color: rgba(0,0,0,0.8) transparent transparent transparent; 17 | z-index: 99; 18 | opacity:0; 19 | } 20 | 21 | [tooltip]::after { 22 | transition: .3s; 23 | white-space: nowrap; 24 | content: attr(tooltip); 25 | position: absolute; 26 | left:50%; 27 | top:-6px; 28 | transform: translateX(-50%) translateY(-100%); 29 | background: rgba(0,0,0,0.8); 30 | text-align: center; 31 | color: #fff; 32 | font-size: .875rem; 33 | min-width: 80px; 34 | max-width: 420px; 35 | border-radius: 3px; 36 | pointer-events: none; 37 | padding: 4px 8px; 38 | z-index:99; 39 | opacity:0; 40 | } 41 | 42 | [tooltip]:hover::after,[tooltip]:hover::before { 43 | opacity:1 44 | } 45 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | let environment = { 2 | plugins: [ 3 | require('tailwindcss'), 4 | require('autoprefixer'), 5 | require('postcss-import'), 6 | require('postcss-flexbugs-fixes'), 7 | require('postcss-preset-env')({ 8 | autoprefixer: { 9 | flexbox: 'no-2009' 10 | }, 11 | stage: 3 12 | }), 13 | ] 14 | } 15 | 16 | // Only run PurgeCSS in production (you can also add staging here) 17 | if (process.env.RAILS_ENV === "production") { 18 | environment.plugins.push( 19 | require('@fullhuman/postcss-purgecss')({ 20 | keyframes: true, 21 | whitelistPatterns: [ 22 | /^tagify(.*?)$/, 23 | /^bg-(gray|red|yellow|green|blue|indigo|purple|pink)-900$/, 24 | ], 25 | content: [ 26 | './app/**/*.html.erb', 27 | './app/helpers/**/*.rb', 28 | './app/javascript/**/*.js', 29 | './app/javascript/**/*.vue', 30 | './app/javascript/**/*.jsx', 31 | ], 32 | defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || [] 33 | }) 34 | ) 35 | } 36 | 37 | module.exports = environment -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | hypercable · Simple, privacy-friendly alternative to Google Analytics 9 | <%= csrf_meta_tags %> 10 | <%= csp_meta_tag %> 11 | <%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %> 12 | <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> 13 | 14 | 15 | <%= render 'layouts/nav' %> 16 |
17 |
18 | <%= render "layouts/flash" %> 19 | <%= yield %> 20 |
21 |
22 | <%= render 'layouts/footer' %> 23 | 24 | 25 | -------------------------------------------------------------------------------- /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 setup or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at anytime 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 | # Install JavaScript dependencies 21 | # system('bin/yarn') 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:prepare' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /diagrams/scale_queue.py: -------------------------------------------------------------------------------- 1 | from diagrams import Cluster, Diagram 2 | from diagrams.custom import Custom 3 | 4 | with Diagram("scale queue", show=False): 5 | ga = Custom("Google Analytics v4", "./imgs/ga.png") 6 | redis = Custom("redis", "./imgs/redis.png") 7 | 8 | openresty = Custom("openresty", "./imgs/openresty.png") 9 | rails = Custom("rails", "./imgs/rails.png") 10 | postgresql = Custom("postgresql", "./imgs/postgresql.png") 11 | timescaledb = Custom("tsdb", "./imgs/TimescaleDB.png") 12 | 13 | 14 | with Cluster("Self-Service BI"): 15 | metabase = Custom("metabase", "./imgs/metabase.png") 16 | chartio = Custom("chartio", "./imgs/chartio.png") 17 | bi = [metabase, chartio] 18 | 19 | with Cluster("Sidekiq Group"): 20 | sidekiq1 = Custom("sidekiq-1", "./imgs/sidekiq.png") 21 | sidekiq2 = Custom("sidekiq-2", "./imgs/sidekiq.png") 22 | sidekiq3 = Custom("sidekiq-3", "./imgs/sidekiq.png") 23 | sidekiq_group = [sidekiq1, sidekiq2, sidekiq3] 24 | 25 | 26 | rails >> [postgresql, timescaledb] 27 | ga >> openresty >> redis >> sidekiq_group 28 | sidekiq1 >> timescaledb 29 | sidekiq2 >> timescaledb 30 | sidekiq3 >> timescaledb 31 | timescaledb >> bi 32 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: users 6 | # 7 | # id :bigint not null, primary key 8 | # email :string not null 9 | # email_verification_token :string 10 | # email_verified :boolean default(FALSE) 11 | # password_digest :string 12 | # remember_token :string 13 | # created_at :datetime not null 14 | # updated_at :datetime not null 15 | # 16 | # Indexes 17 | # 18 | # index_users_on_remember_token (remember_token) UNIQUE 19 | # 20 | class User < ApplicationRecord 21 | has_secure_password 22 | has_many :sites 23 | has_many :goals 24 | 25 | has_many :site_members 26 | has_many :own_sites, through: :site_members, class_name: "Site" 27 | 28 | validates_format_of :email, with: /\A[^@\s]+@[^@\s]+\z/ 29 | validates :email, uniqueness: true 30 | 31 | before_create { generate_token(:remember_token) } 32 | before_create { generate_token(:email_verification_token) } 33 | 34 | def generate_token(column) 35 | begin 36 | self[column] = SecureRandom.hex(32) 37 | end while User.exists?(column => self[column]) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/controllers/registrations_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RegistrationsController < ApplicationController 4 | layout "auth" 5 | skip_before_action :authenticate_user!, only: %i[new create verify] 6 | skip_before_action :email_verify!, only: %i[new create verify verification] 7 | 8 | def new 9 | @user = User.new 10 | end 11 | 12 | def verify 13 | if user = User.where(email_verification_token: params[:token]).first 14 | user.update(email_verified: true) 15 | flash[:success] = "Email verify successed" 16 | else 17 | flash[:error] = "Email verify failed" 18 | end 19 | redirect_to new_session_path 20 | end 21 | 22 | def verification 23 | end 24 | 25 | def create 26 | @user = User.new(user_params) 27 | if @user.save 28 | cookies[:remember_token] = { 29 | value: @user.remember_token, 30 | expires: 1.year.from_now.utc 31 | } 32 | VerificationMailer.with(user: @user).verify.deliver_later 33 | redirect_to verification_registrations_path 34 | else 35 | flash[:error] = t("sign_up_failed") 36 | render :new 37 | end 38 | end 39 | 40 | def user_params 41 | params.require(:user).permit(:email, :password) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/models/site_connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: site_connections 6 | # 7 | # id :bigint not null, primary key 8 | # password :string not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # site_id :bigint 12 | # 13 | # Indexes 14 | # 15 | # index_site_connections_on_site_id (site_id) 16 | # 17 | class SiteConnection < ApplicationRecord 18 | belongs_to :site 19 | 20 | before_create do 21 | username = site.uuid 22 | self.password = SecureRandom.hex(10) 23 | ApplicationHyperRecord.connection.execute(%Q[CREATE USER "#{username}" WITH PASSWORD '#{password}']) 24 | ApplicationHyperRecord.connection.execute(%Q[GRANT readonly TO "#{username}"]) 25 | ApplicationHyperRecord.connection.execute(%Q[ALTER ROLE "#{username}" SET statement_timeout=10000]) 26 | end 27 | 28 | before_destroy do 29 | ApplicationHyperRecord.connection.execute(%Q[DROP USER IF EXISTS "#{site.uuid}"]) 30 | end 31 | 32 | # TODO replace host & port 33 | def link 34 | "postgresql://#{site.uuid}:#{password}@#{ENV["HOST"].to_s.split(":").first}:5532/#{ApplicationHyperRecord.connection.current_database}" 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors') 2 | 3 | module.exports = { 4 | purge: [ 5 | ], 6 | darkMode: 'class', 7 | theme: { 8 | container: { 9 | center: true, 10 | padding: '1rem', 11 | }, 12 | extend: { 13 | colors: { 14 | orange: colors.orange, 15 | 'gray-850': 'rgb(26, 32, 44)', 16 | 'gray-825': 'rgb(37, 47, 63)' 17 | }, 18 | spacing: { 19 | '44': '11rem' 20 | }, 21 | width: { 22 | '31percent': '31%', 23 | }, 24 | opacity: { 25 | '15': '0.15', 26 | }, 27 | zIndex: { 28 | '9': 9, 29 | }, 30 | maxWidth: { 31 | '2xs': '16rem', 32 | } 33 | }, 34 | }, 35 | variants: { 36 | textColor: ['responsive', 'hover', 'focus', 'group-hover'], 37 | display: ['responsive', 'hover', 'focus', 'group-hover'], 38 | extend: { 39 | textColor: ['dark'], 40 | borderWidth: ['dark'], 41 | backgroundOpacity: ['dark'], 42 | display: ['dark'], 43 | cursor: ['hover'], 44 | justifyContent: ['responsive'] 45 | } 46 | }, 47 | plugins: [ 48 | require('@tailwindcss/forms'), 49 | require('@tailwindcss/typography'), 50 | require('@tailwindcss/aspect-ratio'), 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SessionsController < ApplicationController 4 | layout "auth" 5 | skip_before_action :authenticate_user!, only: %i[new create] 6 | skip_before_action :email_verify!, only: %i[new create] 7 | 8 | def new 9 | end 10 | 11 | def create 12 | @user = User.where(email: user_params[:email]).first 13 | if @user&.authenticate(user_params[:password]) 14 | set_current_user(@user, true) 15 | 16 | if @user.email_verified? 17 | flash[:success] = "Login successfully" 18 | if @user.sites.count == 0 19 | redirect_to new_site_path 20 | elsif @user.sites.count == 1 21 | redirect_to site_path(@user.sites.first) 22 | else 23 | redirect_to sites_path 24 | end 25 | else 26 | redirect_to verification_registrations_path, error: "Please verify your email" 27 | end 28 | else 29 | flash.now[:error] = "Email or password is wrong" 30 | render "new" 31 | end 32 | end 33 | 34 | def destroy 35 | cookies[:remember_token] = { 36 | value: nil 37 | } 38 | redirect_to root_path 39 | end 40 | 41 | def user_params 42 | params.require(:user).permit(:email, :password, :remember_me) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/models/buffer_queue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class BufferQueue 4 | attr_reader :max_batch_size, :execution_interval, :timeout_interval, :callback 5 | def initialize(max_batch_size: 100, execution_interval: 60, timeout_interval: 60, &callback) 6 | @max_batch_size = max_batch_size 7 | @execution_interval = execution_interval 8 | @timeout_interval = timeout_interval 9 | @queue = Queue.new 10 | @timer = Concurrent::TimerTask.new(execution_interval: execution_interval, timeout_interval: timeout_interval) do 11 | flush 12 | end 13 | @timer.execute 14 | @callback = callback 15 | at_exit { shutdown } 16 | end 17 | 18 | def flush 19 | batch = [] 20 | max_batch_size.times do 21 | if not @queue.empty? 22 | begin 23 | batch << @queue.pop(true) 24 | rescue ThreadError 25 | puts "queue is empty" 26 | break 27 | end 28 | else 29 | break 30 | end 31 | end 32 | callback.call(batch) unless batch.empty? 33 | end 34 | 35 | def push(item) 36 | @queue << item 37 | if @queue.size >= max_batch_size 38 | flush 39 | end 40 | item 41 | end 42 | 43 | def shutdown 44 | puts "shutdown ..." 45 | @timer.shutdown 46 | flush 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/views/layouts/flash/_success.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 |
8 |
9 |

10 | <%= flash[:success] %> 11 |

12 |
13 |
14 |
15 | 20 |
21 |
22 |
23 |
-------------------------------------------------------------------------------- /app/views/layouts/flash/_info.html.erb: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 | 7 | 8 |
9 |
10 |

11 | <%= flash[:info] %> 12 |

13 |
14 |
15 |
16 | 21 |
22 |
23 |
24 |
-------------------------------------------------------------------------------- /app/views/oses/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Top Operating Systems

3 |
4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @top_oses.each do |agg| %> 18 | 19 | 20 | 21 | 22 | 23 | 24 | <% end %> 25 | 26 |
OSVisitorsPageviewsSessions
<%= agg.os %><%= pretty_num agg.visitors_count %><%= pretty_num agg.count %><%= pretty_num agg.sessions_count %>
27 |
28 |
29 | -------------------------------------------------------------------------------- /diagrams/scale_collector.py: -------------------------------------------------------------------------------- 1 | from diagrams import Cluster, Diagram 2 | from diagrams.custom import Custom 3 | from diagrams.aws.network import ELB 4 | 5 | with Diagram("scale collector", show=False): 6 | ga = Custom("Google Analytics v4", "./imgs/ga.png") 7 | lb = ELB("lb") 8 | timescaledb = Custom("tsdb", "./imgs/TimescaleDB.png") 9 | bi = Custom("metabase", "./imgs/metabase.png") 10 | 11 | with Cluster("collector"): 12 | openresty = Custom("openresty", "./imgs/openresty.png") 13 | redis = Custom("redis", "./imgs/redis.png") 14 | sidekiq = Custom("sidekiq", "./imgs/sidekiq.png") 15 | collector_group = openresty >> redis >> sidekiq 16 | 17 | with Cluster("collector1"): 18 | openresty1 = Custom("openresty", "./imgs/openresty.png") 19 | redis1 = Custom("redis", "./imgs/redis.png") 20 | sidekiq1 = Custom("sidekiq", "./imgs/sidekiq.png") 21 | collector_group1 = openresty1 >> redis1 >> sidekiq1 22 | 23 | with Cluster("collector2"): 24 | openresty2 = Custom("openresty", "./imgs/openresty.png") 25 | redis2 = Custom("redis", "./imgs/redis.png") 26 | sidekiq2 = Custom("sidekiq", "./imgs/sidekiq.png") 27 | collector_group2 = openresty2 >> redis2 >> sidekiq2 28 | 29 | ga >> lb 30 | lb >> openresty 31 | lb >> openresty1 32 | lb >> openresty2 33 | [collector_group, collector_group1, collector_group2] >> timescaledb >> bi 34 | -------------------------------------------------------------------------------- /app/views/browsers/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Top Browsers

3 |
4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @top_browsers.each do |agg| %> 18 | 19 | 20 | 21 | 22 | 23 | 24 | <% end %> 25 | 26 |
BrowserVisitorsPageviewsSessions
<%= agg.browser %><%= pretty_num agg.visitors_count %><%= pretty_num agg.count %><%= pretty_num agg.sessions_count %>
27 |
28 |
29 | -------------------------------------------------------------------------------- /app/views/device_types/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Top Device Types

3 |
4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @top_device_types.each do |agg| %> 18 | 19 | 20 | 21 | 22 | 23 | 24 | <% end %> 25 | 26 |
Device TypeVisitorsPageviewsSessions
<%= agg.device_type %><%= pretty_num agg.visitors_count %><%= pretty_num agg.count %><%= pretty_num agg.sessions_count %>
27 |
28 |
29 | -------------------------------------------------------------------------------- /app/views/countries/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Top Countris or Regions

3 |
4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @top_countries.each do |agg| %> 18 | 19 | 20 | 21 | 22 | 23 | 24 | <% end %> 25 | 26 |
Country or regionVisitorsPageviewsSessions
<%= agg.country %><%= pretty_num agg.visitors_count %><%= pretty_num agg.count %><%= pretty_num agg.sessions_count %>
27 |
28 |
29 | -------------------------------------------------------------------------------- /app/views/layouts/flash/_error.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 |
8 |
9 |

10 | <%= flash[:error]%> 11 |

12 |
13 |
14 |
15 | 20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /app/javascript/stylesheets/modal.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2021 Plausible. */ 2 | .modal { 3 | display: none; 4 | } 5 | 6 | .modal.is-open { 7 | display: block; 8 | } 9 | 10 | .modal[aria-hidden="false"] .modal__overlay { 11 | animation: mmfadeIn .2s ease-in; 12 | } 13 | 14 | .modal[aria-hidden="true"] .modal__overlay { 15 | animation: mmfadeOut .2s ease-in; 16 | } 17 | 18 | .modal-enter { 19 | opacity: 0; 20 | } 21 | 22 | .modal-enter-active { 23 | opacity: 1; 24 | transition: opacity 100ms ease-in; 25 | } 26 | 27 | .modal__overlay { 28 | position: fixed; 29 | top: 0; 30 | left: 0; 31 | right: 0; 32 | bottom: 0; 33 | background: rgba(0,0,0,0.6); 34 | z-index: 99; 35 | overflow-x: hidden; 36 | overflow-y: auto; 37 | } 38 | 39 | .modal__container { 40 | background-color: #fff; 41 | padding: 1rem 2rem; 42 | max-width: 860px; 43 | border-radius: 4px; 44 | margin: 50px auto; 45 | box-sizing: border-box; 46 | min-height: 509px; 47 | transition: height 200ms ease-in; 48 | } 49 | 50 | .modal__close { 51 | position: fixed; 52 | color: #b8c2cc; 53 | font-size: 48px; 54 | font-weight: bold; 55 | top: 12px; 56 | right: 24px; 57 | } 58 | 59 | .modal__close:before { content: "\2715"; } 60 | 61 | .modal__content { 62 | margin-bottom: 2rem; 63 | } 64 | 65 | @keyframes mmfadeIn { 66 | from { opacity: 0; } 67 | to { opacity: 1; } 68 | } 69 | 70 | @keyframes mmfadeOut { 71 | from { opacity: 1; } 72 | to { opacity: 0; } 73 | } 74 | -------------------------------------------------------------------------------- /app/views/traffic_mediums/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Top Traffic Medium

3 |
4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @top_traffic_mediums.each do |agg| %> 18 | 19 | 20 | 21 | 22 | 23 | 24 | <% end %> 25 | 26 |
Traffic MediumVisitorsPageviewsSessions
<%= agg.traffic_medium %><%= pretty_num agg.visitors_count %><%= pretty_num agg.count %><%= pretty_num agg.sessions_count %>
27 |
28 |
29 | -------------------------------------------------------------------------------- /app/views/traffic_sources/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Top Traffic Source

3 |
4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @top_traffic_sources.each do |agg| %> 18 | 19 | 20 | 21 | 22 | 23 | 24 | <% end %> 25 | 26 |
Traffic SourceVisitorsPageviewsSessions
<%= agg.traffic_source %><%= pretty_num agg.visitors_count %><%= pretty_num agg.count %><%= pretty_num agg.sessions_count %>
27 |
28 |
29 | -------------------------------------------------------------------------------- /app/views/traffic_campaigns/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Top Traffic Campaign

3 |
4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @top_traffic_campaigns.each do |agg| %> 18 | 19 | 20 | 21 | 22 | 23 | 24 | <% end %> 25 | 26 |
Traffic CampaignVisitorsPageviewsSessions
<%= agg.traffic_campaign %><%= pretty_num agg.visitors_count %><%= pretty_num agg.count %><%= pretty_num agg.sessions_count %>
27 |
28 |
29 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | root "sessions#new" 5 | resources :sessions, only: [:create, :new] 6 | delete "sign_out", to: "sessions#destroy", as: :destroy_session 7 | resources :registrations, only: [:create, :new] do 8 | collection do 9 | get "verification" 10 | get "verify" 11 | end 12 | end 13 | resources :sites do 14 | resources :location_urls, only: [:index] 15 | resources :countries, only: [:index] 16 | resources :referrer_sources, only: [:index] 17 | resources :traffic_mediums, only: [:index] 18 | resources :traffic_sources, only: [:index] 19 | resources :traffic_campaigns, only: [:index] 20 | resources :browsers, only: [:index] 21 | resources :device_types, only: [:index] 22 | resources :oses, only: [:index] 23 | 24 | resources :site_connections, only: [:index, :new, :show, :destroy, :create] 25 | resources :measurement_protocols, only: [:index, :new, :show, :destroy, :create] 26 | member do 27 | get "snippet" 28 | get "debug" 29 | end 30 | resources :settings, only: %i[index] do 31 | collection do 32 | get "general" 33 | patch "update_general" 34 | end 35 | 36 | collection do 37 | get "visibility" 38 | patch "update_visibility" 39 | end 40 | end 41 | end 42 | # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html 43 | end 44 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | before_action :authenticate_user! 5 | before_action :email_verify! 6 | 7 | helper_method :authenticate_user!, :current_user, :user_signed_in?, :default_host, :email_verify! 8 | 9 | def current_user 10 | @current_user ||= User.where(remember_token: cookies[:remember_token]).first if cookies[:remember_token] 11 | end 12 | 13 | protected 14 | 15 | def default_host 16 | Rails.application.routes.default_url_options[:host] 17 | end 18 | 19 | def authenticate_user! 20 | redirect_to new_session_path, error: t("login_required") unless current_user 21 | end 22 | 23 | def email_verify! 24 | if current_user 25 | redirect_to verification_registrations_path, error: "Please verify your email" unless current_user.email_verified? 26 | else 27 | redirect_to new_session_path, error: t("login_required") 28 | end 29 | end 30 | 31 | def set_current_user(user, remember_me = true) 32 | cookies[:remember_token] = if remember_me 33 | { 34 | value: user.remember_token, 35 | expires: 1.year.from_now.utc 36 | } 37 | else 38 | { 39 | value: user.remember_token 40 | } 41 | end 42 | @current_user = user 43 | @current_user 44 | end 45 | 46 | def user_signed_in? 47 | !!current_user 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/views/location_urls/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Top Urls

3 | <%= render partial: "shared/sql" %> 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% @top_location_urls.each do |url| %> 16 | 17 | 18 | 19 | 20 | 21 | 22 | <% end %> 23 | 24 |
Location urlVisitorsPageviewsSessions
<%= url.location_url %><%= pretty_num url.visitors_count %><%= pretty_num url.count %><%= pretty_num url.sessions_count %>
25 |
26 |
27 | -------------------------------------------------------------------------------- /test_request.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | curl 'http://localhost:8000/ahoy/events' \ 4 | -X POST \ 5 | -H 'Connection: keep-alive' \ 6 | -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36' \ 7 | -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundarycF1PzwWBXs6VuEz7' \ 8 | -H 'Accept: */*' \ 9 | -H 'Origin: http://localhost:3000' \ 10 | -H 'Sec-Fetch-Site: same-site' \ 11 | -H 'Sec-Fetch-Mode: no-cors' \ 12 | -H 'Sec-Fetch-Dest: empty' \ 13 | -H 'Referer: http://localhost:3000/' \ 14 | -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7' \ 15 | -H 'Cookie: a=1' \ 16 | -H 'X-Forwarded-For: 52.77.234.3' \ 17 | --data-binary $'------WebKitFormBoundarycF1PzwWBXs6VuEz7\r\nContent-Disposition: form-data; name="visit_token"\r\n\r\n4f989f5d-1a8a-412f-baac-479bd701e6d8\r\n------WebKitFormBoundarycF1PzwWBXs6VuEz7\r\nContent-Disposition: form-data; name="visitor_token"\r\n\r\n25bfcf36-5859-4c8f-ba17-b5a74b9072db\r\n------WebKitFormBoundarycF1PzwWBXs6VuEz7\r\nContent-Disposition: form-data; name="authenticity_token"\r\n\r\nIybjvw2tO+p+MN7wLltaCoOAGboIPKqVOjLunVK0+7YUP83j9xM1U+p13Yc4RAPrqlb/Xcj5+nmKJWR8CiiHOA==\r\n------WebKitFormBoundarycF1PzwWBXs6VuEz7\r\nContent-Disposition: form-data; name="events_json"\r\n\r\n[{"name":"$view","properties":{"url":"http://localhost:3000/bookmarks","title":"极客分享","page":"/bookmarks"},"time":1608823797.563,"id":"d66dfce2-1277-4d58-8679-b9a1595f2b3e","js":true}]\r\n------WebKitFormBoundarycF1PzwWBXs6VuEz7--\r\n' -------------------------------------------------------------------------------- /app/views/settings/_nav.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 8 |
9 | 22 |
-------------------------------------------------------------------------------- /app/views/layouts/auth.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= csrf_meta_tags %> 9 | <%= csp_meta_tag %> 10 | <%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %> 11 | <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> 12 | 13 | 14 | Hypercable · Web analytics 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | Hypercable logo 24 | 25 |
26 | <%= render "layouts/flash" %> 27 | <%= yield %> 28 |

29 | ©2020 Hypercable Analytics. All rights reserved. The UI section modified from Plausible. 30 |

31 | 32 | 33 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Define an application-wide content security policy 5 | # For further information see the following documentation 6 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 7 | 8 | # Rails.application.config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # If you are using webpack-dev-server then specify webpack-dev-server host 16 | # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? 17 | 18 | # # Specify URI for violation reports 19 | # # policy.report_uri "/csp-violation-report-endpoint" 20 | # end 21 | 22 | # If you are using UJS then enable automatic nonce generation 23 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 24 | 25 | # Set the nonce only to specific directives 26 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 27 | 28 | # Report CSP violations to a specified URI 29 | # For further information see the following documentation: 30 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 31 | # Rails.application.config.content_security_policy_report_only = true 32 | -------------------------------------------------------------------------------- /app/models/site.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: sites 6 | # 7 | # id :bigint not null, primary key 8 | # domain :string 9 | # public :boolean default(FALSE) 10 | # timezone :string 11 | # uuid :uuid not null 12 | # created_at :datetime not null 13 | # updated_at :datetime not null 14 | # tracking_id :string 15 | # user_id :bigint 16 | # 17 | # Indexes 18 | # 19 | # index_sites_on_tracking_id (tracking_id) UNIQUE 20 | # index_sites_on_user_id (user_id) 21 | # index_sites_on_uuid (uuid) UNIQUE 22 | # 23 | class Site < ApplicationRecord 24 | belongs_to :user 25 | has_many :goals 26 | 27 | has_many :site_members 28 | has_many :members, through: :site_members, class_name: "User" 29 | 30 | has_many :shared_links 31 | has_many :site_connections 32 | has_many :measurement_protocols 33 | 34 | validates :domain, presence: true 35 | validates :tracking_id, presence: true 36 | 37 | def to_param 38 | uuid 39 | end 40 | 41 | def utc_offset 42 | # Beijing -> 28800 43 | ActiveSupport::TimeZone[timezone].utc_offset 44 | end 45 | 46 | def utc_offset_hours 47 | # Beijing -> 8 48 | utc_offset / 60 / 60.0 49 | end 50 | 51 | def utc_offset_string 52 | # Beijing -> +08:00 53 | ActiveSupport::TimeZone[timezone].formatted_offset 54 | end 55 | 56 | def visitors_count_of_24h 57 | current_range = (1.days.ago.in_time_zone(timezone).to_s(:db)..) 58 | Hyper::Event.where(site_id: uuid).where(started_at: current_range).distinct.count(:client_id) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/views/sites/debug.html.erb: -------------------------------------------------------------------------------- 1 | 2 |
3 | 36 |
37 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "boot" 4 | 5 | require "rails" 6 | # Pick the frameworks you want: 7 | require "active_model/railtie" 8 | require "active_job/railtie" 9 | require "active_record/railtie" 10 | require "active_storage/engine" 11 | require "action_controller/railtie" 12 | require "action_mailer/railtie" 13 | # require "action_mailbox/engine" 14 | require "action_text/engine" 15 | require "action_view/railtie" 16 | require "action_cable/engine" 17 | require "sprockets/railtie" 18 | require "rails/test_unit/railtie" 19 | 20 | # Require the gems listed in Gemfile, including any gems 21 | # you've limited to :test, :development, or :production. 22 | Bundler.require(*Rails.groups) 23 | 24 | Dotenv::Railtie.load 25 | 26 | module HypercableWeb 27 | class Application < Rails::Application 28 | # Initialize configuration defaults for originally generated Rails version. 29 | config.load_defaults 6.0 30 | 31 | # Settings in config/environments/* take precedence over those specified here. 32 | # Application configuration can go into files in config/initializers 33 | # -- all .rb files in that directory are automatically loaded after loading 34 | # the framework and any gems in your application. 35 | config.active_record.schema_format = :sql 36 | config.action_mailer.perform_deliveries = true 37 | 38 | config.action_mailer.smtp_settings = { 39 | address: ENV["SMTP_SERVER"], 40 | user_name: ENV["SMTP_USER"], 41 | 42 | password: ENV["SMTP_PASSWORD"], 43 | port: ENV["SMTP_PORT"] || 25 44 | } 45 | 46 | config.action_mailer.delivery_method = :smtp 47 | config.logger = ActiveSupport::Logger.new(STDOUT) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/views/referrer_sources/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Top Referrer Source

3 |
4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @top_referrer_sources.each do |agg| %> 18 | 19 | 26 | 27 | 28 | 29 | 30 | <% end %> 31 | 32 |
Referrer SourceVisitorsPageviewsSessions
20 | 21 | 22 | <%= agg.referrer_source %> 23 | 24 | 25 | <%= pretty_num agg.visitors_count %><%= pretty_num agg.count %><%= pretty_num agg.sessions_count %>
33 |
34 |
35 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Puma can serve each request in a thread from an internal thread pool. 4 | # The `threads` method setting takes two numbers: a minimum and maximum. 5 | # Any libraries that use thread pools should be configured to match 6 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 7 | # and maximum; this matches the default thread size of Active Record. 8 | # 9 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 10 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 11 | threads min_threads_count, max_threads_count 12 | 13 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 14 | # 15 | port ENV.fetch("PORT") { 3000 } 16 | 17 | # Specifies the `environment` that Puma will run in. 18 | # 19 | environment ENV.fetch("RAILS_ENV") { "development" } 20 | 21 | # Specifies the `pidfile` that Puma will use. 22 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 23 | 24 | # Specifies the number of `workers` to boot in clustered mode. 25 | # Workers are forked web server processes. If using threads and workers together 26 | # the concurrency of the application would be max `threads` * `workers`. 27 | # Workers do not work on JRuby or Windows (both of which do not support 28 | # processes). 29 | # 30 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 31 | 32 | # Use the `preload_app!` method when specifying a `workers` number. 33 | # This directive tells Puma to first boot the application and load code 34 | # before forking the application. This takes advantage of Copy On Write 35 | # process behavior so workers use less memory. 36 | # 37 | # preload_app! 38 | 39 | # Allow puma to be restarted by `rails restart` command. 40 | plugin :tmp_restart 41 | -------------------------------------------------------------------------------- /app/views/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | <%= form_for(User.new, url: sessions_path, html: {method: :post, class: "mt-8"}) do |f| %> 4 | 5 |

Enter your email and password

6 | 7 |
8 | <%= f.label :email, class: "block text-gray-700 dark:text-gray-300 text-sm font-bold mb-2" %> 9 | <%= f.email_field :email, class: "bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal appearance-none focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500", placeholder: "user@example.com" %> 10 |
11 | 12 |
13 | <%= f.label :password, class: "block text-gray-700 dark:text-gray-300 text-sm font-bold mb-2" %> 14 | <%= f.password_field :password, class: "transition bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal appearance-none focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500" %> 15 |

Forgot password? Click here to reset it.

16 |
17 | 18 | <%= f.button "Login→", class: "button mt-4 w-full", type: :submit, data: {disable_with: disable_with_spinner('Login →')} %> 19 | 20 |

21 | Don't have an account? Register instead. 22 |

23 | 24 | <% end %> 25 |
-------------------------------------------------------------------------------- /app/views/shared/_date_filter.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
<%= time_range_names[params[:period]] %> 4 | 5 | 6 |
7 | 14 |
15 |
-------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/hyper_migrate/20210213182643_create_events.rb: -------------------------------------------------------------------------------- 1 | class CreateEvents < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :events, id: false do |t| 4 | t.string :event_name, default: 'page_view' 5 | t.string :site_id, null: false 6 | 7 | t.string :session_id, null: false 8 | t.string :client_id, null: false 9 | t.string :user_id 10 | t.string :tracking_id, null: false 11 | 12 | t.string :protocol_version, default: '2' 13 | t.string :data_source, default: 'web' 14 | 15 | t.boolean :session_engagement, default: false 16 | t.integer :engagement_time 17 | t.integer :session_count 18 | t.integer :request_number 19 | 20 | # standard 21 | t.string :location_url 22 | t.string :hostname 23 | t.string :path 24 | t.string :title 25 | t.string :user_agent 26 | t.string :ip 27 | t.string :referrer 28 | t.string :referrer_source 29 | t.string :screen_resolution 30 | t.string :user_language 31 | 32 | # location 33 | t.string :country 34 | t.string :region 35 | t.string :city 36 | t.float :latitude 37 | t.float :longitude 38 | 39 | # utm parameters 40 | t.string :utm_source 41 | t.string :utm_medium 42 | t.string :utm_term 43 | t.string :utm_content 44 | t.string :utm_campaign 45 | 46 | # technology 47 | t.string :browser 48 | t.string :os 49 | t.string :device_type 50 | 51 | t.jsonb :user_props, default: {} 52 | t.jsonb :event_props, default: {} 53 | 54 | t.boolean :non_interaction_hit, default: false 55 | 56 | t.datetime :started_at, null: false 57 | 58 | t.jsonb :raw_event, default: {} 59 | end 60 | 61 | execute "SELECT create_hypertable('events', 'started_at');" 62 | add_index :events, [:site_id, :session_id, :started_at], order: {started_at: :desc} 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /deploy/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name .learnsql.io; 4 | return 301 https://$host$1$request_uri; 5 | } 6 | 7 | server { 8 | server_name .learnsql.io; 9 | listen 443 ssl http2; 10 | ssl_certificate /usr/local/share/ca-certificates/learnsql.io/full_chain.pem; 11 | ssl_certificate_key /usr/local/share/ca-certificates/learnsql.io/private.key; 12 | 13 | client_max_body_size 4G; 14 | keepalive_timeout 10; 15 | 16 | charset utf-8; 17 | real_ip_header X-Forwarded-For; 18 | 19 | location ~ g/collect$ { 20 | proxy_set_header Host $host; 21 | proxy_set_header X-Real-IP $remote_addr; 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 23 | proxy_set_header Origin http://$Host; 24 | proxy_pass http://127.0.0.1:8000; 25 | proxy_read_timeout 90; 26 | 27 | proxy_redirect http://127.0.0.1:8000 https://learnsql.io; 28 | access_log /var/log/nginx/access.log; 29 | error_log /var/log/nginx/error.log; 30 | } 31 | 32 | 33 | location / { 34 | proxy_set_header Host $host; 35 | proxy_set_header X-Real-IP $remote_addr; 36 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 37 | # proxy_set_header X-Forwarded-Proto $scheme; 38 | # add_header Access-Control-Allow-Origin *; 39 | # add_header Access-Control-Allow-Methods "POST, GET, OPTIONS"; 40 | # add_header Access-Control-Allow-Headers "Origin, Authorization, Accept"; 41 | # add_header Access-Control-Allow-Credentials true; 42 | 43 | # Fix the “It appears that your reverse proxy set up is broken" error. 44 | proxy_set_header Origin http://$Host; 45 | proxy_pass http://127.0.0.1:3333; 46 | proxy_read_timeout 90; 47 | 48 | proxy_redirect http://127.0.0.1:3333 https://learnsql.io; 49 | access_log /var/log/nginx/access.log; 50 | error_log /var/log/nginx/error.log; 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | // This file is automatically compiled by Webpack, along with any other files 2 | // present in this directory. You're encouraged to place your actual application logic in 3 | // a relevant structure within app/javascript and only use these pack files to reference 4 | // that code so it'll be compiled. 5 | 6 | require("@rails/ujs").start() 7 | require("turbolinks").start() 8 | require("@rails/activestorage").start() 9 | require("channels") 10 | 11 | require("chartkick") 12 | require("chart.js") 13 | 14 | 15 | // Uncomment to copy all static images under ../images to the output folder and reference 16 | // them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>) 17 | // or the `imagePath` JavaScript helper below. 18 | // 19 | // const images = require.context('../images', true) 20 | // const imagePath = (name) => images(name, true) 21 | 22 | import "controllers" 23 | import "../stylesheets/application" 24 | 25 | import 'alpinejs' 26 | 27 | document.addEventListener("turbolinks:load", function () { 28 | const triggers = document.querySelectorAll('[data-dropdown-trigger]') 29 | 30 | for (const trigger of triggers) { 31 | trigger.addEventListener('click', function(e) { 32 | e.stopPropagation() 33 | e.currentTarget.nextElementSibling.classList.remove('hidden') 34 | }) 35 | } 36 | 37 | if (triggers.length > 0) { 38 | document.addEventListener('click', function(e) { 39 | const dropdown = e.target.closest('[data-dropdown]') 40 | 41 | if (dropdown && e.target.tagName === 'A') { 42 | dropdown.classList.add('hidden') 43 | } 44 | }) 45 | 46 | document.addEventListener('click', function(e) { 47 | const clickedInDropdown = e.target.closest('[data-dropdown]') 48 | 49 | if (!clickedInDropdown) { 50 | for (const dropdown of document.querySelectorAll('[data-dropdown]')) { 51 | dropdown.classList.add('hidden') 52 | } 53 | } 54 | }) 55 | } 56 | }) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | var validEnv = ['development', 'test', 'production'] 3 | var currentEnv = api.env() 4 | var isDevelopmentEnv = api.env('development') 5 | var isProductionEnv = api.env('production') 6 | var isTestEnv = api.env('test') 7 | 8 | if (!validEnv.includes(currentEnv)) { 9 | throw new Error( 10 | 'Please specify a valid `NODE_ENV` or ' + 11 | '`BABEL_ENV` environment variables. Valid values are "development", ' + 12 | '"test", and "production". Instead, received: ' + 13 | JSON.stringify(currentEnv) + 14 | '.' 15 | ) 16 | } 17 | 18 | return { 19 | presets: [ 20 | isTestEnv && [ 21 | '@babel/preset-env', 22 | { 23 | targets: { 24 | node: 'current' 25 | } 26 | } 27 | ], 28 | (isProductionEnv || isDevelopmentEnv) && [ 29 | '@babel/preset-env', 30 | { 31 | forceAllTransforms: true, 32 | useBuiltIns: 'entry', 33 | corejs: 3, 34 | modules: false, 35 | exclude: ['transform-typeof-symbol'] 36 | } 37 | ] 38 | ].filter(Boolean), 39 | plugins: [ 40 | 'babel-plugin-macros', 41 | '@babel/plugin-syntax-dynamic-import', 42 | isTestEnv && 'babel-plugin-dynamic-import-node', 43 | '@babel/plugin-transform-destructuring', 44 | [ 45 | '@babel/plugin-proposal-class-properties', 46 | { 47 | loose: true 48 | } 49 | ], 50 | [ 51 | '@babel/plugin-proposal-object-rest-spread', 52 | { 53 | useBuiltIns: true 54 | } 55 | ], 56 | [ 57 | '@babel/plugin-transform-runtime', 58 | { 59 | helpers: false, 60 | regenerator: true, 61 | corejs: false 62 | } 63 | ], 64 | [ 65 | '@babel/plugin-transform-regenerator', 66 | { 67 | async: false 68 | } 69 | ] 70 | ].filter(Boolean) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults_5_2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | # 4 | # This file contains migration options to ease your Rails 5.2 upgrade. 5 | # 6 | # Once upgraded flip defaults one by one to migrate to the new default. 7 | # 8 | # Read the Guide for Upgrading Ruby on Rails for more info on each option. 9 | 10 | # Make Active Record use stable #cache_key alongside new #cache_version method. 11 | # This is needed for recyclable cache keys. 12 | # Rails.application.config.active_record.cache_versioning = true 13 | 14 | # Use AES-256-GCM authenticated encryption for encrypted cookies. 15 | # Also, embed cookie expiry in signed or encrypted cookies for increased security. 16 | # 17 | # This option is not backwards compatible with earlier Rails versions. 18 | # It's best enabled when your entire app is migrated and stable on 5.2. 19 | # 20 | # Existing cookies will be converted on read then written with the new scheme. 21 | # Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true 22 | 23 | # Use AES-256-GCM authenticated encryption as default cipher for encrypting messages 24 | # instead of AES-256-CBC, when use_authenticated_message_encryption is set to true. 25 | # Rails.application.config.active_support.use_authenticated_message_encryption = true 26 | 27 | # Add default protection from forgery to ActionController::Base instead of in 28 | # ApplicationController. 29 | # Rails.application.config.action_controller.default_protect_from_forgery = true 30 | 31 | # Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and 32 | # 'f' after migrating old data. 33 | # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true 34 | 35 | # Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. 36 | # Rails.application.config.active_support.use_sha1_digests = true 37 | 38 | # Make `form_with` generate id attributes for any generated HTML tags. 39 | # Rails.application.config.action_view.form_with_generates_ids = true 40 | -------------------------------------------------------------------------------- /test/fixtures/events.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: events 4 | # 5 | # browser :string 6 | # city :string 7 | # country :string 8 | # data_source :string default("web") 9 | # device_type :string 10 | # engagement_time :integer 11 | # event_name :string default("page_view") 12 | # event_props :jsonb 13 | # hostname :string 14 | # ip :string 15 | # latitude :float 16 | # location_url :string 17 | # longitude :float 18 | # os :string 19 | # path :string 20 | # protocol_version :string default("2") 21 | # raw_event :jsonb 22 | # referrer :string 23 | # referrer_source :string 24 | # region :string 25 | # request_number :integer 26 | # request_params :jsonb 27 | # screen_resolution :string 28 | # session_count :integer 29 | # session_engagement :boolean default(FALSE) 30 | # started_at :datetime not null 31 | # title :string 32 | # traffic_campaign :string 33 | # traffic_medium :string 34 | # traffic_source :string 35 | # user_agent :string 36 | # user_language :string 37 | # user_props :jsonb 38 | # client_id :string not null 39 | # session_id :string not null 40 | # site_id :string not null 41 | # tracking_id :string not null 42 | # user_id :string 43 | # 44 | # Indexes 45 | # 46 | # events_started_at_idx (started_at) 47 | # index_events_on_site_id_and_session_id_and_started_at (site_id,session_id,started_at DESC) 48 | # 49 | 50 | # This model initially had no columns defined. If you add columns to the 51 | # model remove the '{}' from the fixture names and add the columns immediately 52 | # below each fixture, per the syntax in the comments below 53 | # 54 | one: {} 55 | # column: value 56 | # 57 | two: {} 58 | # column: value 59 | -------------------------------------------------------------------------------- /app/views/shared/sources/_traffic_medium.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= render partial: 'shared/sources/nav' %> 4 |
Traffic MediumVisitors 5 |
6 |
7 | <% @top_traffic_mediums.each do |source| %> 8 | <% @max_medium ||= source.count %> 9 |
10 |
11 |
12 | 13 |
14 | 15 | <%= link_to site_path(@site, request.query_parameters.merge(traffic_medium: source.traffic_medium)), class: 'block truncate hover:underline' do %> 16 | <%= source.traffic_medium %> 17 | <% end%> 18 | 19 |
<%= pretty_num(source.count) %> 20 |
21 | <% end %> 22 | 23 |
24 | 25 |
26 | MORE 27 | 28 |
29 |
30 |
-------------------------------------------------------------------------------- /app/views/shared/devices/_os.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= render partial: 'shared/devices/nav' %> 4 |
5 |
OSVisitors 6 |
7 | <% @top_oses.each do |type| %> 8 | <% @max_os ||= type.count %> 9 |
10 |
11 |
12 | 13 |
14 | 15 | <%= link_to type.os, site_path(@site, request.query_parameters.merge(device_type: type.os)), class: 'block hover:underline', title: type.os %> 16 | 17 |
<%= pretty_num type.count %> (<%= number_to_percentage(100 * type.count / @unique_visitors_summary.to_f, precision: 1) %>) 18 |
19 | <% end %> 20 |
21 | 22 |
23 | MORE 24 | 25 |
26 |
27 |
-------------------------------------------------------------------------------- /app/views/shared/sources/_traffic_source.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= render partial: 'shared/sources/nav' %> 4 |
Traffic SourceVisitors 5 |
6 |
7 | <% @top_traffic_sources.each do |source| %> 8 | <% @max_source ||= source.count %> 9 |
10 |
11 |
12 | 13 |
14 | 15 | <%= link_to site_path(@site, request.query_parameters.merge(traffic_source: source.traffic_source)), class: 'block truncate hover:underline' do %> 16 | <%= source.traffic_source %> 17 | <% end%> 18 | 19 |
<%= pretty_num(source.count) %> 20 |
21 | <% end %> 22 | 23 |
24 | 25 |
26 | MORE 27 | 28 |
29 |
30 |
-------------------------------------------------------------------------------- /test/models/event_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: events 6 | # 7 | # browser :string 8 | # city :string 9 | # country :string 10 | # data_source :string default("web") 11 | # device_type :string 12 | # engagement_time :integer 13 | # event_name :string default("page_view") 14 | # event_props :jsonb 15 | # hostname :string 16 | # ip :string 17 | # latitude :float 18 | # location_url :string 19 | # longitude :float 20 | # non_interaction_hit :boolean default(FALSE) 21 | # os :string 22 | # path :string 23 | # protocol_version :string default("2") 24 | # raw_event :jsonb 25 | # referrer :string 26 | # referrer_source :string 27 | # region :string 28 | # request_number :integer 29 | # screen_resolution :string 30 | # session_count :integer 31 | # session_engagement :boolean default(FALSE) 32 | # started_at :datetime not null 33 | # title :string 34 | # user_agent :string 35 | # user_language :string 36 | # user_props :jsonb 37 | # utm_campaign :string 38 | # utm_content :string 39 | # utm_medium :string 40 | # utm_source :string 41 | # utm_term :string 42 | # client_id :string not null 43 | # session_id :string not null 44 | # site_id :string not null 45 | # tracking_id :string not null 46 | # user_id :string 47 | # 48 | # Indexes 49 | # 50 | # events_started_at_idx (started_at) 51 | # index_events_on_site_id_and_session_id_and_started_at (site_id,session_id,started_at DESC) 52 | # 53 | require "test_helper" 54 | 55 | class EventTest < ActiveSupport::TestCase 56 | # test "the truth" do 57 | # assert true 58 | # end 59 | end 60 | -------------------------------------------------------------------------------- /app/views/shared/sources/_traffic_campaign.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= render partial: 'shared/sources/nav' %> 4 |
Traffic CampaignVisitors 5 |
6 |
7 | <% @top_traffic_campaigns.each do |source| %> 8 | <% @max_campaign ||= source.count %> 9 |
10 |
11 |
12 | 13 |
14 | 15 | <%= link_to site_path(@site, request.query_parameters.merge(traffic_campaign: source.traffic_campaign)), class: 'block truncate hover:underline' do %> 16 | <%= source.traffic_campaign %> 17 | <% end%> 18 | 19 |
<%= pretty_num(source.count) %> 20 |
21 | <% end %> 22 | 23 |
24 | 25 |
26 | MORE 27 | 28 |
29 |
30 |
-------------------------------------------------------------------------------- /app/views/shared/devices/_browser.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= render partial: 'shared/devices/nav' %> 4 |
5 |
BrowserVisitors 6 |
7 | <% @top_browsers.each do |type| %> 8 | <% @max_browser ||= type.count %> 9 |
10 |
11 |
12 | 13 |
14 | 15 | <%= link_to type.browser, site_path(@site, request.query_parameters.merge(browser: type.browser)), class: 'block hover:underline', title: type.browser %> 16 | 17 |
<%= pretty_num type.count %> (<%= number_to_percentage(100 * type.count / @unique_visitors_summary.to_f, precision: 1) %>) 18 |
19 | <% end %> 20 |
21 | 22 |
23 | MORE 24 | 25 |
26 |
27 |
-------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 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 | # Raises error for missing translations. 49 | # config.action_view.raise_on_missing_translations = true 50 | end 51 | -------------------------------------------------------------------------------- /app/views/shared/devices/_device_type.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= render partial: 'shared/devices/nav' %> 4 |
5 |
Device TypeVisitors 6 |
7 | <% @top_device_types.each do |type| %> 8 | <% @max_device ||= type.count %> 9 |
10 |
11 |
12 | 13 |
14 | 15 | <%= link_to type.device_type, site_path(@site, request.query_parameters.merge(device_type: type.device_type)), class: 'block hover:underline', title: type.device_type %> 16 | 17 |
<%= pretty_num type.count %> (<%= number_to_percentage(100 * type.count / @unique_visitors_summary.to_f, precision: 1) %>) 18 |
19 | <% end %> 20 |
21 | 22 |
23 | MORE 24 | 25 |
26 |
27 |
-------------------------------------------------------------------------------- /app/views/shared/_steps.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/shared/sources/_referrer_source.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= render partial: 'shared/sources/nav' %> 4 |
ReferralVisitors 5 |
6 |
7 | <% @top_referrer_sources.each do |source| %> 8 | <% @max_source ||= source.count %> 9 |
10 |
11 |
12 | 13 |
14 | 15 | <%= link_to site_path(@site, request.query_parameters.merge(referrer_source: source.referrer_source)), class: 'block truncate hover:underline' do %> 16 | <%= source.referrer_source %> 17 | <% end%> 18 | 19 |
<%= pretty_num(source.count) %> 20 |
21 | <% end %> 22 | 23 |
24 | 25 |
26 | MORE 27 | 28 |
29 |
30 |
-------------------------------------------------------------------------------- /config/webpacker.yml: -------------------------------------------------------------------------------- 1 | # Note: You must restart bin/webpack-dev-server for changes to take effect 2 | 3 | default: &default 4 | source_path: app/javascript 5 | source_entry_path: packs 6 | public_root_path: public 7 | public_output_path: packs 8 | cache_path: tmp/cache/webpacker 9 | check_yarn_integrity: false 10 | webpack_compile_output: true 11 | 12 | # Additional paths webpack should lookup modules 13 | # ['app/assets', 'engine/foo/app/assets'] 14 | resolved_paths: [] 15 | 16 | # Reload manifest.json on all requests so we reload latest compiled packs 17 | cache_manifest: false 18 | 19 | # Extract and emit a css file 20 | extract_css: false 21 | 22 | static_assets_extensions: 23 | - .jpg 24 | - .jpeg 25 | - .png 26 | - .gif 27 | - .tiff 28 | - .ico 29 | - .svg 30 | - .eot 31 | - .otf 32 | - .ttf 33 | - .woff 34 | - .woff2 35 | 36 | extensions: 37 | - .mjs 38 | - .js 39 | - .sass 40 | - .scss 41 | - .css 42 | - .module.sass 43 | - .module.scss 44 | - .module.css 45 | - .png 46 | - .svg 47 | - .gif 48 | - .jpeg 49 | - .jpg 50 | 51 | development: 52 | <<: *default 53 | compile: true 54 | 55 | # Verifies that correct packages and versions are installed by inspecting package.json, yarn.lock, and node_modules 56 | check_yarn_integrity: true 57 | 58 | # Reference: https://webpack.js.org/configuration/dev-server/ 59 | dev_server: 60 | https: false 61 | host: localhost 62 | port: 3035 63 | public: localhost:13035 64 | hmr: false 65 | # Inline should be set to true if using HMR 66 | inline: true 67 | overlay: true 68 | compress: true 69 | disable_host_check: true 70 | use_local_ip: false 71 | quiet: false 72 | pretty: false 73 | headers: 74 | 'Access-Control-Allow-Origin': '*' 75 | watch_options: 76 | ignored: '**/node_modules/**' 77 | 78 | 79 | test: 80 | <<: *default 81 | compile: true 82 | 83 | # Compile test packs to a separate directory 84 | public_output_path: packs-test 85 | 86 | production: 87 | <<: *default 88 | 89 | # Production depends on precompilation of packs prior to booting for performance. 90 | compile: false 91 | 92 | # Extract and emit a css file 93 | extract_css: true 94 | 95 | # Cache manifest.json for performance 96 | cache_manifest: true 97 | -------------------------------------------------------------------------------- /app/views/shared/_summary.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Unique visitors 4 |
5 |
6 | <%= pretty_num @unique_visitors_summary %> 7 | 8 | <%= render partial: 'shared/growth_rate', locals: {growth: GrowthRate.new(@unique_visitors_summary_p, @unique_visitors_summary)} %> 9 | 10 |
11 |
12 | 13 |
14 |
Total pageviews 15 |
16 | 17 |
<%= pretty_num @total_pageviews_summary %> 18 | 19 | <%= render partial: 'shared/growth_rate', locals: {growth: GrowthRate.new(@total_pageviews_summary_p, @total_pageviews_summary)} %> 20 | 21 |
22 |
23 |
New visitors 24 |
25 |
26 | 27 | <%= pretty_num(@new_visitors_count) %> 28 | 29 | <%= render partial: 'shared/growth_rate', locals: {growth: GrowthRate.new(@new_visitors_count_p, @new_visitors_count)} %> 30 | 31 |
32 |
33 |
34 |
Avg Engagement Time 35 |
36 |
37 | <%= pretty_time(@avg_engagement_time.to_i / 1000) %> 38 | 39 | <%= render partial: 'shared/growth_rate', locals: {growth: GrowthRate.new(@avg_engagement_time_p, @avg_engagement_time)} %> 40 |
41 |
42 |
-------------------------------------------------------------------------------- /app/views/shared/_top_page.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Top Pages

4 |
Page urlVisitors 5 |
6 |
7 | <% @top_pages.each do |page| %> 8 | <% @max_page ||= page.count %> 9 |
10 |
11 |
12 | 13 |
14 | 15 | <%= link_to url_to_path(page.location_url), site_path(@site, request.query_parameters.merge(location_url: page.location_url)), class: 'block hover:underline', title: page.location_url %> 16 | 17 | 18 |
<%= pretty_num page.count %> 19 |
20 | <% end %> 21 |
22 | 23 |
24 | MORE 25 | 26 |
27 |
28 |
-------------------------------------------------------------------------------- /app/views/sites/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | My sites 4 |

5 | <%= link_to '+ Add a website', new_site_path, class: 'button my-2 sm:my-0 w-auto' %> 6 |
7 | 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'README*.md' 9 | - 'docs/**' 10 | pull_request: 11 | branches: 12 | - main 13 | paths-ignore: 14 | - 'README*.md' 15 | - 'docs/**' 16 | 17 | jobs: 18 | build: 19 | if: ${{ !contains(github.event.commits[0].message, '[skip ci]') }} 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: 3.0.0 26 | bundler-cache: true 27 | - run: bundle exec rubocop --parallel 28 | 29 | docker_web: 30 | needs: build 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v2 35 | - name: Set up QEMU 36 | uses: docker/setup-qemu-action@v1 37 | - name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v1 39 | - name: Login to DockerHub 40 | uses: docker/login-action@v1 41 | with: 42 | username: ${{ secrets.DOCKER_USERNAME }} 43 | password: ${{ secrets.DOCKER_PASSWORD }} 44 | - name: Build and push 45 | uses: docker/build-push-action@v2 46 | with: 47 | context: . 48 | file: ./docker/Dockerfile 49 | platforms: linux/amd64 50 | push: true 51 | tags: | 52 | ${{ secrets.DOCKER_USERNAME }}/hypercable:latest 53 | build-args: | 54 | SECRET_KEY_BASE=precompile_placeholder 55 | RAILS_ENV=production 56 | 57 | docker_collector: 58 | needs: build 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v2 63 | - name: Set up QEMU 64 | uses: docker/setup-qemu-action@v1 65 | - name: Set up Docker Buildx 66 | uses: docker/setup-buildx-action@v1 67 | - name: Login to DockerHub 68 | uses: docker/login-action@v1 69 | with: 70 | username: ${{ secrets.DOCKER_USERNAME }} 71 | password: ${{ secrets.DOCKER_PASSWORD }} 72 | - name: Build and push 73 | uses: docker/build-push-action@v2 74 | with: 75 | context: ./docker/dockerfiles 76 | file: ./docker/dockerfiles/openresty.Dockerfile 77 | platforms: linux/amd64 78 | push: true 79 | tags: | 80 | ${{ secrets.DOCKER_USERNAME }}/hypercable-openresty:latest 81 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 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 on 7 | # every request. 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 an error on page load if there are pending migrations. 45 | config.active_record.migration_error = :page_load 46 | 47 | # Highlight code that triggered database queries in logs. 48 | config.active_record.verbose_query_logs = true 49 | 50 | # Debug mode disables concatenation and preprocessing of assets. 51 | # This option may cause significant delays in view rendering with a large 52 | # number of complex assets. 53 | config.assets.debug = true 54 | 55 | # Suppress logger output for asset requests. 56 | config.assets.quiet = true 57 | 58 | # Raises error for missing translations. 59 | # config.action_view.raise_on_missing_translations = true 60 | 61 | # Use an evented file watcher to asynchronously detect changes in source code, 62 | # routes, locales, etc. This feature depends on the listen gem. 63 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker 64 | end 65 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 5 | 6 | 7 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 8 | gem "rails", "~> 6.0", ">= 6.1.0.rc1" 9 | # Use postgresql as the database for Active Record 10 | gem "pg", ">= 0.18", "< 2.0" 11 | # Use Puma as the app server 12 | gem "puma", "~> 4" 13 | # Use SCSS for stylesheets 14 | gem "sass-rails", ">= 6" 15 | # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker 16 | gem "webpacker", "~> 4.0" 17 | # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks 18 | gem "turbolinks", "~> 5" 19 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 20 | gem "jbuilder", "~> 2.7" 21 | # Use Redis adapter to run Action Cable in production 22 | # gem 'redis', '~> 4.0' 23 | # Use Active Model has_secure_password 24 | # gem 'bcrypt', '~> 3.1.7' 25 | 26 | # Use Active Storage variant 27 | # gem 'image_processing', '~> 1.2' 28 | 29 | group :development, :test do 30 | gem "rubocop", require: false 31 | gem "rubocop-performance" 32 | gem "rubocop-rails" 33 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 34 | gem "byebug", platforms: [:mri, :mingw, :x64_mingw] 35 | end 36 | 37 | group :development do 38 | # Access an interactive console on exception pages or by calling 'console' anywhere in the code. 39 | gem "web-console", ">= 3.3.0" 40 | end 41 | 42 | gem "annotate" 43 | 44 | group :test do 45 | # Adds support for Capybara system testing and selenium driver 46 | gem "capybara", ">= 2.15" 47 | gem "selenium-webdriver" 48 | # Easy installation and use of web drivers to run system tests with browsers 49 | gem "webdrivers" 50 | end 51 | 52 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 53 | # gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 54 | 55 | gem "dotenv-rails", "~> 2.7", require: "dotenv/rails-now" 56 | gem "sidekiq" 57 | 58 | gem "pry-rails", "~> 0.3.9" 59 | 60 | gem "mini_sql", "~> 0.3" 61 | 62 | gem "device_detector", "~> 1.0" 63 | 64 | gem "geoip2_compat" 65 | 66 | gem "lru_redux", "~> 1.1" 67 | 68 | gem "addressable", "~> 2.7" 69 | 70 | gem "sidekiq-worker-killer", "~> 1.0" 71 | 72 | gem "mini_portile2", "~> 2.5" 73 | 74 | gem "bcrypt", "~> 3.1" 75 | 76 | gem "sendgrid-actionmailer", "~> 3.1" 77 | 78 | gem "chartkick", "~> 3.4" 79 | 80 | gem "groupdate", "~> 5.2" 81 | 82 | gem "user_agent_parser", "~> 2.7" 83 | 84 | gem "referer-parser", "~> 0.3.0" 85 | 86 | gem "activerecord-import", "~> 1.0" 87 | 88 | gem "hiredis", "~> 0.6.3" 89 | -------------------------------------------------------------------------------- /app/views/layouts/detail.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to 'Debug View', debug_site_path(@site), class: 'button' %> 2 | 3 |
4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 |
12 | 15 |
16 |
17 | <% QueryBuilder::FILTER_KEYS.each do |filter| %> 18 | <% if params[filter].present? %> 19 | 20 | 21 | <%= filter %>: <%= params[filter] %> 22 | 23 | 24 | 25 | <%= link_to "X", request.params.except(filter), class: 'ml-1 cursor-pointer hover:text-indigo-500' %> 26 | 27 | 28 | <% end %> 29 | <% end %> 30 |
31 | <% if params.slice(*QueryBuilder::FILTER_KEYS).blank? %> 32 | <%= @current_visitors_count %> current visitors 33 | <% end %> 34 |
35 | <%= render partial: 'shared/date_filter' %> 36 |
37 |
38 | <%= yield %> 39 |
40 | 41 |
42 | <%= parent_layout "layouts/application" %> -------------------------------------------------------------------------------- /app/jobs/mp_event_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class MpEventJob 4 | include Sidekiq::Worker 5 | 6 | COLUMN_NAMES = Hyper::Event.column_names 7 | 8 | def perform(*args) 9 | Thread.current[:bqmp] ||= BufferQueue.new(max_batch_size: (ENV["MAX_BATCH_SIZE"] || 50).to_i, execution_interval: (ENV["EXECUTION_INTERVAL"] || 5).to_i) do |batch| 10 | puts "bulk insert #{batch.size} records" 11 | Hyper::Event.insert_all!(batch) 12 | end 13 | params, form, request = args 14 | 15 | api_secret = params["api_secret"] 16 | return if api_secret.nil? 17 | _firebase_app_id = params["firebase_app_id"] 18 | 19 | meta_path = request["path"].match(/\/(?.*?)\/mp\/collect/) 20 | site_uuid = meta_path["uuid"] 21 | return if site_uuid.nil? 22 | return unless RedisClient.get(api_secret) == site_uuid 23 | payload = JSON.parse(form) 24 | events = payload["events"] 25 | _app_instance_id = payload["app_instance_id"] 26 | client_id = payload["client_id"] 27 | user_id = payload["user_id"] 28 | _time = payload["timestamp_micros"] 29 | user_props = payload["user_properties"] 30 | tracking_id = params["measurement_id"] 31 | 32 | events.each do |event| 33 | tech_info = TechDetector.detect(request["user_agent"]) 34 | tech_info.delete(:is_bot) 35 | real_ip = request["x_forwarded_for"] || request["ip"] 36 | ip_info = IPDB.get(real_ip) 37 | 38 | referrer_source = RefererSourceDetector.detect(params["dr"]) 39 | traffic_info = TrafficDetector.detect({ "dl" => params["dl"], "dr" => params["dr"] }) 40 | 41 | result = {} 42 | result.merge!(traffic_info) 43 | result.merge!(tech_info) 44 | result.merge!({ 45 | site_id: site_uuid, 46 | event_name: event["name"], 47 | session_id: client_id, 48 | client_id: client_id, 49 | user_id: user_id, 50 | tracking_id: tracking_id, 51 | started_at: Time.now, 52 | 53 | protocol_version: "2", 54 | data_source: "web", 55 | 56 | title: params["dt"], 57 | user_agent: request["user_agent"], 58 | ip: real_ip, 59 | referrer: params["dr"], 60 | referrer_source: referrer_source, 61 | screen_resolution: params["sr"], 62 | user_language: params["ul"], 63 | 64 | country: ip_info[:country_name], 65 | city: ip_info[:city], 66 | region: ip_info[:region_name], 67 | latitude: ip_info[:latitude], 68 | longitude: ip_info[:longitude], 69 | 70 | user_props: user_props, 71 | event_props: event["params"], 72 | 73 | raw_event: event 74 | }) 75 | Thread.current[:bqmp].push result 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /config/initializers/traffic_detector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # https://support.google.com/analytics/answer/6205762?hl=en 4 | 5 | require "addressable/uri" 6 | 7 | class TrafficDetector 8 | DEFAULT_SOURCE = "(direct)" 9 | DEFAULT_MEDIUM = "(none)" 10 | DEFAULT_CAMPAIGN = "(direct)" 11 | 12 | REF_PARSER = RefererParser::Parser.new("./data/referers.yml") 13 | 14 | def self.detect(event) 15 | result = {} 16 | request_url = event["dl"] 17 | return result if request_url.nil? 18 | 19 | request_uri = Addressable::URI.parse(request_url).normalize 20 | request_params = request_uri.query_values || {} 21 | 22 | referrer_url = event["dr"] 23 | 24 | result[:hostname] = request_uri.domain 25 | result[:path] = request_uri.path 26 | result[:location_url] = request_uri.to_s 27 | result[:request_params] = request_params 28 | 29 | return result.merge!( 30 | traffic_campaign: "adwords", 31 | traffic_source: "Google", 32 | traffic_medium: "cpc" 33 | ) if request_params["gclid"].present? 34 | 35 | return result.merge!( 36 | traffic_campaign: "DoubleClick", 37 | traffic_source: "Google", 38 | traffic_medium: "cpc" 39 | ) if request_params["gclsrc"].present? 40 | 41 | return result.merge!( 42 | traffic_campaign: request_params["utm_campaign"] || "(not set)", 43 | traffic_source: request_params["utm_source"], 44 | traffic_medium: request_params["utm_medium"] || "(not set)" 45 | ) if request_params["utm_source"].present? 46 | 47 | if referrer_url.present? 48 | referrer_uri = Addressable::URI.parse(referrer_url) 49 | if referrer_uri.host == request_uri.host 50 | return result.merge!( 51 | traffic_campaign: "(internal)", 52 | traffic_source: referrer_uri.domain, 53 | traffic_medium: "(internal)" 54 | ) 55 | else 56 | ref_parser_result = REF_PARSER.parse(referrer_uri.normalize.to_s) 57 | if ref_parser_result[:known] 58 | return result.merge!( 59 | traffic_campaign: "(not set)", 60 | traffic_source: ref_parser_result[:source], 61 | traffic_medium: ref_parser_result[:medium] 62 | ) 63 | else 64 | return result.merge!( 65 | traffic_campaign: "(not set)", 66 | traffic_source: referrer_uri.domain, 67 | traffic_medium: "referral" 68 | ) 69 | end 70 | end 71 | else 72 | return result.merge!( 73 | traffic_campaign: DEFAULT_CAMPAIGN, 74 | traffic_source: DEFAULT_SOURCE, 75 | traffic_medium: DEFAULT_MEDIUM 76 | ) 77 | end 78 | result.merge!( 79 | traffic_campaign: DEFAULT_CAMPAIGN, 80 | traffic_source: DEFAULT_SOURCE, 81 | traffic_medium: DEFAULT_MEDIUM 82 | ) 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/tasks/auto_annotate_models.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # NOTE: only doing this in development as some production environments (Heroku) 4 | # NOTE: are sensitive to local FS writes, and besides -- it's just not proper 5 | # NOTE: to have a dev-mode tool do its thing in production. 6 | if Rails.env.development? 7 | require "annotate" 8 | task :set_annotation_options do 9 | # You can override any of these by setting an environment variable of the 10 | # same name. 11 | Annotate.set_defaults( 12 | "active_admin" => "false", 13 | "additional_file_patterns" => [], 14 | "routes" => "false", 15 | "models" => "true", 16 | "position_in_routes" => "before", 17 | "position_in_class" => "before", 18 | "position_in_test" => "before", 19 | "position_in_fixture" => "before", 20 | "position_in_factory" => "before", 21 | "position_in_serializer" => "before", 22 | "show_foreign_keys" => "true", 23 | "show_complete_foreign_keys" => "false", 24 | "show_indexes" => "true", 25 | "simple_indexes" => "false", 26 | "model_dir" => "app/models", 27 | "root_dir" => "", 28 | "include_version" => "false", 29 | "require" => "", 30 | "exclude_tests" => "false", 31 | "exclude_fixtures" => "false", 32 | "exclude_factories" => "false", 33 | "exclude_serializers" => "false", 34 | "exclude_scaffolds" => "true", 35 | "exclude_controllers" => "true", 36 | "exclude_helpers" => "true", 37 | "exclude_sti_subclasses" => "false", 38 | "ignore_model_sub_dir" => "false", 39 | "ignore_columns" => nil, 40 | "ignore_routes" => nil, 41 | "ignore_unknown_models" => "false", 42 | "hide_limit_column_types" => "integer,bigint,boolean", 43 | "hide_default_column_types" => "json,jsonb,hstore", 44 | "skip_on_db_migrate" => "false", 45 | "format_bare" => "true", 46 | "format_rdoc" => "false", 47 | "format_yard" => "false", 48 | "format_markdown" => "false", 49 | "sort" => "false", 50 | "force" => "false", 51 | "frozen" => "false", 52 | "classified_sort" => "true", 53 | "trace" => "false", 54 | "wrapper_open" => nil, 55 | "wrapper_close" => nil, 56 | "with_comment" => "true" 57 | ) 58 | end 59 | 60 | Annotate.load_tasks 61 | end 62 | -------------------------------------------------------------------------------- /app/views/settings/visibility.html.erb: -------------------------------------------------------------------------------- 1 |
2 | ← Back to stats 3 |
4 |

5 | Settings for <%= @site.domain %> 6 |

7 |
8 |
9 | <%= render partial: 'nav' %> 10 | 11 |
12 | <%= form_with model: @site, local: true, url: update_general_site_settings_path(@site) do |f| %> 13 |
14 |
15 |
16 |

General information

17 |

Update your reporting timezone.

18 | 19 | 20 | 21 |
22 | 23 |
24 |
25 | <%= f.label 'Domain', class: 'block text-sm font-medium leading-5 text-gray-700 dark:text-gray-300' %> 26 | <%= f.text_field :domain, class: 'dark:bg-gray-900 mt-1 block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-100' %> 27 |
28 | 29 |
30 | <%= f.label 'Timezone', class: 'block text-sm font-medium leading-5 text-gray-700 dark:text-gray-300' %> 31 | <%= f.time_zone_select(:timezone, ActiveSupport::TimeZone.all, {default: "UTC"}, class: "dark:bg-gray-900 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:text-gray-100 cursor-pointer") %> 32 |
33 |
34 |
35 |
36 | 37 | <%= f.button 'Save', class: 'button', type: :submit, data: {disable_with: disable_with_spinner('Save')} %> 38 | 39 |
40 |
41 | <% end %> 42 | 43 |
44 |
45 |
--------------------------------------------------------------------------------