├── 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 |
"
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 |
--------------------------------------------------------------------------------
/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 |
4 | <%= link_to "Device Type", site_path(@site, **params.permit.merge(device_meniu: :device_type), anchor: 'top_devices') %>
5 | <%= link_to "Browser", site_path(@site, **params.permit.merge(device_meniu: :browser), anchor: 'top_devices') %>
6 | <%= link_to "OS", site_path(@site, **params.permit.merge(device_meniu: :os), anchor: 'top_devices') %>
7 |
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 |
4 | <%= link_to "Referral", site_path(@site, **params.permit.merge(source_meniu: :referrer_source), anchor: 'top_traffics') %>
5 | <%= link_to "Medium", site_path(@site, **params.permit.merge(source_meniu: :traffic_medium), anchor: 'top_traffics') %>
6 | <%= link_to "Source", site_path(@site, **params.permit.merge(source_meniu: :traffic_source), anchor: 'top_traffics') %>
7 | <%= link_to "Campaign", site_path(@site, **params.permit.merge(source_meniu: :traffic_campaign), anchor: 'top_traffics') %>
8 |
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 |
8 |
9 |
10 | <%= flash[:success] %>
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/views/layouts/flash/_info.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 | <%= flash[:info] %>
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/views/oses/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
Top Operating Systems
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | OS
11 | Visitors
12 | Pageviews
13 | Sessions
14 |
15 |
16 |
17 | <% @top_oses.each do |agg| %>
18 |
19 | <%= agg.os %>
20 | <%= pretty_num agg.visitors_count %>
21 | <%= pretty_num agg.count %>
22 | <%= pretty_num agg.sessions_count %>
23 |
24 | <% end %>
25 |
26 |
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 | Browser
11 | Visitors
12 | Pageviews
13 | Sessions
14 |
15 |
16 |
17 | <% @top_browsers.each do |agg| %>
18 |
19 | <%= agg.browser %>
20 | <%= pretty_num agg.visitors_count %>
21 | <%= pretty_num agg.count %>
22 | <%= pretty_num agg.sessions_count %>
23 |
24 | <% end %>
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/views/device_types/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
Top Device Types
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Device Type
11 | Visitors
12 | Pageviews
13 | Sessions
14 |
15 |
16 |
17 | <% @top_device_types.each do |agg| %>
18 |
19 | <%= agg.device_type %>
20 | <%= pretty_num agg.visitors_count %>
21 | <%= pretty_num agg.count %>
22 | <%= pretty_num agg.sessions_count %>
23 |
24 | <% end %>
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/views/countries/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
Top Countris or Regions
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Country or region
11 | Visitors
12 | Pageviews
13 | Sessions
14 |
15 |
16 |
17 | <% @top_countries.each do |agg| %>
18 |
19 | <%= agg.country %>
20 | <%= pretty_num agg.visitors_count %>
21 | <%= pretty_num agg.count %>
22 | <%= pretty_num agg.sessions_count %>
23 |
24 | <% end %>
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/views/layouts/flash/_error.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | <%= flash[:error]%>
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 | Traffic Medium
11 | Visitors
12 | Pageviews
13 | Sessions
14 |
15 |
16 |
17 | <% @top_traffic_mediums.each do |agg| %>
18 |
19 | <%= agg.traffic_medium %>
20 | <%= pretty_num agg.visitors_count %>
21 | <%= pretty_num agg.count %>
22 | <%= pretty_num agg.sessions_count %>
23 |
24 | <% end %>
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/views/traffic_sources/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
Top Traffic Source
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Traffic Source
11 | Visitors
12 | Pageviews
13 | Sessions
14 |
15 |
16 |
17 | <% @top_traffic_sources.each do |agg| %>
18 |
19 | <%= agg.traffic_source %>
20 | <%= pretty_num agg.visitors_count %>
21 | <%= pretty_num agg.count %>
22 | <%= pretty_num agg.sessions_count %>
23 |
24 | <% end %>
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/views/traffic_campaigns/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
Top Traffic Campaign
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Traffic Campaign
11 | Visitors
12 | Pageviews
13 | Sessions
14 |
15 |
16 |
17 | <% @top_traffic_campaigns.each do |agg| %>
18 |
19 | <%= agg.traffic_campaign %>
20 | <%= pretty_num agg.visitors_count %>
21 | <%= pretty_num agg.count %>
22 | <%= pretty_num agg.sessions_count %>
23 |
24 | <% end %>
25 |
26 |
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 | Location url
9 | Visitors
10 | Pageviews
11 | Sessions
12 |
13 |
14 |
15 | <% @top_location_urls.each do |url| %>
16 |
17 | <%= url.location_url %>
18 | <%= pretty_num url.visitors_count %>
19 | <%= pretty_num url.count %>
20 | <%= pretty_num url.sessions_count %>
21 |
22 | <% end %>
23 |
24 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
4 | <% @events.each do |event| %>
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
<%= event.event_name %>
<%= JSON.pretty_generate(event.attributes) %>
20 |
21 |
22 | <%= event.started_at %>
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | <% end %>
34 |
35 |
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 | Referrer Source
11 | Visitors
12 | Pageviews
13 | Sessions
14 |
15 |
16 |
17 | <% @top_referrer_sources.each do |agg| %>
18 |
19 |
20 |
21 |
22 | <%= agg.referrer_source %>
23 |
24 |
25 |
26 | <%= pretty_num agg.visitors_count %>
27 | <%= pretty_num agg.count %>
28 | <%= pretty_num agg.sessions_count %>
29 |
30 | <% end %>
31 |
32 |
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 |
8 |
9 | <% %w[realtime 7d 30d 6m 12m today week month].each do |filter| %>
10 | <%= link_to time_range_names[filter], request.params.merge(period: filter), class: 'bg-gray-100 block px-2 py-1 lg:px-4 lg:py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out', role: 'menuitem' %>
11 | <% end %>
12 |
13 |
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 Medium Visitors
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 |
29 |
30 |
--------------------------------------------------------------------------------
/app/views/shared/devices/_os.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= render partial: 'shared/devices/nav' %>
4 |
5 |
OS Visitors
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 |
26 |
27 |
--------------------------------------------------------------------------------
/app/views/shared/sources/_traffic_source.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= render partial: 'shared/sources/nav' %>
4 |
Traffic Source Visitors
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 |
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 Campaign Visitors
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 |
29 |
30 |
--------------------------------------------------------------------------------
/app/views/shared/devices/_browser.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= render partial: 'shared/devices/nav' %>
4 |
5 |
Browser Visitors
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 |
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 Type Visitors
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 |
26 |
27 |
--------------------------------------------------------------------------------
/app/views/shared/_steps.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Register
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
27 | Activate account
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
39 | Add site info
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
51 | Install snippet
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/app/views/shared/sources/_referrer_source.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= render partial: 'shared/sources/nav' %>
4 |
Referral Visitors
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 |
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 url Visitors
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 |
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 |
9 |
10 | <% @sites.each do |site| %>
11 |
33 | <% end %>
34 |
35 |
--------------------------------------------------------------------------------
/.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 |
13 | <%= @site.domain %>
14 |
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 |
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 |
--------------------------------------------------------------------------------