├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── app ├── assets │ └── javascripts │ │ └── notable_web.js ├── controllers │ └── notable_web │ │ └── home_controller.rb └── views │ ├── layouts │ └── notable_web │ │ └── application.html.erb │ └── notable_web │ └── home │ ├── _stream.html.erb │ ├── index.html.erb │ ├── slow_action.html.erb │ ├── slow_actions.html.erb │ └── users.html.erb ├── config └── routes.rb ├── lib ├── notable_web.rb └── notable_web │ ├── engine.rb │ └── version.rb ├── notable_web.gemspec └── test ├── controller_test.rb ├── internal ├── app │ └── assets │ │ └── config │ │ └── manifest.js ├── config │ ├── database.yml │ └── routes.rb └── db │ └── schema.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /test/internal/tmp/ 10 | /tmp/ 11 | *.bundle 12 | *.so 13 | *.o 14 | *.a 15 | *.log 16 | *.sqlite 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 (2023-01-30) 2 | 3 | - Dropped support for Ruby < 2.7 and Rails < 6 4 | 5 | ## 0.1.2 (2019-12-23) 6 | 7 | - Fixed error with Rails 6 8 | 9 | ## 0.1.1 (2019-10-27) 10 | 11 | - Added support for Sprockets 4 12 | 13 | ## 0.1.0 (2019-05-28) 14 | 15 | - Fixed errors with Rails 5 and 6 16 | - Dropped support for Rails < 5 17 | 18 | ## 0.0.1 (2014-12-22) 19 | 20 | - First release 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "combustion" 7 | gem "activerecord" 8 | gem "sqlite3" 9 | gem "sprockets-rails" 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2023 Andrew Kane 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notable Web 2 | 3 | A web interface for [Notable](https://github.com/ankane/notable) 4 | 5 | ## Installation 6 | 7 | Add this line to your application’s Gemfile: 8 | 9 | ```ruby 10 | gem "notable_web" 11 | ``` 12 | 13 | And add it to your `config/routes.rb`. 14 | 15 | ```ruby 16 | mount NotableWeb::Engine, at: "notable" 17 | ``` 18 | 19 | Be sure to secure the dashboard in production. 20 | 21 | #### Basic Authentication 22 | 23 | Set the following variables in your environment or an initializer. 24 | 25 | ```ruby 26 | ENV["NOTABLE_USERNAME"] = "andrew" 27 | ENV["NOTABLE_PASSWORD"] = "secret" 28 | ``` 29 | 30 | #### Devise 31 | 32 | ```ruby 33 | authenticate :user, ->(user) { user.admin? } do 34 | mount NotableWeb::Engine, at: "notable" 35 | end 36 | ``` 37 | 38 | #### will_paginate 39 | 40 | If using will_paginate, create an initializer `config/initializers/kaminari.rb` with: 41 | 42 | ```ruby 43 | Kaminari.configure do |config| 44 | config.page_method_name = :per_page_kaminari 45 | end 46 | ``` 47 | 48 | to avoid conflicts. 49 | 50 | ## History 51 | 52 | View the [changelog](https://github.com/ankane/notable_web/blob/master/CHANGELOG.md) 53 | 54 | ## Contributing 55 | 56 | Everyone is encouraged to help improve this project. Here are a few ways you can help: 57 | 58 | - [Report bugs](https://github.com/ankane/notable_web/issues) 59 | - Fix bugs and [submit pull requests](https://github.com/ankane/notable_web/pulls) 60 | - Write, clarify, or fix documentation 61 | - Suggest or add new features 62 | 63 | To get started with development: 64 | 65 | ```sh 66 | git clone https://github.com/ankane/notable_web.git 67 | cd notable_web 68 | bundle install 69 | bundle exec rake test 70 | ``` 71 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.test_files = FileList["test/**/*_test.rb"] 7 | end 8 | 9 | task default: :test 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/notable_web.js: -------------------------------------------------------------------------------- 1 | //= require chartkick 2 | //= require Chart.bundle 3 | -------------------------------------------------------------------------------- /app/controllers/notable_web/home_controller.rb: -------------------------------------------------------------------------------- 1 | module NotableWeb 2 | class HomeController < ActionController::Base 3 | layout "notable_web/application" 4 | 5 | protect_from_forgery with: :exception 6 | 7 | http_basic_authenticate_with name: ENV["NOTABLE_USERNAME"], password: ENV["NOTABLE_PASSWORD"] if ENV["NOTABLE_PASSWORD"] 8 | 9 | def index 10 | where = safe_params.slice(:status, :note_type, :note, :user_id, :user_type) 11 | where = {notable_requests: where} if where.any? 12 | 13 | page_method_name = Kaminari.config.page_method_name 14 | 15 | # https://github.com/rails/rails/issues/9055 16 | @requests = Notable::Request.order("notable_requests.id DESC").where(where).preload(:user).public_send(page_method_name, params[:page]).per(100) 17 | 18 | if params[:action_name] 19 | @requests = @requests.where(action: params[:action_name]) 20 | end 21 | 22 | if params[:status] 23 | @requests = @requests.where("note_type != 'Slow Request'") 24 | end 25 | 26 | @requests = 27 | case params[:scope] 28 | when "slow_requests" 29 | @requests.where("action IS NOT NULL AND (status = 503 OR note_type = 'Slow Request')") 30 | when "referrer" 31 | @requests.where("referrer IS NOT NULL") 32 | when "external_referrer" 33 | domain = PublicSuffix.parse(request.host).domain rescue request.host 34 | @requests.where("referrer IS NOT NULL").where("referrer NOT LIKE ?", "%#{domain}%") 35 | else 36 | @requests 37 | end 38 | end 39 | 40 | def users 41 | requests = Notable::Request.where("created_at > ?", 1.day.ago) 42 | # yuck make this better 43 | user_ids = [] 44 | counts = Hash.new(0) 45 | requests.select("user_id, user_type").order("created_at desc").where("user_id IS NOT NULL").each do |request| 46 | keys = [request.user_id, request.user_type] 47 | counts[keys] += 1 48 | user_ids << keys if counts[keys] == 3 49 | break if user_ids.size >= 20 50 | end 51 | groups = user_ids.group_by{|v| v[1] } 52 | where = [groups.map{|_, _v| "(user_type = ? AND user_id IN (?))" }.join(" OR ")] + groups.flat_map{|v| [v[0], v[1].map{|v1| v1[0] }] } 53 | @users = requests.includes(:user).where(where).order("created_at DESC").group_by(&:user) 54 | end 55 | 56 | def slow_actions 57 | @top_actions = slow_requests.where("created_at > ?", 3.days.ago).select("action, SUM(request_time) AS total_time, AVG(request_time) AS average_time, COUNT(*) AS requests, SUM(CASE WHEN status = 503 THEN 1 ELSE 0 END) AS timeouts").group("action").order("total_time DESC").limit(50) 58 | @total_time_by_day = [{name: "Total Time (min)", data: slow_requests.group_by_day(:created_at, last: 14).sum("request_time").map{|k, v| [k, (v / 60.0).round] }}] 59 | end 60 | 61 | def slow_action 62 | @action = params[:action_name] 63 | @total_time_by_day = [{name: "Total Time (min)", data: slow_requests.where(action: @action).group_by_day(:created_at, last: 14).sum("request_time").map{|k, v| [k, (v / 60.0).round] }}] 64 | end 65 | 66 | protected 67 | 68 | def slow_requests 69 | Notable::Request.where("action IS NOT NULL AND (status = 503 OR note_type = 'Slow Request')") 70 | end 71 | 72 | def safe_params 73 | params.slice(:status, :note_type, :note, :user_id, :user_type, :action_name, :status, :scope).permit!.to_h 74 | end 75 | helper_method :safe_params 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /app/views/layouts/notable_web/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Notable 5 | 6 | 7 | 8 | 66 | 67 | <%= javascript_include_tag "notable_web" %> 68 | 69 | 70 |
71 | 81 | 85 |
86 | 87 | User 88 | <%= hidden_field_tag :user_type, "User" %> 89 | <%= text_field_tag :user_id %> 90 | 91 |
92 | <%= yield %> 93 |
94 | 95 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /app/views/notable_web/home/_stream.html.erb: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /app/views/notable_web/home/index.html.erb: -------------------------------------------------------------------------------- 1 | <% if (params[:note_type] == "Validation Errors" or params[:note_type] == "Error") && !params[:action_name] %> 2 |

Top

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <% Notable::Request.select("note, action, COUNT(DISTINCT (user_type || user_id)) as count_user, COUNT(*) as count_all").where(note_type: params[:note_type]).group([:note, :action]).order("count_all desc").each do |agg| %> 14 | 15 | 19 | 20 | 21 | 22 | <% end %> 23 | 24 |
ErrorsUsersCount
16 | <%= agg.note %> <%= agg.action %> 17 | <%= link_to "view", safe_params.merge(note: agg.note, action_name: agg.action) %> 18 | <%= agg.count_user %><%= agg.count_all %>
25 | <% end %> 26 | 27 | <% if params[:user_type] and params[:user_id] %> 28 |

<%= params[:user_type] %> <%= params[:user_id] %>

29 | <% end %> 30 | 31 | <%= render partial: "stream", locals: {requests: @requests, show_user: !(params[:user_id] && params[:user_type])} %> 32 | 33 | <% if @requests.current_page != 1 %> 34 | <%= link_to "Prev", safe_params.merge(page: @requests.current_page - 1), style: "margin-right: 20px;" %> 35 | <% end %> 36 | <% if @requests.size == @requests.limit_value %> 37 | <%= link_to "Next", safe_params.merge(page: @requests.current_page + 1) %> 38 | <% end %> 39 | -------------------------------------------------------------------------------- /app/views/notable_web/home/slow_action.html.erb: -------------------------------------------------------------------------------- 1 |

<%= @action %>

2 | 3 | <%= line_chart @total_time_by_day %> 4 | -------------------------------------------------------------------------------- /app/views/notable_web/home/slow_actions.html.erb: -------------------------------------------------------------------------------- 1 | <%= line_chart @total_time_by_day %> 2 | 3 |

Last 3 Days

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <% @top_actions.each do |action| %> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <% end %> 25 | 26 |
ActionTotal Time (min)Average Time (sec)RequestsTimeouts
<%= link_to action.action, slow_action_path(action_name: action.action) %><%= (action.total_time / 60.0).round %><%= action.average_time.round(1) %><%= action.requests %><%= action.timeouts %>
27 | -------------------------------------------------------------------------------- /app/views/notable_web/home/users.html.erb: -------------------------------------------------------------------------------- 1 | <% @users.each do |user, requests| %> 2 | <% next if !user %> 3 | <% requests = requests.sort_by(&:created_at).reverse.first(5) %> 4 |

<%= NotableWeb.user_name_method.call(user) %>

5 | <%= render partial: "stream", locals: {requests: requests, show_user: false} %> 6 |

<%= link_to "more", root_path(user_id: user.id, user_type: user.class.name) %>

7 | <% end %> 8 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | NotableWeb::Engine.routes.draw do 2 | get "users", to: "home#users" 3 | get "slow_actions", to: "home#slow_actions" 4 | get "slow_action", to: "home#slow_action" 5 | root to: "home#index" 6 | end 7 | -------------------------------------------------------------------------------- /lib/notable_web.rb: -------------------------------------------------------------------------------- 1 | require "notable_web/version" 2 | 3 | # dependencies 4 | require "notable" 5 | require "groupdate" 6 | require "chartkick" 7 | require "kaminari" 8 | require "public_suffix" 9 | 10 | # engine 11 | require "notable_web/engine" 12 | 13 | module NotableWeb 14 | class << self 15 | attr_accessor :user_name_method 16 | attr_accessor :time_zone 17 | end 18 | self.user_name_method = proc{|user| user.try(:name) || "#{user.class.name} #{user.id}" } 19 | end 20 | -------------------------------------------------------------------------------- /lib/notable_web/engine.rb: -------------------------------------------------------------------------------- 1 | module NotableWeb 2 | class Engine < ::Rails::Engine 3 | isolate_namespace NotableWeb 4 | 5 | initializer "notable_web" do |app| 6 | if defined?(Sprockets) && Sprockets::VERSION >= "4" 7 | app.config.assets.precompile << "notable_web.js" 8 | else 9 | # use a proc instead of a string 10 | app.config.assets.precompile << proc { |path| path == "notable_web.js" } 11 | end 12 | 13 | NotableWeb.time_zone ||= Time.zone 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/notable_web/version.rb: -------------------------------------------------------------------------------- 1 | module NotableWeb 2 | VERSION = "0.2.0" 3 | end 4 | -------------------------------------------------------------------------------- /notable_web.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/notable_web/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "notable_web" 5 | spec.version = NotableWeb::VERSION 6 | spec.summary = "A web interface for Notable" 7 | spec.homepage = "https://github.com/ankane/notable_web" 8 | spec.license = "MIT" 9 | 10 | spec.author = "Andrew Kane" 11 | spec.email = "andrew@ankane.org" 12 | 13 | spec.files = Dir["*.{md,txt}", "{app,config,lib}/**/*"] 14 | spec.require_path = "lib" 15 | 16 | spec.required_ruby_version = ">= 2.7" 17 | 18 | spec.add_dependency "railties", ">= 6" 19 | spec.add_dependency "notable" 20 | spec.add_dependency "groupdate" 21 | spec.add_dependency "chartkick", ">= 4" 22 | spec.add_dependency "kaminari" 23 | spec.add_dependency "public_suffix" 24 | end 25 | -------------------------------------------------------------------------------- /test/controller_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class ControllerTest < ActionDispatch::IntegrationTest 4 | def test_index 5 | get notable_web.root_path 6 | assert_response :success 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/internal/app/assets/config/manifest.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankane/notable_web/a440cfb37916578acb7ae6c2d10c26a3023b1d33/test/internal/app/assets/config/manifest.js -------------------------------------------------------------------------------- /test/internal/config/database.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: sqlite3 3 | database: db/combustion_test.sqlite 4 | -------------------------------------------------------------------------------- /test/internal/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | mount NotableWeb::Engine, at: "/" 3 | end 4 | -------------------------------------------------------------------------------- /test/internal/db/schema.rb: -------------------------------------------------------------------------------- 1 | 2 | ActiveRecord::Schema.define do 3 | create_table :notable_requests do |t| 4 | t.string :note_type 5 | t.text :note 6 | t.references :user, polymorphic: true 7 | t.text :action 8 | t.integer :status 9 | t.text :url 10 | t.string :request_id 11 | t.string :ip 12 | t.text :user_agent 13 | t.text :referrer 14 | t.text :params 15 | t.float :request_time 16 | t.datetime :created_at 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "combustion" 3 | Bundler.require(:default) 4 | require "minitest/autorun" 5 | require "minitest/pride" 6 | 7 | logger = ActiveSupport::Logger.new(ENV["VERBOSE"] ? STDERR : nil) 8 | 9 | Combustion.path = "test/internal" 10 | Combustion.initialize! :active_record, :action_controller do 11 | config.action_controller.logger = logger 12 | config.active_record.logger = logger 13 | end 14 | --------------------------------------------------------------------------------