├── .codeclimate.yml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── config.yml ├── .gitignore ├── .reek.yml ├── .rspec ├── .rubocop.yml ├── .ruby-gemset ├── .ruby-version ├── .yardopts ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── Gemfile_5_2 ├── Gemfile_6_0 ├── Gemfile_6_1 ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── exception_hunter_manifest.js │ ├── images │ │ └── exception_hunter │ │ │ ├── .keep │ │ │ └── logo.png │ └── stylesheets │ │ └── exception_hunter │ │ ├── application.css │ │ ├── base.css │ │ ├── errors.css │ │ ├── navigation.css │ │ └── sessions.css ├── controllers │ ├── concerns │ │ └── exception_hunter │ │ │ └── authorization.rb │ └── exception_hunter │ │ ├── application_controller.rb │ │ ├── errors_controller.rb │ │ ├── ignored_errors_controller.rb │ │ └── resolved_errors_controller.rb ├── helpers │ └── exception_hunter │ │ ├── application_helper.rb │ │ ├── errors_helper.rb │ │ └── sessions_helper.rb ├── jobs │ └── exception_hunter │ │ ├── application_job.rb │ │ ├── async_logging_job.rb │ │ └── send_notification_job.rb ├── mailers │ └── exception_hunter │ │ └── application_mailer.rb ├── models │ └── exception_hunter │ │ ├── application_record.rb │ │ ├── error.rb │ │ └── error_group.rb ├── presenters │ └── exception_hunter │ │ ├── dashboard_presenter.rb │ │ ├── error_group_presenter.rb │ │ └── error_presenter.rb └── views │ ├── exception_hunter │ ├── devise │ │ └── sessions │ │ │ └── new.html.erb │ └── errors │ │ ├── _error_backtrace.erb │ │ ├── _error_row.erb │ │ ├── _error_summary.erb │ │ ├── _error_user_data.erb │ │ ├── _errors_table.erb │ │ ├── _last_7_days_errors_table.erb │ │ ├── index.html.erb │ │ ├── pagy │ │ └── _pagy_nav.html.erb │ │ └── show.html.erb │ └── layouts │ └── exception_hunter │ ├── application.html.erb │ └── exception_hunter_logged_out.html.erb ├── bin └── rails ├── config ├── rails_best_practices.yml └── routes.rb ├── docs ├── .nojekyll ├── ExceptionHunter.html ├── ExceptionHunter │ ├── ApplicationController.html │ ├── ApplicationHelper.html │ ├── ApplicationJob.html │ ├── ApplicationMailer.html │ ├── ApplicationRecord.html │ ├── Authorization.html │ ├── Config.html │ ├── CreateUsersGenerator.html │ ├── DashboardPresenter.html │ ├── DataRedacter.html │ ├── Devise.html │ ├── Devise │ │ └── SessionsController.html │ ├── Error.html │ ├── ErrorCreator.html │ ├── ErrorGroup.html │ ├── ErrorGroupPresenter.html │ ├── ErrorPresenter.html │ ├── ErrorPresenter │ │ └── BacktraceLine.html │ ├── ErrorReaper.html │ ├── ErrorsController.html │ ├── ErrorsHelper.html │ ├── InstallGenerator.html │ ├── Middleware.html │ ├── Middleware │ │ ├── DelayedJobHunter.html │ │ ├── RequestHunter.html │ │ └── SidekiqHunter.html │ ├── Notifiers.html │ ├── Notifiers │ │ ├── MisconfiguredNotifiers.html │ │ └── SlackNotifier.html │ ├── ResolvedErrorsController.html │ ├── SendNotificationJob.html │ ├── SessionsHelper.html │ ├── Tracking.html │ └── UserAttributesCollector.html ├── _index.html ├── class_list.html ├── css │ ├── common.css │ ├── full_list.css │ └── style.css ├── file.README.html ├── file_list.html ├── frames.html ├── index-screenshot.png ├── index.html ├── js │ ├── app.js │ ├── full_list.js │ └── jquery.js ├── method_list.html └── top-level-namespace.html ├── exception_hunter.gemspec ├── lib ├── exception_hunter.rb ├── exception_hunter │ ├── config.rb │ ├── data_redacter.rb │ ├── devise.rb │ ├── engine.rb │ ├── error_creator.rb │ ├── error_reaper.rb │ ├── middleware │ │ ├── delayed_job_hunter.rb │ │ ├── request_hunter.rb │ │ └── sidekiq_hunter.rb │ ├── notifiers │ │ ├── misconfigured_notifiers.rb │ │ ├── slack_notifier.rb │ │ └── slack_notifier_serializer.rb │ ├── tracking.rb │ ├── user_attributes_collector.rb │ └── version.rb ├── generators │ └── exception_hunter │ │ ├── create_users │ │ └── create_users_generator.rb │ │ └── install │ │ ├── USAGE │ │ ├── install_generator.rb │ │ └── templates │ │ ├── create_exception_hunter_error_groups.rb.erb │ │ ├── create_exception_hunter_errors.rb.erb │ │ └── exception_hunter.rb.erb └── tasks │ ├── code_analysis.rake │ └── exception_hunter_tasks.rake └── spec ├── data_redacter_spec.rb ├── dummy ├── .ruby-version ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── images │ │ │ └── .keep │ │ └── stylesheets │ │ │ └── application.css │ ├── channels │ │ └── application_cable │ │ │ ├── channel.rb │ │ │ └── connection.rb │ ├── controllers │ │ ├── application_controller.rb │ │ ├── concerns │ │ │ └── .keep │ │ └── exception_controller.rb │ ├── helpers │ │ └── application_helper.rb │ ├── javascript │ │ └── packs │ │ │ └── application.js │ ├── jobs │ │ ├── application_job.rb │ │ └── failing_job.rb │ ├── mailers │ │ └── application_mailer.rb │ ├── models │ │ ├── admin_user.rb │ │ ├── application_record.rb │ │ ├── concerns │ │ │ └── .keep │ │ └── user.rb │ └── views │ │ └── layouts │ │ ├── application.html.erb │ │ ├── mailer.html.erb │ │ └── mailer.text.erb ├── bin │ ├── delayed_job │ ├── rails │ ├── rake │ └── setup ├── config.ru ├── config │ ├── application.rb │ ├── application_5_2.rb │ ├── application_6_0.rb │ ├── application_6_1.rb │ ├── boot.rb │ ├── cable.yml │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── application_controller_renderer.rb │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── content_security_policy.rb │ │ ├── cookies_serializer.rb │ │ ├── devise.rb │ │ ├── exception_hunter.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ ├── devise.en.yml │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ ├── spring.rb │ └── storage.yml ├── db │ ├── migrate │ │ ├── 20200421131316_devise_create_users.rb │ │ ├── 20200601134028_devise_create_admin_users.rb │ │ ├── 20200608130254_create_exception_hunter_error_groups.rb │ │ ├── 20200608130255_create_exception_hunter_errors.rb │ │ └── 20200616151049_create_delayed_jobs.rb │ └── schema.rb ├── lib │ └── assets │ │ └── .keep ├── log │ └── .keep └── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ └── favicon.ico ├── error_creator_spec.rb ├── error_reaper_spec.rb ├── exception_hunter_spec.rb ├── factories └── exception_hunter │ ├── admin_user.rb │ ├── error_groups.rb │ └── errors.rb ├── helpers └── exception_hunter │ ├── application_helper_spec.rb │ └── errors_helper_spec.rb ├── jobs ├── async_logging_job_spec.rb └── send_notification_job_spec.rb ├── middleware └── exception_hunter │ ├── delayed_job_hunter_spec.rb │ ├── request_hunter_spec.rb │ └── sidekiq_hunter_spec.rb ├── models └── exception_hunter │ ├── error_group_spec.rb │ └── error_spec.rb ├── notifiers └── slack_notifier_spec.rb ├── presenters └── exception_hunter │ ├── dashboard_presenter_spec.rb │ ├── error_group_presenter_spec.rb │ └── error_presenter_spec.rb ├── rails_helper.rb ├── requests └── exception_hunter │ ├── errors_request_spec.rb │ └── exceptions_spec.rb ├── spec_helper.rb ├── support ├── controller_routes.rb └── devise_request_spec_helpers.rb └── tasks └── purge_errors_spec.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | checks: 3 | argument-count: 4 | enabled: false 5 | config: 6 | threshold: 4 7 | complex-logic: 8 | enabled: true 9 | config: 10 | threshold: 4 11 | file-lines: 12 | enabled: false 13 | config: 14 | threshold: 250 15 | method-complexity: 16 | enabled: false 17 | config: 18 | threshold: 5 19 | method-count: 20 | enabled: false 21 | config: 22 | threshold: 20 23 | method-lines: 24 | enabled: true 25 | config: 26 | threshold: 25 27 | nested-control-flow: 28 | enabled: true 29 | config: 30 | threshold: 4 31 | return-statements: 32 | enabled: true 33 | config: 34 | threshold: 4 35 | similar-code: 36 | enabled: false 37 | config: 38 | threshold: #language-specific defaults. overrides affect all languages. 39 | identical-code: 40 | enabled: false 41 | config: 42 | threshold: #language-specific defaults. overrides affect all languages. 43 | plugins: 44 | brakeman: 45 | enabled: true 46 | channel: brakeman-4-45 47 | bundler-audit: 48 | enabled: true 49 | csslint: 50 | enabled: false 51 | duplication: 52 | enabled: true 53 | exclude_patterns: 54 | - spec/**/* 55 | flog: 56 | enabled: true 57 | exclude_patterns: 58 | - db/**/* 59 | - config/**/* 60 | - spec/**/* 61 | rubocop: 62 | enabled: true 63 | channel: rubocop-0-65 64 | config: 65 | file: '.rubocop.yml' 66 | exclude_patterns: 67 | - db/**/* 68 | - bin/ 69 | reek: 70 | enabled: true 71 | channel: reek-5-3-1 72 | config: 73 | file: '.reek.yml' 74 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, they will be 3 | # requested for review when someone opens a pull request. 4 | * @brunvez @t-romani @martinjaimem 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Bug report: 11 | * **Expected Behavior**: 12 | * **Actual Behavior**: 13 | * **Steps to Reproduce**: 14 | 1. 15 | 2. 16 | 3. 17 | 18 | * **Version of the repo**: 19 | * **Ruby and Rails Version**: 20 | * **Rails Stacktrace**: this can be found in the `log/development.log` or `log/test.log`, if this is applicable. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[FEATURE]' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | 7 | 8 | ### Other Information 9 | 10 | 13 | -------------------------------------------------------------------------------- /.github/workflows/config.yml: -------------------------------------------------------------------------------- 1 | name: Rails tests 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | ruby-version: [2.6, 2.7, 3.0] 11 | rails-version: [5_2, 6_0, 6_1] 12 | exclude: 13 | - rails-version: 5_2 14 | ruby-version: 3.0 15 | fail-fast: false 16 | 17 | # Service containers to run with `runner-job` 18 | services: 19 | postgres: 20 | image: postgres:11 21 | ports: 22 | - 5432:5432 23 | env: 24 | POSTGRES_PASSWORD: postgres 25 | POSTGRES_USER: postgres 26 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 27 | 28 | steps: 29 | - uses: actions/checkout@v2 30 | 31 | - uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: ${{ matrix.ruby-version }} 34 | 35 | - name: Copy Gemfile 36 | run: yes | cp -rf Gemfile_$RAILS_VERSION Gemfile 37 | env: 38 | RAILS_VERSION: ${{ matrix.rails-version }} 39 | 40 | - name: Copy Application.rb 41 | run: yes | cp -rf spec/dummy/config/application_$RAILS_VERSION.rb spec/dummy/config/application.rb 42 | env: 43 | RAILS_VERSION: ${{ matrix.rails-version }} 44 | 45 | 46 | - name: Install PostgreSQL 11 client 47 | run: | 48 | sudo apt-get -yqq install libpq-dev 49 | 50 | - name: Bundle install 51 | run: | 52 | gem install bundler 53 | bundle update --jobs 4 --retry 3 54 | 55 | - name: Setup Database 56 | env: 57 | PGHOST: localhost 58 | PGUSER: postgres 59 | PGPASSWORD: postgres 60 | RAILS_ENV: test 61 | RAILS_VERSION: ${{ matrix.rails-version }} 62 | run: | 63 | bundle exec rails db:setup 64 | 65 | - name: CodeClimate 66 | run: | 67 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 68 | chmod +x ./cc-test-reporter 69 | ./cc-test-reporter before-build 70 | env: 71 | CC_TEST_REPORTER_ID: 6cbfbd07cb24649965a02f72f52ade4f63042525e86075ef38f04817d4fe8a67 72 | 73 | - name: Run tests 74 | env: 75 | PGHOST: localhost 76 | PGUSER: postgres 77 | PGPASSWORD: postgres 78 | RAILS_ENV: test 79 | RAILS_VERSION: ${{ matrix.rails-version }} 80 | run: COVERAGE=true bundle exec rspec --require rails_helper 81 | 82 | - name: Run code analysis 83 | run: bundle exec rake code_analysis 84 | 85 | - name: Report to CodeClimate 86 | run: | 87 | ./cc-test-reporter after-build --exit-code 0 88 | env: 89 | CC_TEST_REPORTER_ID: 6cbfbd07cb24649965a02f72f52ade4f63042525e86075ef38f04817d4fe8a67 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | spec/dummy/log/*.log 5 | spec/dummy/storage/ 6 | spec/dummy/tmp/ 7 | .yardoc 8 | 9 | .idea/ 10 | coverage 11 | .byebug_history 12 | 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /.reek.yml: -------------------------------------------------------------------------------- 1 | detectors: 2 | Attribute: 3 | enabled: false 4 | exclude: [] 5 | 6 | BooleanParameter: 7 | enabled: true 8 | exclude: [] 9 | 10 | ClassVariable: 11 | enabled: false 12 | exclude: [] 13 | 14 | ControlParameter: 15 | enabled: true 16 | exclude: 17 | - ExceptionHunter::ErrorCreator 18 | 19 | DataClump: 20 | enabled: true 21 | exclude: 22 | - ExceptionHunter::ErrorCreator 23 | - ExceptionHunter::Tracking 24 | max_copies: 2 25 | min_clump_size: 2 26 | 27 | DuplicateMethodCall: 28 | enabled: false 29 | exclude: [] 30 | max_calls: 2 31 | allow_calls: [] 32 | 33 | FeatureEnvy: 34 | enabled: false 35 | exclude: [] 36 | 37 | InstanceVariableAssumption: 38 | enabled: false 39 | 40 | IrresponsibleModule: 41 | enabled: false 42 | exclude: [] 43 | 44 | LongParameterList: 45 | enabled: true 46 | exclude: [] 47 | max_params: 4 48 | overrides: 49 | initialize: 50 | max_params: 5 51 | 52 | LongYieldList: 53 | enabled: true 54 | exclude: [] 55 | max_params: 3 56 | 57 | NestedIterators: 58 | enabled: true 59 | exclude: [] 60 | max_allowed_nesting: 2 61 | ignore_iterators: [] 62 | 63 | NilCheck: 64 | enabled: false 65 | exclude: [] 66 | 67 | RepeatedConditional: 68 | enabled: true 69 | exclude: [] 70 | max_ifs: 3 71 | 72 | TooManyInstanceVariables: 73 | enabled: true 74 | exclude: [] 75 | max_instance_variables: 9 76 | 77 | TooManyMethods: 78 | enabled: false 79 | exclude: [] 80 | max_methods: 25 81 | 82 | TooManyStatements: 83 | enabled: false 84 | exclude: 85 | - initialize 86 | max_statements: 12 87 | 88 | TooManyConstants: 89 | enabled: false 90 | 91 | UncommunicativeMethodName: 92 | enabled: true 93 | exclude: [] 94 | reject: 95 | - "/^[a-z]$/" 96 | - "/[0-9]$/" 97 | - "/[A-Z]/" 98 | accept: [] 99 | 100 | UncommunicativeModuleName: 101 | enabled: true 102 | exclude: ["V1"] 103 | reject: 104 | - "/^.$/" 105 | - "/[0-9]$/" 106 | accept: 107 | - Inline::C 108 | 109 | UncommunicativeParameterName: 110 | enabled: true 111 | exclude: [] 112 | reject: 113 | - "/^.$/" 114 | - "/[0-9]$/" 115 | - "/[A-Z]/" 116 | accept: [] 117 | 118 | UncommunicativeVariableName: 119 | enabled: true 120 | exclude: 121 | - ExceptionHunter::RequestHunter#call 122 | - ExceptionHunter::UserAttributesCollector#collect_attributes 123 | reject: 124 | - "/^.$/" 125 | - "/[0-9]$/" 126 | - "/[A-Z]/" 127 | accept: 128 | - _ 129 | 130 | UnusedParameters: 131 | enabled: true 132 | exclude: [] 133 | 134 | UnusedPrivateMethod: 135 | enabled: true 136 | exclude: 137 | - ExceptionHunter::Error 138 | 139 | UtilityFunction: 140 | enabled: false 141 | 142 | exclude_paths: 143 | - config 144 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require rails_helper 3 | --order random 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.5.4 3 | Exclude: 4 | - "spec/dummy/config/**/*" 5 | - "spec/dummy/db/**/*" 6 | - "config/initializers/**/*" 7 | 8 | Documentation: 9 | Enabled: false 10 | 11 | Lint/AmbiguousBlockAssociation: 12 | Exclude: 13 | - spec/**/* 14 | 15 | Lint/RescueException: 16 | Exclude: 17 | - lib/exception_hunter/request_hunter.rb 18 | - app/jobs/exception_hunter/async_logging_job.rb 19 | 20 | Metrics/AbcSize: 21 | Enabled: false 22 | # The ABC size is a calculated magnitude, so this number can be an Integer or 23 | # a Float. 24 | Max: 18 25 | 26 | Metrics/BlockLength: 27 | CountComments: false # count full line comments? 28 | Max: 25 29 | Exclude: 30 | - config/**/* 31 | - spec/**/* 32 | Severity: warning 33 | 34 | Metrics/BlockNesting: 35 | Max: 4 36 | Enabled: false 37 | 38 | Metrics/ClassLength: 39 | CountComments: false # count full line comments? 40 | Max: 200 41 | Enabled: false 42 | 43 | # Avoid complex methods. 44 | Metrics/CyclomaticComplexity: 45 | Max: 6 46 | Enabled: false 47 | 48 | Metrics/MethodLength: 49 | CountComments: false # count full line comments? 50 | Max: 24 51 | Severity: warning 52 | 53 | Metrics/ModuleLength: 54 | CountComments: false # count full line comments? 55 | Max: 200 56 | Enabled: false 57 | 58 | Layout/LineLength: 59 | Max: 120 60 | AllowURI: true 61 | URISchemes: 62 | - http 63 | - https 64 | Exclude: 65 | - spec/**/* 66 | 67 | Metrics/ParameterLists: 68 | Max: 5 69 | CountKeywordArgs: true 70 | Enabled: false 71 | 72 | Metrics/PerceivedComplexity: 73 | Max: 12 74 | Enabled: false 75 | 76 | Naming/AccessorMethodName: 77 | Exclude: [] 78 | 79 | Naming/RescuedExceptionsVariableName: 80 | Enabled: false 81 | 82 | Style/FrozenStringLiteralComment: 83 | Enabled: false 84 | 85 | Style/ModuleFunction: 86 | Enabled: false 87 | 88 | Style/BlockDelimiters: 89 | EnforcedStyle: braces_for_chaining 90 | 91 | Style/BlockComments: 92 | Exclude: 93 | - spec/spec_helper.rb 94 | 95 | Style/HashEachMethods: 96 | Enabled: true 97 | 98 | Style/HashTransformKeys: 99 | Enabled: true 100 | 101 | Style/HashTransformValues: 102 | Enabled: true 103 | 104 | Style/StringLiterals: 105 | Exclude: 106 | - spec/dummy/**/* 107 | 108 | Style/RescueModifier: 109 | Exclude: 110 | - spec/**/* 111 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | exception_hunter 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.6.5 2 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --output-dir './docs' 2 | --hide-void-return 3 | --no-private 4 | --embed-mixin ExceptionHunter::Tracking 5 | --exclude app 6 | --exclude lib/generators 7 | --exclude lib/devise 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | ## master (unreleased) 3 | 4 | ### New features 5 | 6 | ### Bug fixes 7 | 8 | ## 1.1.1 (2021-10-08) 9 | 10 | ### Changes 11 | 12 | - [#127](https://github.com/rootstrap/exception_hunter/pull/127) Upgrade to Pagy from 3.x to 4.x ([@megatux][]) 13 | 14 | ## 1.1.0 (2021-07-30) 15 | 16 | ### New features 17 | 18 | - [#113](https://github.com/rootstrap/exception_hunter/pull/113) Add configuration to turn on/off async logging using ActionJob. ( 19 | [@matteo95g][]) 20 | - [#119](https://github.com/rootstrap/exception_hunter/pull/119) Add support for tracking manual exceptions within transactions. ([@lalopsb][]) 21 | ### Others 22 | 23 | - [#114](https://github.com/rootstrap/exception_hunter/pull/114) Fix development vulnerabilities ([@brunvez][]) 24 | - [#115](https://github.com/rootstrap/exception_hunter/pull/115) Fix Github actions running multiple times. ([@brunvez][]) 25 | - [#116](https://github.com/rootstrap/exception_hunter/pull/116) Fix documentation link on the README. ([@brunvez][]) 26 | - [#117](https://github.com/rootstrap/exception_hunter/pull/117) Fix specification on example for Manual tracking on the README ([@lalopsb][]) 27 | - [#118](https://github.com/rootstrap/exception_hunter/pull/118) Refactor code duplication on errors for last week partial ([@lalopsb][]) 28 | ## 1.0.2 (2020-02-03) 29 | 30 | ### Bug fixes 31 | 32 | - [#110](https://github.com/rootstrap/exception_hunter/pull/110) Fix: purge errors task. ( 33 | [@martinjaimem][]) 34 | - [#111](https://github.com/rootstrap/exception_hunter/pull/111) Send notifications with delay. ( 35 | [@martinjaimem][]) 36 | 37 | ### Changes 38 | 39 | ## 1.0.1 (2020-12-18) 40 | 41 | ### Changes 42 | 43 | - [#107](https://github.com/rootstrap/exception_hunter/pull/107) Excluded code_analysis task from the build. ([@andresg4][]) 44 | 45 | ## 1.0.0 (2020-11-05) 46 | 47 | ### New features 48 | 49 | - [#88](https://github.com/rootstrap/exception_hunter/pull/88) Add slack notifications. ([@andresg4][]) 50 | - [#93](https://github.com/rootstrap/exception_hunter/pull/93) Show project name instead of repo name on navbar. ([@yurichandra][]) 51 | - [#101](https://github.com/rootstrap/exception_hunter/pull/101) Allow user to ignore certain errors. ([@ajazfarhad][]) 52 | - [#104](https://github.com/rootstrap/exception_hunter/pull/104) Filter sensitive data. ([@andresg4][]) 53 | 54 | ### Bug fixes 55 | 56 | - [#98](https://github.com/rootstrap/exception_hunter/pull/98) Fix module_parent_name not present on rails < 6.0.0. ([@ivoloshy][], [@Snick555][]) 57 | 58 | ### Changes 59 | 60 | - [#100](https://github.com/rootstrap/exception_hunter/pull/100) Add CHANGELOG. ([@brunvez][]) 61 | - [#99](https://github.com/rootstrap/exception_hunter/pull/99) Add matrix testing. ([@brunvez][]) 62 | - [#92](https://github.com/rootstrap/exception_hunter/pull/92) Add documentation on how to test on dev. ([@brunvez][]) 63 | - [#87](https://github.com/rootstrap/exception_hunter/pull/87) Add manual tracking to documentation. ([@brunvez][]) 64 | 65 | ## 0.4.2 (2020-09-17) 66 | 67 | ### Bug fixes 68 | 69 | - [#84](https://github.com/rootstrap/exception_hunter/pull/84) Fix constants not being found correctly. ([@brunvez][]) 70 | - [#84](https://github.com/rootstrap/exception_hunter/pull/84) Fix error with no backtrace breaking the dashboard. ([@brunvez][]) 71 | - [#84](https://github.com/rootstrap/exception_hunter/pull/84) Fix manually tracked error having `nil` environment data. ([@brunvez][]) 72 | 73 | ### Changes 74 | 75 | - [#83](https://github.com/rootstrap/exception_hunter/pull/83) Add PR and ISSUE templates. ([@vitogit][]) 76 | - [#82](https://github.com/rootstrap/exception_hunter/pull/82) Create CONTRIBUTING.md. ([@vitogit][]) 77 | - [#81](https://github.com/rootstrap/exception_hunter/pull/81) Create CODE_OF_CONDUCT.md. ([@vitogit][]) 78 | 79 | ## 0.4.1 (2020-07-20) 80 | 81 | ### Bug fixes 82 | 83 | - [#77](https://github.com/rootstrap/exception_hunter/pull/77) Fix ErrorPresenter failing when the error does not have environment data. ([@SandroDamilano][]) 84 | 85 | ## < 0.4.0 86 | 87 | - Lots of features ([@t-romani][]) 88 | 89 | [@brunvez]: https://github.com/brunvez 90 | [@andresg4]: https://github.com/andresg4 91 | [@ivoloshy]: https://github.com/ivoloshy 92 | [@lalopsb]: https://github.com/lalopsb 93 | [@matteo95g]: https://github.com/matteo95g 94 | [@sandrodamilano]: https://github.com/SandroDamilano 95 | [@snick555]: https://github.com/Snick555 96 | [@t-romani]: https://github.com/t-romani 97 | [@vitogit]: https://github.com/vitogit 98 | [@yurichandra]: https://github.com/yurichandra 99 | [@ajazfarhad]: https://github.com/ajazfarhad 100 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ruby.maintainers@rootstrap.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing ## 2 | 3 | You can contribute to this repo if you have an issue, found a bug or think there's some functionality required that would add value to the gem. To do so, please check if there's not already an [issue](https://github.com/rootstrap/exception_hunter/issues) for that, if you find there's not, create a new one with as much detail as possible. 4 | 5 | If you want to contribute with code as well, please follow the next steps: 6 | 7 | 1. Read, understand and agree to our [code of conduct](https://github.com/rootstrap/exception_hunter/blob/master/CODE_OF_CONDUCT.md) 8 | 2. [Fork the repo](https://help.github.com/articles/about-forks/) 9 | 3. Clone the project into your machine: 10 | `$ git clone git@github.com:rootstrap/exception_hunter.git` 11 | 4. Access the repo: 12 | `$ cd exception_hunter` 13 | 5. Create your feature/bugfix branch: 14 | `$ git checkout -b your_new_feature` 15 | or 16 | `$ git checkout -b fix/your_fix` in case of a bug fix 17 | (if your PR is to address an existing issue, it would be good to name the branch after the issue, for example: if you are trying to solve issue 182, then a good idea for the branch name would be `182_your_new_feature`) 18 | 6. Write tests for your changes (feature/bug) 19 | 7. Code your (feature/bugfix) 20 | 8. Run the code analysis tool by doing: 21 | `$ rake code_analysis` 22 | 9. Run the tests: 23 | `$ bundle exec rspec` 24 | All tests must pass. If all tests (both code analysis and rspec) do pass, then you are ready to go to the next step: 25 | 10. Commit your changes: 26 | `$ git commit -m 'Your feature or bugfix title'` 27 | 11. Push to the branch `$ git push origin your_new_feature` 28 | 12. Create a new [pull request](https://help.github.com/articles/creating-a-pull-request/) 29 | 30 | Some helpful guides that will help you know how we work: 31 | 1. [Code review](https://github.com/rootstrap/tech-guides/tree/master/code-review) 32 | 2. [GIT workflow](https://github.com/rootstrap/tech-guides/tree/master/git) 33 | 3. [Ruby style guide](https://github.com/rootstrap/tech-guides/tree/master/ruby) 34 | 4. [Rails style guide](https://github.com/rootstrap/tech-guides/blob/master/ruby/rails.md) 35 | 5. [RSpec style guide](https://github.com/rootstrap/tech-guides/blob/master/ruby/rspec/README.md) 36 | 37 | For more information or guides like the ones mentioned above, please check our [tech guides](https://github.com/rootstrap/tech-guides). Keep in mind that the more you know about these guides, the easier it will be for your code to get approved and merged. 38 | 39 | Note: You can push as many commits as you want when working on a pull request, we just ask that they are descriptive and tell a story. Try to open a pull request with just one commit but if you think you need to divide what you did into more commits to convey what you are trying to do go for it. 40 | 41 | Thank you very much for your time and for considering helping in this project. 42 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | # Declare your gem's dependencies in exception_hunter.gemspec. 5 | # Bundler will treat runtime dependencies like base dependencies, and 6 | # development dependencies will be added by default to the :development group. 7 | gemspec 8 | 9 | group :development, :test do 10 | gem 'byebug', '~> 11.1' 11 | gem 'delayed_job_active_record', '~> 4.1', '>= 4.1.4' 12 | gem 'devise', '~> 4.7' 13 | gem 'rails', '~> 6.1' 14 | gem 'sidekiq', '~> 6.0.4' 15 | gem 'yard', '~> 0.9.25' 16 | end 17 | 18 | group :test do 19 | gem 'rails-controller-testing', '~> 1.0.4' 20 | gem 'rspec-rails', '~> 4.0' 21 | gem 'shoulda-matchers', '~> 4.3' 22 | 23 | # https://github.com/rspec/rspec-rails/issues/2177 24 | gem 'rspec-core', git: 'https://github.com/rspec/rspec-core' 25 | gem 'rspec-expectations', git: 'https://github.com/rspec/rspec-expectations' 26 | gem 'rspec-mocks', git: 'https://github.com/rspec/rspec-mocks' 27 | gem 'rspec-support', git: 'https://github.com/rspec/rspec-support' 28 | end 29 | -------------------------------------------------------------------------------- /Gemfile_5_2: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | # Declare your gem's dependencies in exception_hunter.gemspec. 5 | # Bundler will treat runtime dependencies like base dependencies, and 6 | # development dependencies will be added by default to the :development group. 7 | gemspec 8 | 9 | group :development, :test do 10 | gem 'byebug', '~> 11.1' 11 | gem 'delayed_job_active_record', '~> 4.1', '>= 4.1.4' 12 | gem 'devise', '~> 4.7' 13 | gem 'rails', '~> 5.2' 14 | gem 'sidekiq', '~> 6.0.4' 15 | end 16 | 17 | group :test do 18 | gem 'rails-controller-testing', '~> 1.0.4' 19 | gem 'rspec-rails', '~> 4.0' 20 | gem 'shoulda-matchers', '~> 4.3' 21 | 22 | # https://github.com/rspec/rspec-rails/issues/2177 23 | gem 'rspec-core', git: 'https://github.com/rspec/rspec-core' 24 | gem 'rspec-expectations', git: 'https://github.com/rspec/rspec-expectations' 25 | gem 'rspec-mocks', git: 'https://github.com/rspec/rspec-mocks' 26 | gem 'rspec-support', git: 'https://github.com/rspec/rspec-support' 27 | end 28 | -------------------------------------------------------------------------------- /Gemfile_6_0: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | # Declare your gem's dependencies in exception_hunter.gemspec. 5 | # Bundler will treat runtime dependencies like base dependencies, and 6 | # development dependencies will be added by default to the :development group. 7 | gemspec 8 | 9 | group :development, :test do 10 | gem 'byebug', '~> 11.1' 11 | gem 'delayed_job_active_record', '~> 4.1', '>= 4.1.4' 12 | gem 'devise', '~> 4.7' 13 | gem 'rails', '~> 6.0' 14 | gem 'sidekiq', '~> 6.0.4' 15 | end 16 | 17 | group :test do 18 | gem 'rails-controller-testing', '~> 1.0.4' 19 | gem 'rspec-rails', '~> 4.0' 20 | gem 'shoulda-matchers', '~> 4.3' 21 | 22 | # https://github.com/rspec/rspec-rails/issues/2177 23 | gem 'rspec-core', git: 'https://github.com/rspec/rspec-core' 24 | gem 'rspec-expectations', git: 'https://github.com/rspec/rspec-expectations' 25 | gem 'rspec-mocks', git: 'https://github.com/rspec/rspec-mocks' 26 | gem 'rspec-support', git: 'https://github.com/rspec/rspec-support' 27 | end 28 | -------------------------------------------------------------------------------- /Gemfile_6_1: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | # Declare your gem's dependencies in exception_hunter.gemspec. 5 | # Bundler will treat runtime dependencies like base dependencies, and 6 | # development dependencies will be added by default to the :development group. 7 | gemspec 8 | 9 | group :development, :test do 10 | gem 'byebug', '~> 11.1' 11 | gem 'delayed_job_active_record', '~> 4.1', '>= 4.1.4' 12 | gem 'devise', '~> 4.7' 13 | gem 'rails', '~> 6.1' 14 | gem 'sidekiq', '~> 6.0.4' 15 | end 16 | 17 | group :test do 18 | gem 'rails-controller-testing', '~> 1.0.4' 19 | gem 'rspec-rails', '~> 4.0' 20 | gem 'shoulda-matchers', '~> 4.3' 21 | 22 | # https://github.com/rspec/rspec-rails/issues/2177 23 | gem 'rspec-core', git: 'https://github.com/rspec/rspec-core' 24 | gem 'rspec-expectations', git: 'https://github.com/rspec/rspec-expectations' 25 | gem 'rspec-mocks', git: 'https://github.com/rspec/rspec-mocks' 26 | gem 'rspec-support', git: 'https://github.com/rspec/rspec-support' 27 | end 28 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Bruno Vezoli 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'ExceptionHunter' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.md') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__) 18 | load 'rails/tasks/engine.rake' 19 | 20 | load 'rails/tasks/statistics.rake' 21 | 22 | load 'lib/tasks/code_analysis.rake' 23 | -------------------------------------------------------------------------------- /app/assets/config/exception_hunter_manifest.js: -------------------------------------------------------------------------------- 1 | //= link_directory ../stylesheets/exception_hunter .css 2 | //= link_directory ../images/exception_hunter 3 | 4 | -------------------------------------------------------------------------------- /app/assets/images/exception_hunter/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/exception_hunter/b6f7cab9e9a9157147c5a165d0b79762c6158664/app/assets/images/exception_hunter/.keep -------------------------------------------------------------------------------- /app/assets/images/exception_hunter/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/exception_hunter/b6f7cab9e9a9157147c5a165d0b79762c6158664/app/assets/images/exception_hunter/logo.png -------------------------------------------------------------------------------- /app/assets/stylesheets/exception_hunter/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/exception_hunter/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background-grey: #E5E5E5; 3 | --highlight-color: #CF4031; 4 | --highlight-good-color: #2CBB85; 5 | --highlight-inactive-color: #808183; 6 | --inactive-grey: #D1D1D3; 7 | --focused-grey: #808183; 8 | --highlighted-link-blue: #0036F7; 9 | --header-grey: #F8F8F8; 10 | --border-grey: #F1F2F5; 11 | --file-name-color: #2CBB85; 12 | --tag-color: #EAE639; 13 | } 14 | 15 | body { 16 | font-family: 'Inter', sans-serif; 17 | background-color: var(--background-grey); 18 | } 19 | 20 | .container { 21 | padding: 0; 22 | } 23 | 24 | .row { 25 | margin: 0; 26 | width: 100%; 27 | } 28 | 29 | .row .column { 30 | padding: 0; 31 | margin-bottom: 0; 32 | } 33 | 34 | .wrapper { 35 | margin: 6.5rem auto auto; 36 | } 37 | 38 | .text--underline { 39 | text-decoration: underline; 40 | } 41 | 42 | a { 43 | color: inherit; 44 | } 45 | 46 | a:hover, a:focus, a:active { 47 | color: inherit; 48 | } 49 | 50 | form { 51 | margin-bottom: 0; 52 | } 53 | 54 | .flash.flash--notice { 55 | background-color: #FFF; 56 | margin-bottom: 2rem; 57 | padding: 0.7rem 2rem; 58 | border-radius: 5px; 59 | line-height: 3.8rem; 60 | font-weight: 400; 61 | } 62 | 63 | .button.button-dismiss { 64 | margin-bottom: 0; 65 | padding: 0; 66 | color: var(--focused-grey); 67 | width: 100%; 68 | text-align: right; 69 | } 70 | 71 | .button.button-dismiss:hover, .button.button-dismiss:focus, .button.button-dismiss:active { 72 | color: var(--inactive-grey); 73 | } 74 | 75 | .mr-5{ 76 | margin-right: 5px; 77 | } 78 | -------------------------------------------------------------------------------- /app/assets/stylesheets/exception_hunter/navigation.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | background: #000; 3 | display: block; 4 | height: 4.2rem; 5 | left: 0; 6 | max-width: 100%; 7 | position: fixed; 8 | right: 0; 9 | top: 0; 10 | width: 100%; 11 | z-index: 1; 12 | } 13 | .container__nav { 14 | display: flex; 15 | height: 100%; 16 | } 17 | 18 | .nav__title { 19 | line-height: 4.2rem; 20 | font-style: normal; 21 | font-weight: 600; 22 | font-size: 16px; 23 | color: #FFF; 24 | } 25 | 26 | .footer { 27 | text-align: center; 28 | padding-top: 4rem; 29 | padding-bottom: 1rem; 30 | } 31 | 32 | .logout { 33 | line-height: 4.2rem; 34 | color: #FFF; 35 | text-align: right; 36 | } 37 | -------------------------------------------------------------------------------- /app/assets/stylesheets/exception_hunter/sessions.css: -------------------------------------------------------------------------------- 1 | .login_form_container { 2 | margin: 0rem auto auto; 3 | display: flex; 4 | max-width: 75%; 5 | margin-top: 20rem; 6 | width: 910px; 7 | height: 356px; 8 | font-family: 'Inter', sans-serif; 9 | background-color: white; 10 | } 11 | 12 | .login_left_container { 13 | width: 455px; 14 | text-align: center; 15 | width: 38rem; 16 | background-color: black; 17 | 18 | } 19 | 20 | .left_column { 21 | max-width: 50%; 22 | padding-top: 15%; 23 | padding-left: 10%; 24 | line-height: 130%; 25 | color: white; 26 | text-align: left; 27 | font-size: 30px; 28 | font-weight: 600; 29 | } 30 | 31 | .login_right_container { 32 | width: 350px; 33 | margin: 3rem 3.5rem 1rem; 34 | font-size: 24px; 35 | font-weight: 400; 36 | text-align: left; 37 | line-height: 29px; 38 | line-height: 100%; 39 | color: black; 40 | } 41 | 42 | .login_row { 43 | margin: 2rem 0rem 0rem; 44 | width: 100%; 45 | align-items: center; 46 | } 47 | 48 | .login_button{ 49 | margin: 3rem 3rem 0rem; 50 | align-items: right; 51 | } 52 | 53 | .button-log-in { 54 | background-color: #2CBB85!important; 55 | border-color: #2CBB85!important; 56 | } 57 | 58 | .field{ 59 | border: 0.1rem solid black; 60 | border-radius: .4rem; 61 | } 62 | 63 | input[type='password'], 64 | input[type='email'], 65 | textarea:focus, 66 | select:focus { 67 | border-color: black!important; 68 | outline: 0; 69 | font-size: 1.2rem!important; 70 | color: black!important; 71 | } 72 | -------------------------------------------------------------------------------- /app/controllers/concerns/exception_hunter/authorization.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | module Authorization 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | before_action :authenticate_admin_user_class 7 | end 8 | 9 | def authenticate_admin_user_class 10 | return unless ExceptionHunter::Config.auth_enabled? && !send("current_#{underscored_admin_user_class}") 11 | 12 | redirect_to '/exception_hunter/login' 13 | end 14 | 15 | def redirect_to_login 16 | render 'exception_hunter/devise/sessions/new' 17 | end 18 | 19 | def underscored_admin_user_class 20 | ExceptionHunter::Config.admin_user_class.underscore 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/exception_hunter/application_controller.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | class ApplicationController < ActionController::Base 3 | include ExceptionHunter::Authorization 4 | 5 | protect_from_forgery with: :exception 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/exception_hunter/errors_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'exception_hunter/application_controller' 2 | 3 | module ExceptionHunter 4 | class ErrorsController < ApplicationController 5 | include Pagy::Backend 6 | 7 | def index 8 | @dashboard = DashboardPresenter.new(current_tab) 9 | shown_errors = errors_for_tab(@dashboard).order(updated_at: :desc).distinct 10 | @errors = ErrorGroupPresenter.wrap_collection(shown_errors) 11 | end 12 | 13 | def show 14 | @pagy, errors = pagy(most_recent_errors, items: 1) 15 | @error = ErrorPresenter.new(errors.first!) 16 | end 17 | 18 | def destroy 19 | ErrorReaper.purge 20 | 21 | redirect_back fallback_location: errors_path, notice: 'Errors purged successfully' 22 | end 23 | 24 | private 25 | 26 | def most_recent_errors 27 | Error.most_recent(params[:id]) 28 | end 29 | 30 | def current_tab 31 | params[:tab] 32 | end 33 | 34 | def errors_for_tab(dashboard) 35 | case dashboard.current_tab 36 | when DashboardPresenter::LAST_7_DAYS_TAB 37 | ErrorGroup.with_errors_in_last_7_days.active 38 | when DashboardPresenter::CURRENT_MONTH_TAB 39 | ErrorGroup.with_errors_in_current_month.active 40 | when DashboardPresenter::TOTAL_ERRORS_TAB 41 | ErrorGroup.active 42 | when DashboardPresenter::RESOLVED_ERRORS_TAB 43 | ErrorGroup.resolved 44 | when DashboardPresenter::IGNORED_ERRORS_TAB 45 | ErrorGroup.ignored 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/controllers/exception_hunter/ignored_errors_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'exception_hunter/application_controller' 2 | 3 | module ExceptionHunter 4 | class IgnoredErrorsController < ApplicationController 5 | def create 6 | error_group.ignored! 7 | redirect_to errors_path, notice: 'Error ignored successfully' 8 | end 9 | 10 | def reopen 11 | error_group.active! 12 | redirect_to errors_path, notice: 'Error re-opened successfully' 13 | end 14 | 15 | private 16 | 17 | def error_group 18 | @error_group ||= ErrorGroup.find(error_group_params[:id]) 19 | end 20 | 21 | def error_group_params 22 | params.require(:error_group).permit(:id) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/exception_hunter/resolved_errors_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'exception_hunter/application_controller' 2 | 3 | module ExceptionHunter 4 | class ResolvedErrorsController < ApplicationController 5 | def create 6 | ErrorGroup.find(params[:error_group][:id]).resolved! 7 | 8 | redirect_to errors_path, notice: 'Error resolved successfully' 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/helpers/exception_hunter/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | module ApplicationHelper 3 | include Pagy::Frontend 4 | 5 | def application_name 6 | if defined? Rails.application.class.module_parent_name 7 | Rails.application.class.module_parent_name 8 | else 9 | Rails.application.class.parent_name 10 | end 11 | end 12 | 13 | def display_action_button(title, error) 14 | button_to(title.to_s, route_for_button(title, error), 15 | class: "button button-outline #{title}-button", 16 | data: { confirm: "Are you sure you want to #{title} this error?" }).to_s 17 | end 18 | 19 | def route_for_button(title, error) 20 | if title.eql?('ignore') 21 | ignored_errors_path(error_group: { id: error.id }) 22 | else 23 | resolved_errors_path(error_group: { id: error.id }) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/helpers/exception_hunter/errors_helper.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | module ErrorsHelper 3 | def format_tracked_data(tracked_data) 4 | JSON.pretty_generate(tracked_data) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/helpers/exception_hunter/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | module SessionsHelper 3 | def current_admin_user? 4 | underscored_admin_user_class && 5 | current_admin_class_name(underscored_admin_user_class) 6 | end 7 | 8 | def underscored_admin_user_class 9 | ExceptionHunter::Config.admin_user_class.try(:underscore) 10 | end 11 | 12 | def current_admin_class_name(class_name) 13 | send("current_#{class_name.underscore}") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/jobs/exception_hunter/application_job.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | class ApplicationJob < ActiveJob::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/jobs/exception_hunter/async_logging_job.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | class AsyncLoggingJob < ApplicationJob 3 | queue_as :default 4 | 5 | def perform(tag, error_attrs) 6 | error_attrs = error_attrs.merge(occurred_at: Time.at(error_attrs[:occurred_at])) if error_attrs[:occurred_at] 7 | ErrorCreator.call(async_logging: false, tag: tag, **error_attrs) 8 | rescue Exception 9 | # Suppress all exceptions to avoid loop as this would create a new error in EH. 10 | false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/jobs/exception_hunter/send_notification_job.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | class SendNotificationJob < ApplicationJob 3 | queue_as :default 4 | 5 | def perform(serialized_notifier) 6 | # Use SlackNotifierSerializer as it's the only one for now. 7 | serializer = ExceptionHunter::Notifiers::SlackNotifierSerializer 8 | deserialized_notifier = serializer.deserialize(serialized_notifier) 9 | deserialized_notifier.notify 10 | rescue Exception # rubocop:disable Lint/RescueException 11 | # Suppress all exceptions to avoid loop as this would create a new error in EH. 12 | false 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/mailers/exception_hunter/application_mailer.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | class ApplicationMailer < ActionMailer::Base 3 | default from: 'from@example.com' 4 | layout 'mailer' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/models/exception_hunter/application_record.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | class ApplicationRecord < ActiveRecord::Base 3 | self.abstract_class = true 4 | 5 | class << self 6 | delegate :[], to: :arel_table 7 | 8 | def sql_similarity(attr, value) 9 | Arel::Nodes::NamedFunction.new('similarity', [attr, Arel::Nodes.build_quoted(value)]) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/exception_hunter/error.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | class Error < ::ExceptionHunter::ApplicationRecord 3 | validates :class_name, presence: true 4 | validates :occurred_at, presence: true 5 | 6 | belongs_to :error_group, touch: true 7 | 8 | before_validation :set_occurred_at, on: :create 9 | after_create :unresolve_error_group, if: -> { error_group.resolved? } 10 | 11 | scope :most_recent, lambda { |error_group_id| 12 | where(error_group_id: error_group_id).order(occurred_at: :desc) 13 | } 14 | scope :with_occurrences_before, lambda { |max_occurrence_date| 15 | where(Error[:occurred_at].lteq(max_occurrence_date)) 16 | } 17 | scope :in_period, ->(period) { where(occurred_at: period) } 18 | scope :in_last_7_days, -> { in_period(7.days.ago.beginning_of_day..Time.now) } 19 | scope :in_current_month, lambda { 20 | in_period(Date.current.beginning_of_month.beginning_of_day..Date.current.end_of_month.end_of_day) 21 | } 22 | scope :from_active_error_groups, lambda { 23 | joins(:error_group).where(error_group: ErrorGroup.active) 24 | } 25 | scope :from_resolved_error_groups, lambda { 26 | joins(:error_group).where(error_group: ErrorGroup.resolved) 27 | } 28 | 29 | scope :from_ignored_error_groups, lambda { 30 | joins(:error_group).where(error_group: ErrorGroup.ignored) 31 | } 32 | 33 | private 34 | 35 | def set_occurred_at 36 | self.occurred_at ||= Time.now 37 | end 38 | 39 | def unresolve_error_group 40 | error_group.active! 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/models/exception_hunter/error_group.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | class ErrorGroup < ::ExceptionHunter::ApplicationRecord 3 | SIMILARITY_THRESHOLD = 0.75 4 | 5 | validates :error_class_name, presence: true 6 | 7 | has_many :grouped_errors, class_name: 'ExceptionHunter::Error', dependent: :destroy 8 | 9 | enum status: { active: 0, resolved: 1, ignored: 2 } 10 | 11 | scope :most_similar, lambda { |message| 12 | message_similarity = sql_similarity(ErrorGroup[:message], message) 13 | where(message_similarity.gteq(SIMILARITY_THRESHOLD)) 14 | .order(message_similarity.desc) 15 | } 16 | 17 | scope :without_errors, lambda { 18 | is_associated_error = Error[:error_group_id].eq(ErrorGroup[:id]) 19 | where.not(Error.where(is_associated_error).arel.exists) 20 | } 21 | scope :with_errors_in_last_7_days, lambda { 22 | joins(:grouped_errors) 23 | .where(Error.in_last_7_days.where(Error[:error_group_id].eq(ErrorGroup[:id])).arel.exists) 24 | } 25 | scope :with_errors_in_current_month, lambda { 26 | joins(:grouped_errors) 27 | .where(Error.in_current_month.where(Error[:error_group_id].eq(ErrorGroup[:id])).arel.exists) 28 | } 29 | 30 | def self.find_matching_group(error) 31 | where(error_class_name: error.class_name) 32 | .most_similar(error.message.to_s) 33 | .first 34 | end 35 | 36 | def first_occurrence 37 | @first_occurrence ||= grouped_errors.minimum(:occurred_at) 38 | end 39 | 40 | def last_occurrence 41 | @last_occurrence ||= grouped_errors.maximum(:occurred_at) 42 | end 43 | 44 | def total_occurrences 45 | @total_occurrences ||= grouped_errors.count 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/presenters/exception_hunter/dashboard_presenter.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | class DashboardPresenter 3 | LAST_7_DAYS_TAB = 'last_7_days'.freeze 4 | CURRENT_MONTH_TAB = 'current_month'.freeze 5 | TOTAL_ERRORS_TAB = 'total_errors'.freeze 6 | RESOLVED_ERRORS_TAB = 'resolved'.freeze 7 | IGNORED_ERRORS_TAB = 'ignored'.freeze 8 | TABS = [LAST_7_DAYS_TAB, CURRENT_MONTH_TAB, TOTAL_ERRORS_TAB, RESOLVED_ERRORS_TAB, IGNORED_ERRORS_TAB].freeze 9 | DEFAULT_TAB = LAST_7_DAYS_TAB 10 | 11 | attr_reader :current_tab 12 | 13 | def initialize(current_tab) 14 | assign_tab(current_tab) 15 | calculate_tabs_counts 16 | end 17 | 18 | def tab_active?(tab) 19 | tab == current_tab 20 | end 21 | 22 | def partial_for_tab 23 | case current_tab 24 | when LAST_7_DAYS_TAB 25 | 'exception_hunter/errors/last_7_days_errors_table' 26 | when CURRENT_MONTH_TAB, TOTAL_ERRORS_TAB, RESOLVED_ERRORS_TAB, IGNORED_ERRORS_TAB 27 | 'exception_hunter/errors/errors_table' 28 | end 29 | end 30 | 31 | def errors_count(tab) 32 | @tabs_counts[tab] 33 | end 34 | 35 | private 36 | 37 | def assign_tab(tab) 38 | @current_tab = if TABS.include?(tab) 39 | tab 40 | else 41 | DEFAULT_TAB 42 | end 43 | end 44 | 45 | def calculate_tabs_counts 46 | active_errors = Error.from_active_error_groups 47 | @tabs_counts = { 48 | LAST_7_DAYS_TAB => active_errors.in_last_7_days.count, 49 | CURRENT_MONTH_TAB => active_errors.in_current_month.count, 50 | TOTAL_ERRORS_TAB => active_errors.count, 51 | RESOLVED_ERRORS_TAB => Error.from_resolved_error_groups.count, 52 | IGNORED_ERRORS_TAB => Error.from_ignored_error_groups.count 53 | } 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/presenters/exception_hunter/error_group_presenter.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | class ErrorGroupPresenter 3 | delegate_missing_to :error_group 4 | 5 | def initialize(error_group) 6 | @error_group = error_group 7 | end 8 | 9 | def self.wrap_collection(collection) 10 | collection.map { |error_group| new(error_group) } 11 | end 12 | 13 | def self.format_occurrence_day(day) 14 | date = day.to_date 15 | date == Date.yesterday ? 'Yesterday' : date.strftime('%A, %B %d') 16 | end 17 | 18 | def show_for_day?(day) 19 | last_occurrence.in_time_zone.to_date == day.to_date 20 | end 21 | 22 | private 23 | 24 | attr_reader :error_group 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/presenters/exception_hunter/error_presenter.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | class ErrorPresenter 3 | delegate_missing_to :error 4 | delegate :tags, to: :error_group 5 | 6 | BacktraceLine = Struct.new(:path, :file_name, :line_number, :method_call) 7 | 8 | def initialize(error) 9 | @error = error 10 | end 11 | 12 | def backtrace 13 | (error.backtrace || []).map do |line| 14 | format_backtrace_line(line) 15 | end 16 | end 17 | 18 | def environment_data 19 | error.environment_data&.except('params') || {} 20 | end 21 | 22 | def tracked_params 23 | (error.environment_data || {})['params'] 24 | end 25 | 26 | private 27 | 28 | attr_reader :error 29 | 30 | def format_backtrace_line(line) 31 | matches = line.match(%r{(?.*)/(?[^:]*):(?\d*).*`(?.*)'}) 32 | 33 | if matches.nil? 34 | line 35 | else 36 | BacktraceLine.new(matches[:path], 37 | matches[:file_name], 38 | matches[:line_number], 39 | matches[:method_call]) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/views/exception_hunter/devise/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /app/views/exception_hunter/errors/_error_backtrace.erb: -------------------------------------------------------------------------------- 1 | <% if error.backtrace.empty? %> 2 | Unfortunately, no backtrace has been registered for this error. 3 | <% else %> 4 |
5 | <% error.backtrace.each do |line| %> 6 |
7 | <% if line.is_a?(String) %> 8 | <%= line %> 9 | <% else %> 10 |
<%= line.path %>
11 | / 12 |
<%= line.file_name %>
13 | : 14 |
<%= line.line_number %>
15 |
<%= line.method_call %>
16 | <% end %> 17 |
18 | <% end %> 19 |
20 | <% end %> 21 | -------------------------------------------------------------------------------- /app/views/exception_hunter/errors/_error_row.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <% error.tags.each do |tag| %> 4 |
5 | <%= tag %> 6 |
7 | <% end %> 8 |
9 |
10 | <%= link_to error.message, error_path(error.id), class: %w[error-message] %> 11 |
12 | 13 |
14 | <% if error.first_occurrence.present? %> 15 | <%= time_ago_in_words(error.first_occurrence) %> ago 16 | <% else %> 17 | Never 18 | <% end %> 19 |
20 | 21 |
22 | <% if error.last_occurrence.present? %> 23 | <%= time_ago_in_words(error.last_occurrence) %> ago 24 | <% else %> 25 | Never 26 | <% end %> 27 |
28 | 29 |
30 | <%= error.total_occurrences %> 31 |
32 | 33 |
34 |
35 | <% if error.active? %> 36 |
37 | <%= display_action_button('resolve', error) %> 38 |
39 |
40 | <%= display_action_button('ignore', error) %> 41 |
42 | <% elsif error.ignored? %> 43 |
44 | <%= button_to('Reopen', reopen_path(error_group: { id: error.id }), 45 | class: %w[button button-outline resolve-button], 46 | data: { confirm: 'Are you sure you want to reopen this error?' }) %> 47 |
48 | <% end %> 49 |
50 |
51 |
52 | 53 | -------------------------------------------------------------------------------- /app/views/exception_hunter/errors/_error_summary.erb: -------------------------------------------------------------------------------- 1 | <% if error.environment_data.empty? %> 2 |
3 | Unfortunately, no environment information has been registered for this error. 4 |
5 | <% else %> 6 |
7 | Environment Data 8 |
9 |
10 | 
11 | <%= format_tracked_data(error.environment_data) %>
12 |   
13 | <% end %> 14 | 15 | <% unless error.tracked_params.nil? %> 16 |
17 | Tracked Params 18 |
19 |
20 | 
21 | <%= format_tracked_data(error.tracked_params) %>
22 |   
23 | <% end %> 24 | 25 | <% if error.custom_data.nil? %> 26 |
27 | No custom data included. 28 |
29 | <% else %> 30 |
31 | Custom Data 32 |
33 |
34 | 
35 | <%= format_tracked_data(error.custom_data) %>
36 |   
37 | <% end %> 38 | -------------------------------------------------------------------------------- /app/views/exception_hunter/errors/_error_user_data.erb: -------------------------------------------------------------------------------- 1 | <% if error.user_data.empty? %> 2 |
3 | Unfortunately, no user information has been registered for this error. 4 |
5 | <% else %> 6 |
 7 | 
 8 | <%= format_tracked_data(error.user_data) %>
 9 |   
10 | <% end %> 11 | -------------------------------------------------------------------------------- /app/views/exception_hunter/errors/_errors_table.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: 'exception_hunter/errors/error_row', collection: errors, as: :error %> 2 | -------------------------------------------------------------------------------- /app/views/exception_hunter/errors/_last_7_days_errors_table.erb: -------------------------------------------------------------------------------- 1 | <% today_errors = errors.select { |error| error.show_for_day?(Date.current) } %> 2 | <%= render partial: 'exception_hunter/errors/error_row', collection: today_errors, as: :error %> 3 | 4 | <% (1..6).each do |i| %> 5 | <% errors_on_day = errors.select { |error| error.show_for_day?(i.days.ago) } %> 6 |
<%= ExceptionHunter::ErrorGroupPresenter.format_occurrence_day(i.days.ago) %>
7 | <%= render partial: 'exception_hunter/errors/error_row', collection: errors_on_day, as: :error %> 8 | <% end %> 9 | -------------------------------------------------------------------------------- /app/views/exception_hunter/errors/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | <%= link_to errors_path(tab: @dashboard.class::LAST_7_DAYS_TAB) do %> 6 |
7 |
8 | <%= @dashboard.errors_count(@dashboard.class::LAST_7_DAYS_TAB) %> 9 |
10 |
11 | Errors in the last 7 days 12 |
13 |
14 | <% end %> 15 |
16 | 17 |
18 | <%= link_to errors_path(tab: @dashboard.class::CURRENT_MONTH_TAB) do %> 19 |
20 |
21 | <%= @dashboard.errors_count(@dashboard.class::CURRENT_MONTH_TAB) %> 22 |
23 |
24 | Errors this month 25 |
26 |
27 | <% end %> 28 |
29 | 30 |
31 | <%= link_to errors_path(tab: @dashboard.class::TOTAL_ERRORS_TAB) do %> 32 |
33 |
34 | <%= @dashboard.errors_count(@dashboard.class::TOTAL_ERRORS_TAB) %> 35 |
36 |
37 | Total errors 38 |
39 |
40 | <% end %> 41 |
42 | 43 |
44 | <%= link_to errors_path(tab: @dashboard.class::RESOLVED_ERRORS_TAB) do %> 45 |
46 |
47 | <%= @dashboard.errors_count(@dashboard.class::RESOLVED_ERRORS_TAB) || '-' %> 48 |
49 |
50 | Resolved 51 |
52 |
53 | <% end %> 54 |
55 | 56 |
57 | <%= link_to errors_path(tab: @dashboard.class::IGNORED_ERRORS_TAB) do %> 58 |
59 |
60 | <%= @dashboard.errors_count(@dashboard.class::IGNORED_ERRORS_TAB) || '-' %> 61 |
62 |
63 | Ignored 64 |
65 |
66 | <% end %> 67 |
68 |
69 |
70 | 71 |
72 | <%= button_to 'Purge', purge_errors_path, 73 | class: %w[button purge-button], 74 | method: :delete, 75 | data: { confirm: 'This will delete all stale errors, do you want to continue?' } %> 76 |
77 |
78 | 79 |
80 |
81 |
Tags
82 |
Message
83 |
First Occurrence
84 |
Last Occurrence
85 |
Total
86 |
87 | 88 |
89 | 90 | <%= render partial: @dashboard.partial_for_tab, locals: { errors: @errors } %> 91 |
92 | -------------------------------------------------------------------------------- /app/views/exception_hunter/errors/pagy/_pagy_nav.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= link_to pagy_url_for(pagy, 1) do %> 3 | <%= button_tag "First", class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.prev.nil? %> 4 | <% end %> 5 | <%= link_to pagy_url_for(pagy, pagy.prev) do %> 6 | <%= button_tag "<", class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.prev.nil? %> 7 | <% end %> 8 |
9 | <%= occurred_at %> (<%= pagy.page %>/<%= pagy.last %>) 10 |
11 | <%= link_to pagy_url_for(pagy, pagy.next) do %> 12 | <%= button_tag ">", class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.next.nil? %> 13 | <% end %> 14 | <%= link_to pagy_url_for(pagy, pagy.last) do %> 15 | <%= button_tag "Last", class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.next.nil? %> 16 | <% end %> 17 |
18 | -------------------------------------------------------------------------------- /app/views/exception_hunter/errors/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <% @error.tags.each do|tag| %> 4 |
5 | <%= tag %> 6 |
7 | <% end %> 8 |
9 |
10 |
11 | <%= @error.class_name %>: <%= @error.message %> 12 |
13 |
14 |
15 | <%= button_to('Resolve', resolved_errors_path(error_group: { id: @error.error_group_id }), 16 | method: :post, 17 | class: %w[button resolve-button], 18 | data: { confirm: 'Are you sure you want to resolve this error?' }) %> 19 |
20 |
21 | 22 |
23 | 48 |
49 | <%= render partial: 'exception_hunter/errors/pagy/pagy_nav', locals: { pagy: @pagy, occurred_at: @error.occurred_at } %> 50 |
51 |
52 | 53 | 54 |
55 |
56 |
57 | <%= render partial: 'exception_hunter/errors/error_summary', locals: { error: @error } %> 58 |
59 | 60 |
61 | <%= render partial: 'exception_hunter/errors/error_backtrace', locals: { error: @error } %> 62 |
63 | 64 |
65 | <%= render partial: 'exception_hunter/errors/error_user_data', locals: { error: @error } %> 66 |
67 |
68 |
69 | 70 | 73 | -------------------------------------------------------------------------------- /app/views/layouts/exception_hunter/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exception Hunter 5 | <%= csrf_meta_tags %> 6 | <%= csp_meta_tag %> 7 | 8 | <%= favicon_link_tag 'exception_hunter/logo.png' %> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <%= stylesheet_link_tag "exception_hunter/application", media: "all" %> 18 | 19 | 20 | 21 |
22 | 46 | 47 |
48 | <% if flash[:notice] %> 49 |
50 |
51 | <%= flash[:notice] %> 52 |
53 |
54 |
55 | <%= ['Cool!', 'Nice!', 'Ok', 'Dismiss', 'Fine. Whatever.', 'I know that'].sample %> 56 |
57 |
58 |
59 | <% end %> 60 | 61 | <%= yield %> 62 |
63 | 64 | 67 |
68 | 69 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /app/views/layouts/exception_hunter/exception_hunter_logged_out.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exception Hunter 5 | <%= csrf_meta_tags %> 6 | <%= csp_meta_tag %> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <%= stylesheet_link_tag "exception_hunter/application", media: "all" %> 18 | 19 | 20 |
21 | <%= yield %> 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path('..', __dir__) 6 | ENGINE_PATH = File.expand_path('../lib/exception_hunter/engine', __dir__) 7 | APP_PATH = File.expand_path('../spec/dummy/config/application', __dir__) 8 | 9 | # Set up gems listed in the Gemfile. 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 11 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 12 | 13 | require 'active_record/railtie' 14 | require 'action_controller/railtie' 15 | require 'action_mailer/railtie' 16 | require 'sprockets/railtie' 17 | require 'rails/engine/commands' 18 | -------------------------------------------------------------------------------- /config/rails_best_practices.yml: -------------------------------------------------------------------------------- 1 | AddModelVirtualAttributeCheck: { } 2 | AlwaysAddDbIndexCheck: { } 3 | #CheckSaveReturnValueCheck: { } 4 | #CheckDestroyReturnValueCheck: { } 5 | DefaultScopeIsEvilCheck: { } 6 | DryBundlerInCapistranoCheck: { } 7 | #HashSyntaxCheck: { } 8 | IsolateSeedDataCheck: { } 9 | KeepFindersOnTheirOwnModelCheck: { } 10 | LawOfDemeterCheck: { } 11 | #LongLineCheck: { max_line_length: 80 } 12 | MoveCodeIntoControllerCheck: { } 13 | MoveCodeIntoHelperCheck: { array_count: 3 } 14 | MoveCodeIntoModelCheck: { use_count: 2 } 15 | MoveFinderToNamedScopeCheck: { } 16 | # MoveModelLogicIntoModelCheck: { use_count: 4 } 17 | NeedlessDeepNestingCheck: { nested_count: 2 } 18 | NotUseDefaultRouteCheck: { } 19 | #NotUseTimeAgoInWordsCheck: { ignored_files: ['index.html.erb'] } 20 | OveruseRouteCustomizationsCheck: { customize_count: 3 } 21 | ProtectMassAssignmentCheck: { } 22 | RemoveEmptyHelpersCheck: { } 23 | #RemoveTabCheck: { } 24 | RemoveTrailingWhitespaceCheck: { } 25 | RemoveUnusedMethodsInControllersCheck: { except_methods: [] } 26 | RemoveUnusedMethodsInHelpersCheck: { except_methods: [] } 27 | RemoveUnusedMethodsInModelsCheck: { except_methods: [] } 28 | ReplaceComplexCreationWithFactoryMethodCheck: { attribute_assignment_count: 2 } 29 | ReplaceInstanceVariableWithLocalVariableCheck: { } 30 | RestrictAutoGeneratedRoutesCheck: { } 31 | SimplifyRenderInControllersCheck: { } 32 | #SimplifyRenderInViewsCheck: { } 33 | #UseBeforeFilterCheck: { customize_count: 2 } 34 | UseModelAssociationCheck: { } 35 | UseMultipartAlternativeAsContentTypeOfEmailCheck: { } 36 | #UseParenthesesInMethodDefCheck: { } 37 | UseObserverCheck: { } 38 | UseQueryAttributeCheck: { } 39 | UseSayWithTimeInMigrationsCheck: { } 40 | UseScopeAccessCheck: { } 41 | UseTurboSprocketsRails3Check: { } 42 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | ExceptionHunter::Engine.routes.draw do 2 | resources :errors, only: %i[index show] do 3 | delete 'purge', on: :collection, to: 'errors#destroy', as: :purge 4 | end 5 | 6 | resources :resolved_errors, only: %i[create] 7 | resources :ignored_errors, only: %i[create] 8 | post :reopen, to: 'ignored_errors#reopen' 9 | 10 | get '/', to: redirect('/exception_hunter/errors') 11 | 12 | if ExceptionHunter::Config.auth_enabled? 13 | admin_user_class = ExceptionHunter::Config.admin_user_class.underscore.to_sym 14 | 15 | devise_scope admin_user_class do 16 | get '/login', to: 'devise/sessions#new', as: :exception_hunter_login 17 | post '/login', to: 'devise/sessions#create', as: :exception_hunter_create_session 18 | get '/logout', to: 'devise/sessions#destroy', as: :exception_hunter_logout 19 | end 20 | 21 | devise_for admin_user_class, only: [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/exception_hunter/b6f7cab9e9a9157147c5a165d0b79762c6158664/docs/.nojekyll -------------------------------------------------------------------------------- /docs/ExceptionHunter/ApplicationJob.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Class: ExceptionHunter::ApplicationJob 8 | 9 | — Documentation by YARD 0.9.25 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 |
36 | 61 | 62 |

Class: ExceptionHunter::ApplicationJob 63 | 64 | 65 | 66 |

67 |
68 | 69 |
70 |
Inherits:
71 |
72 | ActiveJob::Base 73 | 74 |
    75 |
  • Object
  • 76 | 77 | 78 | 79 | 80 | 81 |
82 | show all 83 | 84 |
85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 |
Defined in:
99 |
app/jobs/exception_hunter/application_job.rb
100 |
101 | 102 |
103 | 104 |
105 |
106 | 107 | 108 |
109 |
110 |
111 | 112 | 113 |
114 |

Direct Known Subclasses

115 |

SendNotificationJob

116 |
117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |
128 | 129 | 134 | 135 |
136 | 137 | -------------------------------------------------------------------------------- /docs/ExceptionHunter/ApplicationMailer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Class: ExceptionHunter::ApplicationMailer 8 | 9 | — Documentation by YARD 0.9.25 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 |
36 | 61 | 62 |

Class: ExceptionHunter::ApplicationMailer 63 | 64 | 65 | 66 |

67 |
68 | 69 |
70 |
Inherits:
71 |
72 | ActionMailer::Base 73 | 74 |
    75 |
  • Object
  • 76 | 77 | 78 | 79 | 80 | 81 |
82 | show all 83 | 84 |
85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 |
Defined in:
99 |
app/mailers/exception_hunter/application_mailer.rb
100 |
101 | 102 |
103 | 104 |
105 |
106 | 107 | 108 |
109 |
110 |
111 | 112 | 113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |
124 | 125 | 130 | 131 |
132 | 133 | -------------------------------------------------------------------------------- /docs/ExceptionHunter/Devise.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Module: ExceptionHunter::Devise 8 | 9 | — Documentation by YARD 0.9.26 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 |
36 | 61 | 62 |

Module: ExceptionHunter::Devise 63 | 64 | 65 | 66 |

67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
80 |
Defined in:
81 |
lib/exception_hunter/devise.rb
82 |
83 | 84 |
85 | 86 |
87 |
88 | 89 | 90 |
91 |
92 |
93 | 94 | 95 |

Defined Under Namespace

96 |

97 | 98 | 99 | 100 | 101 | Classes: SessionsController 102 | 103 | 104 |

105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 |
115 | 116 | 121 | 122 |
123 | 124 | -------------------------------------------------------------------------------- /docs/ExceptionHunter/Error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Class: ExceptionHunter::Error 8 | 9 | — Documentation by YARD 0.9.25 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 |
36 | 61 | 62 |

Class: ExceptionHunter::Error 63 | 64 | 65 | 66 |

67 |
68 | 69 |
70 |
Inherits:
71 |
72 | ApplicationRecord 73 | 74 |
    75 |
  • Object
  • 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
84 | show all 85 | 86 |
87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
100 |
Defined in:
101 |
app/models/exception_hunter/error.rb
102 |
103 | 104 |
105 | 106 |
107 |
108 | 109 | 110 |
111 |
112 |
113 | 114 | 115 |
116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |
128 | 129 | 134 | 135 |
136 | 137 | -------------------------------------------------------------------------------- /docs/ExceptionHunter/Middleware.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Module: ExceptionHunter::Middleware 8 | 9 | — Documentation by YARD 0.9.26 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 |
36 | 61 | 62 |

Module: ExceptionHunter::Middleware 63 | 64 | 65 | 66 |

67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
80 |
Defined in:
81 |
lib/exception_hunter/middleware/request_hunter.rb,
82 | lib/exception_hunter/middleware/sidekiq_hunter.rb,
lib/exception_hunter/middleware/delayed_job_hunter.rb
83 |
84 |
85 | 86 |
87 | 88 |
89 |
90 | 91 | 92 |
93 |
94 |
95 | 96 | 97 |

Defined Under Namespace

98 |

99 | 100 | 101 | 102 | 103 | Classes: DelayedJobHunter, RequestHunter, SidekiqHunter 104 | 105 | 106 |

107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 |
117 | 118 | 123 | 124 |
125 | 126 | -------------------------------------------------------------------------------- /docs/ExceptionHunter/Notifiers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Module: ExceptionHunter::Notifiers 8 | 9 | — Documentation by YARD 0.9.26 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 |
36 | 61 | 62 |

Module: ExceptionHunter::Notifiers 63 | 64 | 65 | 66 |

67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
80 |
Defined in:
81 |
lib/exception_hunter/notifiers/slack_notifier.rb,
82 | lib/exception_hunter/notifiers/misconfigured_notifiers.rb,
lib/exception_hunter/notifiers/slack_notifier_serializer.rb
83 |
84 |
85 | 86 |
87 | 88 |
89 |
90 | 91 | 92 |
93 |
94 |
95 | 96 | 97 |

Defined Under Namespace

98 |

99 | 100 | 101 | 102 | 103 | Classes: MisconfiguredNotifiers, SlackNotifier 104 | 105 | 106 |

107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 |
117 | 118 | 123 | 124 |
125 | 126 | -------------------------------------------------------------------------------- /docs/css/common.css: -------------------------------------------------------------------------------- 1 | /* Override this file with custom rules */ -------------------------------------------------------------------------------- /docs/file_list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | File List 19 | 20 | 21 | 22 |
23 |
24 |

File List

25 |
26 | 27 | 28 | Classes 29 | 30 | 31 | 32 | Methods 33 | 34 | 35 | 36 | Files 37 | 38 | 39 |
40 | 41 | 42 |
43 | 44 |
    45 | 46 | 47 |
  • 48 | 49 |
  • 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /docs/frames.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation by YARD 0.9.26 6 | 7 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /docs/index-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/exception_hunter/b6f7cab9e9a9157147c5a165d0b79762c6158664/docs/index-screenshot.png -------------------------------------------------------------------------------- /docs/top-level-namespace.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Top Level Namespace 8 | 9 | — Documentation by YARD 0.9.26 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 |
36 | 61 | 62 |

Top Level Namespace 63 | 64 | 65 | 66 |

67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
80 | 81 |

Defined Under Namespace

82 |

83 | 84 | 85 | Modules: ExceptionHunter 86 | 87 | 88 | 89 | 90 |

91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 | 102 | 107 | 108 |
109 | 110 | -------------------------------------------------------------------------------- /exception_hunter.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH .push File.expand_path('lib', __dir__) 2 | 3 | # Maintain your gem's version: 4 | require 'exception_hunter/version' 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |spec| 8 | spec.name = 'exception_hunter' 9 | spec.version = ExceptionHunter::VERSION 10 | spec.authors = ['Bruno Vezoli', 'Tiziana Romani'] 11 | spec.email = ['bruno.vezoli@rootstrap.com'] 12 | spec.summary = 'Exception tracking engine' 13 | spec.license = 'MIT' 14 | spec.homepage = 'https://github.com/rootstrap/exception_hunter' 15 | 16 | spec.required_ruby_version = '>= 2.5.5' 17 | 18 | EXCLUDED_FILES = %w[lib/tasks/code_analysis.rake].freeze 19 | spec.files = Dir['{app,config,db,lib}/**/*', 'MIT-LICENSE', 'Rakefile', 'README.md'] - EXCLUDED_FILES 20 | 21 | spec.add_dependency 'pagy', '~> 4' 22 | spec.add_dependency 'slack-notifier', '~> 2.3' 23 | 24 | spec.add_development_dependency 'brakeman', '~> 4.8' 25 | spec.add_development_dependency 'factory_bot_rails' 26 | spec.add_development_dependency 'pg' 27 | spec.add_development_dependency 'rails_best_practices', '~> 1.20' 28 | spec.add_development_dependency 'reek', '~> 5.6' 29 | spec.add_development_dependency 'rubocop', '~> 0.80.1' 30 | spec.add_development_dependency 'simplecov', '~> 0.17.1' 31 | end 32 | -------------------------------------------------------------------------------- /lib/exception_hunter.rb: -------------------------------------------------------------------------------- 1 | require 'pagy' 2 | 3 | require 'exception_hunter/engine' 4 | require 'exception_hunter/middleware/request_hunter' 5 | require 'exception_hunter/config' 6 | require 'exception_hunter/error_creator' 7 | require 'exception_hunter/error_reaper' 8 | require 'exception_hunter/tracking' 9 | require 'exception_hunter/user_attributes_collector' 10 | require 'exception_hunter/notifiers/slack_notifier' 11 | require 'exception_hunter/notifiers/slack_notifier_serializer' 12 | require 'exception_hunter/notifiers/misconfigured_notifiers' 13 | require 'exception_hunter/data_redacter' 14 | 15 | # @api public 16 | module ExceptionHunter 17 | autoload :Devise, 'exception_hunter/devise' 18 | 19 | extend ::ExceptionHunter::Tracking 20 | 21 | # Used to setup ExceptionHunter's configuration 22 | # it receives a block with the {ExceptionHunter::Config} singleton 23 | # class. 24 | # 25 | # @return [void] 26 | def self.setup(&block) 27 | block.call(Config) 28 | validate_config! 29 | end 30 | 31 | # Mounts the ExceptionHunter dashboard at /exception_hunter 32 | # if it's enabled on the current environment. 33 | # 34 | # @example 35 | # Rails.application.routes.draw do 36 | # ExceptionHunter.routes(self) 37 | # end 38 | # 39 | # @param [ActionDispatch::Routing::Mapper] router to mount to 40 | # @return [void] 41 | def self.routes(router) 42 | return unless Config.enabled 43 | 44 | router.mount(ExceptionHunter::Engine, at: 'exception_hunter') 45 | end 46 | 47 | # @private 48 | def self.validate_config! 49 | notifiers = Config.notifiers 50 | return if notifiers.blank? 51 | 52 | notifiers.each do |notifier| 53 | next if notifier[:name] == :slack && notifier.dig(:options, :webhook).present? 54 | 55 | raise ExceptionHunter::Notifiers::MisconfiguredNotifiers, notifier 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/exception_hunter/config.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | # Config singleton class used to customize ExceptionHunter 3 | class Config 4 | # @!attribute 5 | # @return [Boolean] whether ExceptionHunter is active or not 6 | cattr_accessor :enabled, default: true 7 | # @!attribute 8 | # @return [String] the name of the admin class (generally AdminUser) 9 | cattr_accessor :admin_user_class 10 | # @!attribute 11 | # @return [Symbol] the name of the current user method provided by Devise 12 | cattr_accessor :current_user_method 13 | # @return [Array] attributes to whitelist on the user (see {ExceptionHunter::UserAttributesCollector}) 14 | cattr_accessor :user_attributes 15 | # @return [Numeric] number of days until an error is considered stale 16 | cattr_accessor :errors_stale_time, default: 45.days 17 | # @return [Array] configured notifiers for the application (see {ExceptionHunter::Notifiers}) 18 | cattr_accessor :notifiers, default: [] 19 | cattr_accessor :sensitive_fields, default: [] 20 | # @!attribute 21 | # @return [Boolean] whether ExceptionHunter should log async or not 22 | cattr_accessor :async_logging, default: false 23 | 24 | # Returns true if there's an admin user class configured to 25 | # authenticate against. 26 | # 27 | # @return Boolean 28 | def self.auth_enabled? 29 | admin_user_class.present? && admin_user_class.try(:underscore) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/exception_hunter/data_redacter.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | class DataRedacter 3 | attr_reader :params, :params_to_filter 4 | 5 | def initialize(params, params_to_filter) 6 | @params = params 7 | @params_to_filter = params_to_filter 8 | end 9 | 10 | def redact 11 | return params if params.blank? 12 | 13 | parameter_filter = params_filter.new(params_to_filter) 14 | parameter_filter.filter(params) 15 | end 16 | 17 | private 18 | 19 | def params_filter 20 | if defined?(::ActiveSupport::ParameterFilter) 21 | ::ActiveSupport::ParameterFilter 22 | else 23 | ::ActionDispatch::Http::ParameterFilter 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/exception_hunter/devise.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | module Devise 3 | # Used so we can integrate with {https://github.com/heartcombo/devise Devise} and 4 | # provide a custom login on the dashboard. 5 | class SessionsController < ::Devise::SessionsController 6 | skip_before_action :verify_authenticity_token 7 | 8 | layout 'exception_hunter/exception_hunter_logged_out' 9 | 10 | def after_sign_out_path_for(*) 11 | '/exception_hunter/login' 12 | end 13 | 14 | def after_sign_in_path_for(*) 15 | '/exception_hunter' 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/exception_hunter/engine.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | # @private 3 | class Engine < ::Rails::Engine 4 | isolate_namespace ExceptionHunter 5 | 6 | config.generators do |gen| 7 | gen.test_framework :rspec 8 | gen.fixture_replacement :factory_bot 9 | gen.factory_bot dir: 'spec/factories' 10 | end 11 | 12 | initializer 'exception_hunter.precompile', group: :all do |app| 13 | app.config.assets.precompile << 'exception_hunter/application.css' 14 | app.config.assets.precompile << 'exception_hunter/logo.png' 15 | end 16 | 17 | initializer 'exception_hunter.load_middleware', group: :all do 18 | require 'exception_hunter/middleware/sidekiq_hunter' if defined?(Sidekiq) 19 | require 'exception_hunter/middleware/delayed_job_hunter' if defined?(Delayed) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/exception_hunter/error_creator.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | # Core class in charge of the actual persistence of errors and notifications. 3 | class ErrorCreator 4 | HTTP_TAG = 'HTTP'.freeze 5 | WORKER_TAG = 'Worker'.freeze 6 | MANUAL_TAG = 'Manual'.freeze 7 | NOTIFICATION_DELAY = 1.minute 8 | 9 | class << self 10 | # Creates an error with the given attributes and persists it to 11 | # the database. 12 | # 13 | # @param [HTTP_TAG, WORKER_TAG, MANUAL_TAG] tag to append to the error if any 14 | # @return [ExceptionHunter::Error, false] the error or false if it was not possible to create it 15 | def call(async_logging: Config.async_logging, tag: nil, **error_attrs) 16 | return unless should_create? 17 | 18 | if async_logging 19 | # Time is sent in unix format and then converted to Time to avoid ActiveJob::SerializationError 20 | ::ExceptionHunter::AsyncLoggingJob.perform_later(tag, error_attrs.merge(occurred_at: Time.now.to_i)) 21 | else 22 | create_error(tag, **error_attrs) 23 | end 24 | rescue ActiveRecord::RecordInvalid 25 | false 26 | end 27 | 28 | private 29 | 30 | def create_error(tag, **error_attrs) 31 | ActiveRecord::Base.transaction do 32 | error_attrs = extract_user_data(**error_attrs) 33 | error_attrs = hide_sensitive_values(error_attrs) 34 | error = ::ExceptionHunter::Error.new(error_attrs) 35 | error_group = ::ExceptionHunter::ErrorGroup.find_matching_group(error) || ::ExceptionHunter::ErrorGroup.new 36 | update_error_group(error_group, error, tag) 37 | error.error_group = error_group 38 | error.save! 39 | 40 | unless error_group.ignored? 41 | notify(error) 42 | error 43 | end 44 | end 45 | end 46 | 47 | def should_create? 48 | Config.enabled 49 | end 50 | 51 | def update_error_group(error_group, error, tag) 52 | error_group.error_class_name = error.class_name 53 | error_group.message = error.message 54 | error_group.tags << tag unless tag.nil? 55 | error_group.tags.uniq! 56 | 57 | error_group.save! 58 | end 59 | 60 | def extract_user_data(**error_attrs) 61 | user = error_attrs[:user] 62 | error_attrs[:user_data] = UserAttributesCollector.collect_attributes(user) 63 | 64 | error_attrs.delete(:user) 65 | error_attrs 66 | end 67 | 68 | def notify(error) 69 | ExceptionHunter::Config.notifiers.each do |notifier| 70 | slack_notifier = ExceptionHunter::Notifiers::SlackNotifier.new(error, notifier) 71 | serializer = ExceptionHunter::Notifiers::SlackNotifierSerializer 72 | serialized_slack_notifier = serializer.serialize(slack_notifier) 73 | ExceptionHunter::SendNotificationJob.set( 74 | wait: NOTIFICATION_DELAY 75 | ).perform_later(serialized_slack_notifier) 76 | end 77 | end 78 | 79 | def hide_sensitive_values(error_attrs) 80 | sensitive_fields = ExceptionHunter::Config.sensitive_fields 81 | ExceptionHunter::DataRedacter.new(error_attrs, sensitive_fields).redact 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/exception_hunter/error_reaper.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | # Class in charge of disposing of stale errors as specified in the {ExceptionHunter::Config}. 3 | class ErrorReaper 4 | class << self 5 | # Destroys all stale errors. 6 | # 7 | # @example 8 | # ErrorReaper.purge(stale_time: 30.days) 9 | # 10 | # @param [Numeric] stale_time considered when destroying errors 11 | # @return [void] 12 | def purge(stale_time: Config.errors_stale_time) 13 | ActiveRecord::Base.transaction do 14 | Error.with_occurrences_before(Date.today - stale_time).destroy_all 15 | ErrorGroup.without_errors.destroy_all 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/exception_hunter/middleware/delayed_job_hunter.rb: -------------------------------------------------------------------------------- 1 | require 'delayed_job' 2 | 3 | module ExceptionHunter 4 | module Middleware 5 | # DelayedJob plugin to track exceptions on apps using DelayedJob. 6 | class DelayedJobHunter < ::Delayed::Plugin 7 | TRACK_AT_RETRY = [0, 3, 6, 10].freeze 8 | JOB_TRACKED_DATA = %w[ 9 | attempts 10 | ].freeze 11 | ARGS_TRACKED_DATA = %w[ 12 | queue_name 13 | job_class 14 | job_id 15 | arguments 16 | enqueued_at 17 | ].freeze 18 | 19 | callbacks do |lifecycle| 20 | lifecycle.around(:invoke_job) do |job, *args, &block| 21 | block.call(job, *args) 22 | 23 | rescue Exception => exception # rubocop:disable Lint/RescueException 24 | track_exception(exception, job) 25 | 26 | raise exception 27 | end 28 | end 29 | 30 | def self.track_exception(exception, job) 31 | return unless should_track?(job.attempts) 32 | 33 | ErrorCreator.call( 34 | async_logging: false, 35 | tag: ErrorCreator::WORKER_TAG, 36 | class_name: exception.class.to_s, 37 | message: exception.message, 38 | environment_data: environment_data(job), 39 | backtrace: exception.backtrace 40 | ) 41 | end 42 | 43 | def self.environment_data(job) 44 | job_data = 45 | JOB_TRACKED_DATA.reduce({}) do |dict, data_param| 46 | dict.merge(data_param => job.try(data_param)) 47 | end 48 | 49 | job_class = if job.payload_object.class.name == 'ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper' 50 | # support for Rails 4.2 ActiveJob 51 | job.payload_object.job_data['job_class'] 52 | elsif job.payload_object.object.is_a?(Class) 53 | job.payload_object.object.name 54 | else 55 | job.payload_object.object.class.name 56 | end 57 | args_data = (job.payload_object.try(:job_data) || {}).select { |key, _value| ARGS_TRACKED_DATA.include?(key) } 58 | 59 | args_data['job_class'] = job_class || job.payload_object.class.name if args_data['job_class'].nil? 60 | 61 | job_data.merge(args_data) 62 | end 63 | 64 | def self.should_track?(attempts) 65 | TRACK_AT_RETRY.include?(attempts) 66 | end 67 | end 68 | end 69 | end 70 | 71 | Delayed::Worker.plugins << ExceptionHunter::Middleware::DelayedJobHunter 72 | -------------------------------------------------------------------------------- /lib/exception_hunter/middleware/request_hunter.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | module Middleware 3 | # {https://www.rubyguides.com/2018/09/rack-middleware Rack Middleware} used to 4 | # rescue from exceptions track them and then re-raise them. 5 | class RequestHunter 6 | ENVIRONMENT_KEYS = 7 | %w[PATH_INFO 8 | QUERY_STRING 9 | REMOTE_HOST 10 | REQUEST_METHOD 11 | REQUEST_URI 12 | SERVER_PROTOCOL 13 | HTTP_HOST 14 | CONTENT_TYPE 15 | HTTP_USER_AGENT].freeze 16 | 17 | FILTERED_PARAMS = [/password/].freeze 18 | 19 | def initialize(app) 20 | @app = app 21 | end 22 | 23 | def call(env) 24 | @app.call(env) 25 | rescue Exception => exception # rubocop:disable Lint/RescueException 26 | catch_prey(env, exception) 27 | raise exception 28 | end 29 | 30 | private 31 | 32 | def catch_prey(env, exception) 33 | user = user_from_env(env) 34 | ErrorCreator.call( 35 | tag: ErrorCreator::HTTP_TAG, 36 | class_name: exception.class.to_s, 37 | message: exception.message, 38 | environment_data: environment_data(env), 39 | backtrace: exception.backtrace, 40 | user: user 41 | ) 42 | end 43 | 44 | def environment_data(env) 45 | env 46 | .select { |key, _value| ENVIRONMENT_KEYS.include?(key) } 47 | .merge(params: filtered_sensitive_params(env)) 48 | end 49 | 50 | def user_from_env(env) 51 | current_user_method = Config.current_user_method 52 | controller = env['action_controller.instance'] 53 | controller.try(current_user_method) 54 | end 55 | 56 | def filtered_sensitive_params(env) 57 | params = env['action_dispatch.request.parameters'] 58 | ExceptionHunter::DataRedacter.new(params, FILTERED_PARAMS).redact 59 | end 60 | end 61 | end 62 | end 63 | 64 | module ExceptionHunter 65 | # @private 66 | class Railtie < Rails::Railtie 67 | initializer 'exception_hunter.add_middleware', after: :load_config_initializers do |app| 68 | app.config.middleware.insert_after( 69 | ActionDispatch::DebugExceptions, ExceptionHunter::Middleware::RequestHunter 70 | ) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/exception_hunter/middleware/sidekiq_hunter.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | module Middleware 3 | # Middleware to report errors 4 | # when a Sidekiq worker fails 5 | class SidekiqHunter 6 | TRACK_AT_RETRY = [0, 3, 6, 10].freeze 7 | JOB_TRACKED_DATA = %w[ 8 | queue 9 | retry_count 10 | ].freeze 11 | ARGS_TRACKED_DATA = %w[ 12 | job_class 13 | job_id 14 | arguments 15 | enqueued_at 16 | ].freeze 17 | 18 | def call(_worker, context, _queue) 19 | yield 20 | rescue Exception => exception # rubocop:disable Lint/RescueException 21 | track_exception(exception, context) 22 | raise exception 23 | end 24 | 25 | private 26 | 27 | def track_exception(exception, context) 28 | return unless should_track?(context) 29 | 30 | ErrorCreator.call( 31 | async_logging: false, 32 | tag: ErrorCreator::WORKER_TAG, 33 | class_name: exception.class.to_s, 34 | message: exception.message, 35 | environment_data: environment_data(context), 36 | backtrace: exception.backtrace 37 | ) 38 | end 39 | 40 | def environment_data(context) 41 | job_data = context.select { |key, _value| JOB_TRACKED_DATA.include?(key) } 42 | args_data = (context['args']&.first || {}).select { |key, _value| ARGS_TRACKED_DATA.include?(key) } 43 | 44 | job_data.merge(args_data) 45 | end 46 | 47 | def should_track?(context) 48 | TRACK_AT_RETRY.include?(context['retry_count'].to_i) 49 | end 50 | end 51 | end 52 | end 53 | 54 | # As seen in https://github.com/mperham/sidekiq/wiki/Error-Handling 55 | Sidekiq.configure_server do |config| 56 | config.server_middleware do |chain| 57 | chain.add(ExceptionHunter::Middleware::SidekiqHunter) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/exception_hunter/notifiers/misconfigured_notifiers.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | module Notifiers 3 | # Error raised when there's a malformed notifier. 4 | class MisconfiguredNotifiers < StandardError 5 | def initialize(notifier) 6 | super("Notifier has incorrect configuration: #{notifier.inspect}") 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/exception_hunter/notifiers/slack_notifier.rb: -------------------------------------------------------------------------------- 1 | require 'slack-notifier' 2 | 3 | module ExceptionHunter 4 | module Notifiers 5 | # Notifier that sends a message to a Slack channel every time an 6 | # exception is tracked. 7 | class SlackNotifier 8 | attr_reader :error, :notifier 9 | 10 | def initialize(error, notifier) 11 | @error = error 12 | @notifier = notifier 13 | end 14 | 15 | def notify 16 | slack_notifier = Slack::Notifier.new(notifier[:options][:webhook]) 17 | slack_notifier.ping(slack_notification_message) 18 | end 19 | 20 | private 21 | 22 | def slack_notification_message 23 | { 24 | blocks: [ 25 | { 26 | type: 'section', 27 | text: { 28 | type: 'mrkdwn', 29 | text: error_message 30 | } 31 | } 32 | ] 33 | } 34 | end 35 | 36 | def error_message 37 | "*#{error.class_name}*: #{error.message}. \n" \ 38 | "<#{ExceptionHunter::Engine.routes.url_helpers.error_url(error.error_group)}|Click to see the error>" 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/exception_hunter/notifiers/slack_notifier_serializer.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | module Notifiers 3 | # @private 4 | class SlackNotifierSerializer 5 | def self.serialize(slack_notifier) 6 | { 7 | error: slack_notifier.error, 8 | notifier: slack_notifier.notifier.as_json 9 | } 10 | end 11 | 12 | def self.deserialize(hash) 13 | ExceptionHunter::Notifiers::SlackNotifier.new( 14 | hash[:error], 15 | hash[:notifier].deep_symbolize_keys 16 | ) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/exception_hunter/tracking.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | # Mixin used to track manual exceptions. 3 | module Tracking 4 | # Used to manually track errors in cases where raising might 5 | # not be adequate and but some insight is desired. 6 | # 7 | # @example Track the else clause on a case 8 | # case user.status 9 | # when :active then do_something() 10 | # when :inactive then do_something_else() 11 | # else 12 | # ExceptionHunter.track(StandardError.new("User with unknown status"), 13 | # custom_data: { status: user.status }, 14 | # user: user) 15 | # end 16 | # 17 | # @param [Exception] exception to track. 18 | # @param [Hash] custom_data to include and help debug the error. (optional) 19 | # @param [User] user in the current session. (optional) 20 | # @return [void] 21 | def track(exception, custom_data: {}, user: nil) 22 | if open_transactions? 23 | create_error_within_new_thread(exception, custom_data, user) 24 | else 25 | create_error(exception, custom_data, user) 26 | end 27 | 28 | nil 29 | end 30 | 31 | private 32 | 33 | def create_error_within_new_thread(exception, custom_data, user) 34 | Thread.new { 35 | ActiveRecord::Base.connection_pool.with_connection do 36 | create_error(exception, custom_data, user) 37 | end 38 | }.join 39 | end 40 | 41 | def create_error(exception, custom_data, user) 42 | ErrorCreator.call( 43 | tag: ErrorCreator::MANUAL_TAG, 44 | class_name: exception.class.to_s, 45 | message: exception.message, 46 | backtrace: exception.backtrace, 47 | custom_data: custom_data, 48 | user: user, 49 | environment_data: {} 50 | ) 51 | end 52 | 53 | def open_transactions? 54 | ActiveRecord::Base.connection.open_transactions.positive? 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/exception_hunter/user_attributes_collector.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | # Utility module used to whitelist the user's attributes. 3 | # Can be configured in {ExceptionHunter.setup ExceptionHunter.setup} to extract 4 | # custom attributes. 5 | # 6 | # @example 7 | # ExceptionHunter.setup do |config| 8 | # config.user_attributes = [:id, :email, :role, :active?] 9 | # end 10 | # 11 | module UserAttributesCollector 12 | extend self 13 | 14 | # Gets the attributes configured for the user. 15 | # 16 | # @example 17 | # UserAttributesCollector.collect_attributes(current_user) 18 | # # => { id: 42, email: "example@user.com" } 19 | # 20 | # @param user instance in your application 21 | # @return [Hash] the whitelisted attributes from the user 22 | def collect_attributes(user) 23 | return {} unless user 24 | 25 | attributes.reduce({}) do |data, attribute| 26 | data.merge(attribute => user.try(attribute)) 27 | end 28 | end 29 | 30 | private 31 | 32 | def attributes 33 | Config.user_attributes 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/exception_hunter/version.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | VERSION = '1.1.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/exception_hunter/create_users/create_users_generator.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | class CreateUsersGenerator < Rails::Generators::NamedBase 3 | argument :name, type: :string, default: 'AdminUser' 4 | 5 | def install_devise 6 | begin 7 | require 'devise' 8 | rescue LoadError 9 | log :error, 'Please install devise and require add it to your gemfile or run with --skip-users' 10 | exit(false) 11 | end 12 | 13 | initializer_file = 14 | File.join(destination_root, 'config', 'initializers', 'devise.rb') 15 | 16 | if File.exist?(initializer_file) 17 | log :generate, 'No need to install devise, already done.' 18 | else 19 | log :generate, 'devise:install' 20 | invoke 'devise:install' 21 | end 22 | end 23 | 24 | def create_admin_user 25 | invoke 'devise', [name], routes: false 26 | end 27 | 28 | def remove_registerable_from_model 29 | return if options[:registerable] 30 | 31 | model_file = File.join(destination_root, 'app', 'models', "#{file_path}.rb") 32 | gsub_file model_file, /\:registerable([.]*,)?/, '' 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/generators/exception_hunter/install/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generates an initializer and the needed setup to use the ExceptionHunter gem. It also 3 | creates a user for developers to access the dashboard by invoking devise. 4 | 5 | Example: 6 | rails generate exception_hunter:install SuperUser 7 | -------------------------------------------------------------------------------- /lib/generators/exception_hunter/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/active_record' 2 | 3 | module ExceptionHunter 4 | class InstallGenerator < ActiveRecord::Generators::Base 5 | source_root File.expand_path('templates', __dir__) 6 | 7 | argument :name, type: :string, default: 'AdminUser' 8 | hook_for :users, default: 'create_users', desc: 'Admin user generator to run. Skip with --skip-users' 9 | 10 | def copy_initializer 11 | @underscored_user_name = name.underscore.gsub('/', '_') 12 | @use_authentication_method = options[:users].present? 13 | template 'exception_hunter.rb.erb', 'config/initializers/exception_hunter.rb' 14 | end 15 | 16 | def setup_routes 17 | if options[:users] 18 | gsub_file 'config/routes.rb', 19 | "\n devise_for :#{plural_table_name}, skip: :all", 20 | "\n ExceptionHunter.routes(self)" 21 | else 22 | route 'ExceptionHunter.routes(self)' 23 | end 24 | end 25 | 26 | def create_migrations 27 | migration_template 'create_exception_hunter_error_groups.rb.erb', 28 | 'db/migrate/create_exception_hunter_error_groups.rb' 29 | migration_template 'create_exception_hunter_errors.rb.erb', 'db/migrate/create_exception_hunter_errors.rb' 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/generators/exception_hunter/install/templates/create_exception_hunter_error_groups.rb.erb: -------------------------------------------------------------------------------- 1 | class CreateExceptionHunterErrorGroups < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version.to_s %>] 2 | def change 3 | enable_extension :pg_trgm 4 | 5 | create_table :exception_hunter_error_groups do |t| 6 | t.string :error_class_name, null: false 7 | t.string :message 8 | t.integer :status, default: 0 9 | t.text :tags, array: true, default: [] 10 | 11 | t.timestamps 12 | 13 | t.index :message, opclass: :gin_trgm_ops, using: :gin 14 | t.index :status 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/generators/exception_hunter/install/templates/create_exception_hunter_errors.rb.erb: -------------------------------------------------------------------------------- 1 | class CreateExceptionHunterErrors < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version.to_s %>] 2 | def change 3 | create_table :exception_hunter_errors do |t| 4 | t.string :class_name, null: false 5 | t.string :message 6 | t.timestamp :occurred_at, null: false 7 | t.json :environment_data 8 | t.json :custom_data 9 | t.json :user_data 10 | t.string :backtrace, array: true, default: [] 11 | 12 | t.belongs_to :error_group, 13 | index: true, 14 | foreign_key: { to_table: :exception_hunter_error_groups } 15 | 16 | t.timestamps 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/generators/exception_hunter/install/templates/exception_hunter.rb.erb: -------------------------------------------------------------------------------- 1 | ExceptionHunter.setup do |config| 2 | # == Enabling 3 | # 4 | # This flag allows disabling error tracking, it's set to track in 5 | # any environment but development or test by default 6 | # 7 | config.enabled = !(Rails.env.development? || Rails.env.test?) 8 | 9 | # == Dashboard User 10 | # Exception Hunter allows you to restrict users who can see the dashboard 11 | # to the ones included in the database. You can change the table name in 12 | # case you are not satisfied with the default one. You can also remove the 13 | # configuration if you wish to have no access restrictions for the dashboard. 14 | # 15 | <%= @use_authentication_method ? "config.admin_user_class = '#{name}'" : "# config.admin_user_class = '#{name}'" %> 16 | 17 | # == Current User 18 | # 19 | # Exception Hunter will include the user as part of the environment 20 | # data, if it was to be available. The default configuration uses devise 21 | # :current_user method. You can change it in case you named your user model 22 | # in some other way (i.e. Member). You can also remove the configuration if 23 | # you don't wish to track user data. 24 | # 25 | config.current_user_method = :current_user 26 | 27 | # == Current User Attributes 28 | # 29 | # Exception Hunter will try to include the attributes defined here 30 | # as part of the user information that is kept from the request. 31 | # 32 | config.user_attributes = [:id, :email] 33 | 34 | # == Stale errors 35 | # 36 | # You can configure how long it takes for errors to go stale. This is 37 | # taken into account when purging old error messages but nothing will 38 | # happen automatically. 39 | # 40 | # config.errors_stale_time = 45.days 41 | 42 | # == Slack notifications 43 | # 44 | # You can configure if you want to send notifications to slack for each error occurrence. 45 | # You can enter multiple webhook urls. 46 | # Default: [] 47 | # 48 | # config.notifiers << { 49 | # name: :slack, 50 | # options: { 51 | # webhook: 'SLACK_WEBHOOK_URL_1' 52 | # } 53 | # } 54 | # 55 | # config.notifiers << { 56 | # name: :slack, 57 | # options: { 58 | # webhook: SLACK_WEBHOOK_URL_2' 59 | # } 60 | # } 61 | 62 | # == Filter sensitive parameters 63 | # 64 | # You can configure if you want to filter some fields on the error's data for security or privacy issues. 65 | # We use ActiveSupport::ParameterFilter for this, any accepted pattern will work. 66 | # https://api.rubyonrails.org/classes/ActiveSupport/ParameterFilter.html 67 | # Default: [] 68 | # 69 | # config.sensitive_parameters = [:id, :name] 70 | end 71 | -------------------------------------------------------------------------------- /lib/tasks/code_analysis.rake: -------------------------------------------------------------------------------- 1 | desc 'code analysis' 2 | task :code_analysis do 3 | sh 'bundle exec rubocop app config lib spec' 4 | sh 'bundle exec reek app config lib' 5 | sh 'bundle exec rails_best_practices .' 6 | end 7 | -------------------------------------------------------------------------------- /lib/tasks/exception_hunter_tasks.rake: -------------------------------------------------------------------------------- 1 | namespace :exception_hunter do 2 | desc 'Purges old errors' 3 | task purge_errors: [:environment] do 4 | ::ExceptionHunter::ErrorReaper.purge 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/data_redacter_spec.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | describe DataRedacter do 3 | describe '.redact' do 4 | subject { described_class.new(attributes, attributes_to_filter).redact } 5 | 6 | let(:attributes_to_filter) { %i[class_name hide hide_this_too hide_this_hash] } 7 | 8 | context 'when params with content' do 9 | let(:attributes) do 10 | { 11 | class_name: 'SomeError', 12 | message: 'Something went very wrong 123', 13 | environment_data: { 14 | hide: { value_to_hide: 'hide this value' }, 15 | "hide_this_too": { "hide_this": 'hide this' }, 16 | hide_this_hash: { "hide_this_hash": 'hide this' } 17 | } 18 | } 19 | end 20 | 21 | it 'hides the values' do 22 | expect(subject).to eq( 23 | class_name: '[FILTERED]', 24 | message: 'Something went very wrong 123', 25 | environment_data: { 26 | hide: '[FILTERED]', 27 | "hide_this_too": '[FILTERED]', 28 | hide_this_hash: '[FILTERED]' 29 | } 30 | ) 31 | end 32 | end 33 | 34 | context 'when empty params' do 35 | context 'when params is an empty hash' do 36 | let(:attributes) { {} } 37 | 38 | it 'returns empty hash' do 39 | expect(subject).to eq({}) 40 | end 41 | end 42 | 43 | context 'when params is an empty string' do 44 | let(:attributes) { '' } 45 | 46 | it 'returns empty string' do 47 | expect(subject).to eq('') 48 | end 49 | end 50 | 51 | context 'when params is null' do 52 | let(:attributes) { nil } 53 | 54 | it 'returns nil' do 55 | expect(subject).to eq(nil) 56 | end 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/dummy/.ruby-version: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | //= link exception_hunter_manifest.js 4 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/exception_hunter/b6f7cab9e9a9157147c5a165d0b79762c6158664/spec/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /spec/dummy/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/exception_hunter/b6f7cab9e9a9157147c5a165d0b79762c6158664/spec/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy/app/controllers/exception_controller.rb: -------------------------------------------------------------------------------- 1 | class ExceptionController < ApplicationController 2 | skip_before_action :verify_authenticity_token 3 | 4 | def raising_endpoint 5 | raise ArgumentError, 'You should not have called me' 6 | end 7 | 8 | def broken_post 9 | raise ArgumentError, "I don't really work" 10 | end 11 | 12 | def failing_job 13 | FailingJob.perform_later(params.to_unsafe_hash) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require rails-ujs 14 | //= require activestorage 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /spec/dummy/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/app/jobs/failing_job.rb: -------------------------------------------------------------------------------- 1 | class FailingJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(*_args) 5 | raise ArgumentError, "I'll keep failing and failing" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/models/admin_user.rb: -------------------------------------------------------------------------------- 1 | class AdminUser < ApplicationRecord 2 | # Include default devise modules. Others available are: 3 | # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable 4 | devise :database_authenticatable, 5 | :recoverable, :rememberable, :validatable 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/exception_hunter/b6f7cab9e9a9157147c5a165d0b79762c6158664/spec/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | # Include default devise modules. Others available are: 3 | # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable 4 | devise :database_authenticatable, :registerable, 5 | :recoverable, :rememberable, :validatable 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= csrf_meta_tags %> 6 | <%= csp_meta_tag %> 7 | 8 | <%= stylesheet_link_tag 'application', media: 'all' %> 9 | 10 | 11 | 12 | <%= yield %> 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /spec/dummy/bin/delayed_job: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) 4 | require 'delayed/command' 5 | Delayed::Command.new(ARGV).daemonize 6 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to 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 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?('config/database.yml') 22 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! 'bin/rails db:prepare' 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! 'bin/rails log:clear tmp:clear' 30 | 31 | puts "\n== Restarting application server ==" 32 | system! 'bin/rails restart' 33 | end 34 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(*Rails.groups) 6 | require "exception_hunter" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Initialize configuration defaults for originally generated Rails version. 11 | config.load_defaults 6.0 12 | 13 | # Settings in config/environments/* take precedence over those specified here. 14 | # Application configuration can go into files in config/initializers 15 | # -- all .rb files in that directory are automatically loaded after loading 16 | # the framework and any gems in your application. 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/dummy/config/application_5_2.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(*Rails.groups) 6 | require "exception_hunter" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Initialize configuration defaults for originally generated Rails version. 11 | config.load_defaults 5.2 12 | 13 | # Settings in config/environments/* take precedence over those specified here. 14 | # Application configuration can go into files in config/initializers 15 | # -- all .rb files in that directory are automatically loaded after loading 16 | # the framework and any gems in your application. 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/dummy/config/application_6_0.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(*Rails.groups) 6 | require "exception_hunter" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Initialize configuration defaults for originally generated Rails version. 11 | config.load_defaults 6.0 12 | 13 | # Settings in config/environments/* take precedence over those specified here. 14 | # Application configuration can go into files in config/initializers 15 | # -- all .rb files in that directory are automatically loaded after loading 16 | # the framework and any gems in your application. 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/dummy/config/application_6_1.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(*Rails.groups) 6 | require "exception_hunter" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Initialize configuration defaults for originally generated Rails version. 11 | config.load_defaults 6.0 12 | 13 | # Settings in config/environments/* take precedence over those specified here. 14 | # Application configuration can go into files in config/initializers 15 | # -- all .rb files in that directory are automatically loaded after loading 16 | # the framework and any gems in your application. 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) 6 | -------------------------------------------------------------------------------- /spec/dummy/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: dummy_production 11 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 9.3 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On macOS with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On macOS with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem 'pg' 16 | # 17 | default: &default 18 | adapter: postgresql 19 | encoding: unicode 20 | # For details on connection pooling, see Rails configuration guide 21 | # https://guides.rubyonrails.org/configuring.html#database-pooling 22 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 23 | 24 | development: 25 | <<: *default 26 | database: exception_tracker_development 27 | 28 | # The specified database role being used to connect to postgres. 29 | # To create additional roles in postgres see `$ createuser --help`. 30 | # When left blank, postgres will use the default role. This is 31 | # the same name as the operating system user that initialized the database. 32 | #username: exception_tracker 33 | 34 | # The password associated with the postgres role (username). 35 | #password: 36 | 37 | # Connect on a TCP socket. Omitted by default since the client uses a 38 | # domain socket that doesn't need configuration. Windows does not have 39 | # domain sockets, so uncomment these lines. 40 | #host: localhost 41 | 42 | # The TCP port the server listens on. Defaults to 5432. 43 | # If your server runs on a different port number, change accordingly. 44 | #port: 5432 45 | 46 | # Schema search path. The server defaults to $user,public 47 | #schema_search_path: myapp,sharedapp,public 48 | 49 | # Minimum log levels, in increasing order: 50 | # debug5, debug4, debug3, debug2, debug1, 51 | # log, notice, warning, error, fatal, and panic 52 | # Defaults to warning. 53 | #min_messages: notice 54 | 55 | # Warning: The database defined as "test" will be erased and 56 | # re-generated from your development database when you run "rake". 57 | # Do not set this db to the same as development or production. 58 | test: 59 | <<: *default 60 | database: exception_tracker_test 61 | 62 | # As with config/credentials.yml, you never want to store sensitive information, 63 | # like your database password, in your source code. If your source code is 64 | # ever seen by anyone, they now have access to your database. 65 | # 66 | # Instead, provide the password as a unix environment variable when you boot 67 | # the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database 68 | # for a full rundown on how to provide these environment variables in a 69 | # production deployment. 70 | # 71 | # On Heroku and other platform providers, you may have a full connection URL 72 | # available as an environment variable. For example: 73 | # 74 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 75 | # 76 | # You can use this database configuration with: 77 | # 78 | # production: 79 | # url: <%= ENV['DATABASE_URL'] %> 80 | # 81 | production: 82 | <<: *default 83 | database: exception_tracker_production 84 | username: exception_tracker 85 | password: <%= ENV['DUMMY_DATABASE_PASSWORD'] %> 86 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | # Run rails dev:cache to toggle caching. 17 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 18 | config.action_controller.perform_caching = true 19 | config.action_controller.enable_fragment_cache_logging = true 20 | 21 | config.cache_store = :memory_store 22 | config.public_file_server.headers = { 23 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 24 | } 25 | else 26 | config.action_controller.perform_caching = false 27 | 28 | config.cache_store = :null_store 29 | end 30 | 31 | # Store uploaded files on the local file system (see config/storage.yml for options). 32 | config.active_storage.service = :local 33 | 34 | config.active_job.queue_adapter = :delayed_job 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 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. This avoids loading your whole application 12 | # just for the purpose of running a single test. If you are using a tool that 13 | # preloads Rails for running tests, you may have to set it to true. 14 | config.eager_load = false 15 | 16 | # Configure public file server for tests with Cache-Control for performance. 17 | config.public_file_server.enabled = true 18 | config.public_file_server.headers = { 19 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 20 | } 21 | 22 | # Show full error reports and disable caching. 23 | config.consider_all_requests_local = true 24 | config.action_controller.perform_caching = false 25 | config.cache_store = :null_store 26 | 27 | config.active_job.queue_adapter = :delayed_job 28 | 29 | # Raise exceptions instead of rendering exception templates. 30 | config.action_dispatch.show_exceptions = true 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 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 13 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | 19 | # If you are using UJS then enable automatic nonce generation 20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 21 | 22 | # Set the nonce only to specific directives 23 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 24 | 25 | # Report CSP violations to a specified URI 26 | # For further information see the following documentation: 27 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 28 | # Rails.application.config.content_security_policy_report_only = true 29 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/exception_hunter.rb: -------------------------------------------------------------------------------- 1 | ExceptionHunter.setup do |config| 2 | # == Enabling 3 | # 4 | # This flag allows disabling error tracking, it's set to track in 5 | # any environment but development or test by default 6 | # 7 | # config.enabled = !(Rails.env.development? || Rails.env.test?) 8 | 9 | # == Dashboard User 10 | # Exception Hunter allows you to restrict users who can see the dashboard 11 | # to the ones included in the database. You can change the table name in 12 | # case you are not satisfied with the default one. You can also remove the 13 | # configuration if you wish to have no access restrictions for the dashboard. 14 | # 15 | config.admin_user_class = 'AdminUser' 16 | 17 | # == Current User 18 | # 19 | # Exception Hunter will include the user as part of the environment 20 | # data, if it was to be available. The default configuration uses devise 21 | # :current_user method. You can change it in case you named your user model 22 | # in some other way (i.e. Member). You can also remove the configuration if 23 | # you don't wish to track user data. 24 | # 25 | config.current_user_method = :current_user 26 | 27 | # == Current User Attributes 28 | # 29 | # Exception Hunter will try to include the attributes defined here 30 | # as part of the user information that is kept from the request. 31 | # 32 | config.user_attributes = [:id, :email] 33 | 34 | # == Stale errors 35 | # 36 | # You can configure how long it takes for errors to go stale. This is 37 | # taken into account when purging old error messages but nothing will 38 | # happen automatically. 39 | # 40 | # config.errors_stale_time = 45.days 41 | 42 | # == Slack notifications 43 | # 44 | # You can configure if you want to send notifications to slack for each error occurrence. 45 | # You can enter multiple webhook urls. 46 | # Default: [] 47 | # 48 | # config.notifiers << { 49 | # name: :slack, 50 | # options: { 51 | # webhook: 'SLACK_WEBHOOK_URL_1' 52 | # } 53 | # } 54 | # 55 | # config.notifiers << { 56 | # name: :slack, 57 | # options: { 58 | # webhook: SLACK_WEBHOOK_URL_2' 59 | # } 60 | # } 61 | 62 | # == Async Logging 63 | # 64 | # You can configure if you want to log errors async 65 | # Default: false 66 | # 67 | # config.async_logging = true 68 | end 69 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n 2 | 3 | en: 4 | devise: 5 | confirmations: 6 | confirmed: "Your email address has been successfully confirmed." 7 | send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." 8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." 9 | failure: 10 | already_authenticated: "You are already signed in." 11 | inactive: "Your account is not activated yet." 12 | invalid: "Invalid %{authentication_keys} or password." 13 | locked: "Your account is locked." 14 | last_attempt: "You have one more attempt before your account is locked." 15 | not_found_in_database: "Invalid %{authentication_keys} or password." 16 | timeout: "Your session expired. Please sign in again to continue." 17 | unauthenticated: "You need to sign in or sign up before continuing." 18 | unconfirmed: "You have to confirm your email address before continuing." 19 | mailer: 20 | confirmation_instructions: 21 | subject: "Confirmation instructions" 22 | reset_password_instructions: 23 | subject: "Reset password instructions" 24 | unlock_instructions: 25 | subject: "Unlock instructions" 26 | email_changed: 27 | subject: "Email Changed" 28 | password_change: 29 | subject: "Password Changed" 30 | omniauth_callbacks: 31 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"." 32 | success: "Successfully authenticated from %{kind} account." 33 | passwords: 34 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." 35 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." 36 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." 37 | updated: "Your password has been changed successfully. You are now signed in." 38 | updated_not_active: "Your password has been changed successfully." 39 | registrations: 40 | destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." 41 | signed_up: "Welcome! You have signed up successfully." 42 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." 43 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." 44 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." 45 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." 46 | updated: "Your account has been updated successfully." 47 | updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again" 48 | sessions: 49 | signed_in: "Signed in successfully." 50 | signed_out: "Signed out successfully." 51 | already_signed_out: "Signed out successfully." 52 | unlocks: 53 | send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." 54 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." 55 | unlocked: "Your account has been unlocked successfully. Please sign in to continue." 56 | errors: 57 | messages: 58 | already_confirmed: "was already confirmed, please try signing in" 59 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 60 | expired: "has expired, please request a new one" 61 | not_found: "not found" 62 | not_locked: "was not locked" 63 | not_saved: 64 | one: "1 error prohibited this %{resource} from being saved:" 65 | other: "%{count} errors prohibited this %{resource} from being saved:" 66 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /spec/dummy/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 12 | # 13 | port ENV.fetch("PORT") { 3000 } 14 | 15 | # Specifies the `environment` that Puma will run in. 16 | # 17 | environment ENV.fetch("RAILS_ENV") { "development" } 18 | 19 | # Specifies the `pidfile` that Puma will use. 20 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 21 | 22 | # Specifies the number of `workers` to boot in clustered mode. 23 | # Workers are forked web server processes. If using threads and workers together 24 | # the concurrency of the application would be max `threads` * `workers`. 25 | # Workers do not work on JRuby or Windows (both of which do not support 26 | # processes). 27 | # 28 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 29 | 30 | # Use the `preload_app!` method when specifying a `workers` number. 31 | # This directive tells Puma to first boot the application and load code 32 | # before forking the application. This takes advantage of Copy On Write 33 | # process behavior so workers use less memory. 34 | # 35 | # preload_app! 36 | 37 | # Allow puma to be restarted by `rails restart` command. 38 | plugin :tmp_restart 39 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | require 'sidekiq/web' 2 | 3 | Rails.application.routes.draw do 4 | ExceptionHunter.routes(self) 5 | devise_for :users 6 | mount Sidekiq::Web => '/sidekiq' 7 | 8 | get :raising_endpoint, to: 'exception#raising_endpoint' 9 | post :broken_post, to: 'exception#broken_post' 10 | post :failing_job, to: 'exception#failing_job' 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | ".ruby-version", 3 | ".rbenv-vars", 4 | "tmp/restart.txt", 5 | "tmp/caching-dev.txt" 6 | ) 7 | -------------------------------------------------------------------------------- /spec/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20200421131316_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DeviseCreateUsers < ActiveRecord::Migration[6.0] 4 | def change 5 | create_table :users do |t| 6 | ## Database authenticatable 7 | t.string :email, null: false, default: "" 8 | t.string :encrypted_password, null: false, default: "" 9 | 10 | ## Recoverable 11 | t.string :reset_password_token 12 | t.datetime :reset_password_sent_at 13 | 14 | ## Rememberable 15 | t.datetime :remember_created_at 16 | 17 | ## Trackable 18 | # t.integer :sign_in_count, default: 0, null: false 19 | # t.datetime :current_sign_in_at 20 | # t.datetime :last_sign_in_at 21 | # t.inet :current_sign_in_ip 22 | # t.inet :last_sign_in_ip 23 | 24 | ## Confirmable 25 | # t.string :confirmation_token 26 | # t.datetime :confirmed_at 27 | # t.datetime :confirmation_sent_at 28 | # t.string :unconfirmed_email # Only if using reconfirmable 29 | 30 | ## Lockable 31 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 32 | # t.string :unlock_token # Only if unlock strategy is :email or :both 33 | # t.datetime :locked_at 34 | 35 | 36 | t.timestamps null: false 37 | end 38 | 39 | add_index :users, :email, unique: true 40 | add_index :users, :reset_password_token, unique: true 41 | # add_index :users, :confirmation_token, unique: true 42 | # add_index :users, :unlock_token, unique: true 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20200601134028_devise_create_admin_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DeviseCreateAdminUsers < ActiveRecord::Migration[6.0] 4 | def change 5 | create_table :admin_users do |t| 6 | ## Database authenticatable 7 | t.string :email, null: false, default: "" 8 | t.string :encrypted_password, null: false, default: "" 9 | 10 | ## Recoverable 11 | t.string :reset_password_token 12 | t.datetime :reset_password_sent_at 13 | 14 | ## Rememberable 15 | t.datetime :remember_created_at 16 | 17 | ## Trackable 18 | # t.integer :sign_in_count, default: 0, null: false 19 | # t.datetime :current_sign_in_at 20 | # t.datetime :last_sign_in_at 21 | # t.inet :current_sign_in_ip 22 | # t.inet :last_sign_in_ip 23 | 24 | ## Confirmable 25 | # t.string :confirmation_token 26 | # t.datetime :confirmed_at 27 | # t.datetime :confirmation_sent_at 28 | # t.string :unconfirmed_email # Only if using reconfirmable 29 | 30 | ## Lockable 31 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 32 | # t.string :unlock_token # Only if unlock strategy is :email or :both 33 | # t.datetime :locked_at 34 | 35 | 36 | t.timestamps null: false 37 | end 38 | 39 | add_index :admin_users, :email, unique: true 40 | add_index :admin_users, :reset_password_token, unique: true 41 | # add_index :admin_users, :confirmation_token, unique: true 42 | # add_index :admin_users, :unlock_token, unique: true 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20200608130254_create_exception_hunter_error_groups.rb: -------------------------------------------------------------------------------- 1 | class CreateExceptionHunterErrorGroups < ActiveRecord::Migration[6.0] 2 | def change 3 | enable_extension :pg_trgm 4 | 5 | create_table :exception_hunter_error_groups do |t| 6 | t.string :error_class_name, null: false 7 | t.string :message 8 | t.integer :status, default: 0 9 | t.text :tags, array: true, default: [] 10 | 11 | t.timestamps 12 | 13 | t.index :message, opclass: :gin_trgm_ops, using: :gin 14 | t.index :status 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20200608130255_create_exception_hunter_errors.rb: -------------------------------------------------------------------------------- 1 | class CreateExceptionHunterErrors < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :exception_hunter_errors do |t| 4 | t.string :class_name, null: false 5 | t.string :message 6 | t.timestamp :occurred_at, null: false 7 | t.json :environment_data 8 | t.json :custom_data 9 | t.json :user_data 10 | t.string :backtrace, array: true, default: [] 11 | 12 | t.belongs_to :error_group, 13 | index: true, 14 | foreign_key: { to_table: :exception_hunter_error_groups } 15 | 16 | t.timestamps 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20200616151049_create_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | class CreateDelayedJobs < ActiveRecord::Migration[6.0] 2 | def self.up 3 | create_table :delayed_jobs, force: true do |table| 4 | table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue 5 | table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually. 6 | table.text :handler, null: false # YAML-encoded string of the object that will do work 7 | table.text :last_error # reason for last failure (See Note below) 8 | table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. 9 | table.datetime :locked_at # Set when a client is working on this object 10 | table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) 11 | table.string :locked_by # Who is working on this object (if locked) 12 | table.string :queue # The name of the queue this job is in 13 | table.timestamps null: true 14 | end 15 | 16 | add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority" 17 | end 18 | 19 | def self.down 20 | drop_table :delayed_jobs 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `rails 6 | # db:schema:load`. When creating a new database, `rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2020_06_16_151049) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "pg_trgm" 17 | enable_extension "plpgsql" 18 | 19 | create_table "admin_users", force: :cascade do |t| 20 | t.string "email", default: "", null: false 21 | t.string "encrypted_password", default: "", null: false 22 | t.string "reset_password_token" 23 | t.datetime "reset_password_sent_at" 24 | t.datetime "remember_created_at" 25 | t.datetime "created_at", precision: 6, null: false 26 | t.datetime "updated_at", precision: 6, null: false 27 | t.index ["email"], name: "index_admin_users_on_email", unique: true 28 | t.index ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true 29 | end 30 | 31 | create_table "delayed_jobs", force: :cascade do |t| 32 | t.integer "priority", default: 0, null: false 33 | t.integer "attempts", default: 0, null: false 34 | t.text "handler", null: false 35 | t.text "last_error" 36 | t.datetime "run_at" 37 | t.datetime "locked_at" 38 | t.datetime "failed_at" 39 | t.string "locked_by" 40 | t.string "queue" 41 | t.datetime "created_at", precision: 6 42 | t.datetime "updated_at", precision: 6 43 | t.index ["priority", "run_at"], name: "delayed_jobs_priority" 44 | end 45 | 46 | create_table "exception_hunter_error_groups", force: :cascade do |t| 47 | t.string "error_class_name", null: false 48 | t.string "message" 49 | t.integer "status", default: 0 50 | t.text "tags", default: [], array: true 51 | t.datetime "created_at", precision: 6, null: false 52 | t.datetime "updated_at", precision: 6, null: false 53 | t.index ["message"], name: "index_exception_hunter_error_groups_on_message", opclass: :gin_trgm_ops, using: :gin 54 | t.index ["status"], name: "index_exception_hunter_error_groups_on_status" 55 | end 56 | 57 | create_table "exception_hunter_errors", force: :cascade do |t| 58 | t.string "class_name", null: false 59 | t.string "message" 60 | t.datetime "occurred_at", null: false 61 | t.json "environment_data" 62 | t.json "custom_data" 63 | t.json "user_data" 64 | t.string "backtrace", default: [], array: true 65 | t.bigint "error_group_id" 66 | t.datetime "created_at", precision: 6, null: false 67 | t.datetime "updated_at", precision: 6, null: false 68 | t.index ["error_group_id"], name: "index_exception_hunter_errors_on_error_group_id" 69 | end 70 | 71 | create_table "users", force: :cascade do |t| 72 | t.string "email", default: "", null: false 73 | t.string "encrypted_password", default: "", null: false 74 | t.string "reset_password_token" 75 | t.datetime "reset_password_sent_at" 76 | t.datetime "remember_created_at" 77 | t.datetime "created_at", precision: 6, null: false 78 | t.datetime "updated_at", precision: 6, null: false 79 | t.index ["email"], name: "index_users_on_email", unique: true 80 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true 81 | end 82 | 83 | add_foreign_key "exception_hunter_errors", "exception_hunter_error_groups", column: "error_group_id" 84 | end 85 | -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/exception_hunter/b6f7cab9e9a9157147c5a165d0b79762c6158664/spec/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /spec/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/exception_hunter/b6f7cab9e9a9157147c5a165d0b79762c6158664/spec/dummy/log/.keep -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

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

63 |
64 |

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

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

The change you wanted was rejected.

62 |

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

63 |
64 |

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

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

We're sorry, but something went wrong.

62 |
63 |

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

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/dummy/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/exception_hunter/b6f7cab9e9a9157147c5a165d0b79762c6158664/spec/dummy/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /spec/dummy/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/exception_hunter/b6f7cab9e9a9157147c5a165d0b79762c6158664/spec/dummy/public/apple-touch-icon.png -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/exception_hunter/b6f7cab9e9a9157147c5a165d0b79762c6158664/spec/dummy/public/favicon.ico -------------------------------------------------------------------------------- /spec/error_reaper_spec.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | describe ErrorReaper do 3 | describe '.purge' do 4 | subject { described_class.purge(stale_time: stale_time) } 5 | let(:stale_time) { 1.month } 6 | 7 | let(:error_group) { create(:error_group) } 8 | let!(:old_errors) do 9 | (1..3).map { |i| create(:error, occurred_at: i.months.ago - 1.week, error_group: error_group) } 10 | end 11 | let!(:new_errors) do 12 | (1..2).map { |i| create(:error, occurred_at: i.weeks.ago, error_group: error_group) } 13 | end 14 | 15 | let!(:empty_error_group) { create(:error_group) } 16 | 17 | let(:only_old_errors_error_group) { create(:error_group) } 18 | let!(:old_error) { create(:error, occurred_at: 5.weeks.ago, error_group: only_old_errors_error_group) } 19 | 20 | it 'deletes errors with old occurrences' do 21 | expect { subject }.to change(Error, :count).by(-4) 22 | expect(Error.where(id: [old_error.id].concat(old_errors.map(&:id)))).not_to exist 23 | end 24 | 25 | it 'does not delete errors with new occurrences' do 26 | subject 27 | 28 | expect(Error.where(id: new_errors.map(&:id))).to exist 29 | end 30 | 31 | it 'deletes error groups with no associated errors' do 32 | expect { subject }.to change(ErrorGroup, :count).by(-2) 33 | expect(ErrorGroup.where(id: empty_error_group.id)).not_to exist 34 | end 35 | 36 | it 'does not delete error groups with associated errors' do 37 | subject 38 | 39 | expect(ErrorGroup.where(id: error_group.id)).to exist 40 | end 41 | 42 | it 'deletes error groups which have all associated errors with old occurrences' do 43 | subject 44 | 45 | expect(ErrorGroup.where(id: only_old_errors_error_group.id)).not_to exist 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/factories/exception_hunter/admin_user.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :admin_user do 3 | email { 'admin@example.com' } 4 | password { 'password' } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/exception_hunter/error_groups.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :error_group, class: 'ExceptionHunter::ErrorGroup' do 3 | error_class_name { 'ExceptionName' } 4 | message { 'Exception message' } 5 | status { :active } 6 | 7 | trait :ignored_group do 8 | status { :ignored } 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/factories/exception_hunter/errors.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :error, class: 'ExceptionHunter::Error' do 3 | class_name { 'ExceptionName' } 4 | message { 'Exception message' } 5 | occurred_at { Date.today } 6 | environment_data do 7 | { 8 | target: 'http://socialWebExample.com/users', 9 | referer: 'http://socialWebExample.com/', 10 | params: { controller: 'users', action: 'index' }, 11 | user_agent: 'Mozilla 5.0 (X11; Linux x86_64) AppleWebKit 534.30 (KHTML, like Gecko)', 12 | user_info: 'sid345' 13 | } 14 | end 15 | custom_data do 16 | { 17 | followers_ids: [4, 67, 98], 18 | follow_ids: [4, 67, 104, 502] 19 | } 20 | end 21 | backtrace do 22 | [ 23 | "activesupport (3.0.7) lib/active_support/whiny_nil.rb:48:in `method_missing'", 24 | "actionpack (3.0.7) lib/action_view/template.rb:135:in `block in render'", 25 | "activesupport (3.0.7) lib/active_support/notifications.rb:54:in `instrument'" 26 | ] 27 | end 28 | 29 | association(:error_group) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/helpers/exception_hunter/application_helper_spec.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | describe ::ExceptionHunter::ApplicationHelper do 3 | extend ::ExceptionHunter::ApplicationHelper 4 | 5 | describe '#application_name' do 6 | it 'returns a name of parent module' do 7 | expect(application_name).to eq('Dummy') 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/helpers/exception_hunter/errors_helper_spec.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | describe ErrorsHelper do 3 | extend ErrorsHelper 4 | 5 | describe '#format_tracked_data' do 6 | let(:tracked_data) do 7 | { 8 | something: 1, 9 | nested: [ 10 | 'abc', 11 | { email: 'example@email.com' } 12 | ] 13 | } 14 | end 15 | 16 | it 'returns a json string with correct indentation' do 17 | expect(format_tracked_data(tracked_data)).to eq("{\n \"something\": 1,\n \"nested\": [\n \"abc\",\n {\n \"email\": \"example@email.com\"\n }\n ]\n}") 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/jobs/async_logging_job_spec.rb: -------------------------------------------------------------------------------- 1 | describe ExceptionHunter::AsyncLoggingJob, type: :job do 2 | let(:tag) { ExceptionHunter::ErrorCreator::HTTP_TAG } 3 | let(:error_attributes) do 4 | { 5 | class_name: 'SomeError', 6 | message: 'Something went very wrong 123', 7 | environment_data: { 8 | hide: { value_to_hide: 'hide this value' }, 9 | "hide_this_too": 'hide this', 10 | hide_this_hash: { "hide_this_hash": 'hide this' } 11 | }, 12 | occurred_at: Time.now 13 | } 14 | end 15 | 16 | before do 17 | ExceptionHunter::Engine.configure do |config| 18 | config.routes.default_url_options = { host: 'localhost:3000' } 19 | end 20 | 21 | allow(ExceptionHunter::Config).to receive(:async_logging).and_return(true) 22 | end 23 | 24 | describe '#perform' do 25 | subject do 26 | ExceptionHunter::AsyncLoggingJob.perform_now(tag, error_attributes) 27 | end 28 | 29 | context 'logs the error in an async way' do 30 | it 'calls ErrorCreator call' do 31 | expect(ExceptionHunter::ErrorCreator).to receive(:call) 32 | 33 | subject 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/jobs/send_notification_job_spec.rb: -------------------------------------------------------------------------------- 1 | describe ExceptionHunter::SendNotificationJob, type: :job do 2 | let(:error) { create(:error) } 3 | let(:slack_notifier) { ExceptionHunter::Notifiers::SlackNotifier.new(error, notifier) } 4 | let(:serialized_slack_notifier) { ExceptionHunter::Notifiers::SlackNotifierSerializer.serialize(slack_notifier) } 5 | 6 | let(:notifier) do 7 | { 8 | name: :slack, 9 | options: { 10 | webhook: 'test_webhook' 11 | } 12 | } 13 | end 14 | 15 | before do 16 | ExceptionHunter::Engine.configure do |config| 17 | config.routes.default_url_options = { host: 'localhost:3000' } 18 | end 19 | end 20 | 21 | describe '#perform' do 22 | subject do 23 | ExceptionHunter::SendNotificationJob.perform_now(serialized_slack_notifier) 24 | end 25 | 26 | context 'sends notification to slack' do 27 | it 'calls ExceptionHunter::Notifiers::SlackNotifier notify' do 28 | expect_any_instance_of(ExceptionHunter::Notifiers::SlackNotifier) 29 | .to receive(:notify) 30 | 31 | subject 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/middleware/exception_hunter/delayed_job_hunter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'delayed_job' 2 | require 'exception_hunter/middleware/delayed_job_hunter' 3 | 4 | module ExceptionHunter 5 | describe Middleware::DelayedJobHunter do 6 | before do 7 | Delayed::Worker.delay_jobs = false 8 | end 9 | 10 | context 'with an ActiveJob' do 11 | subject { worker.perform_later(1, 'a') } 12 | 13 | let(:worker) do 14 | Class.new(ActiveJob::Base) do 15 | def perform(*) 16 | raise StandardError, 'Something happened!!!!' 17 | end 18 | end 19 | end 20 | 21 | it 'tracks exceptions on failing workers' do 22 | expect { subject rescue StandardError }.to change(Error, :count).by(1) 23 | end 24 | 25 | it 'tracks the correct data' do 26 | subject rescue StandardError 27 | 28 | error = Error.last 29 | 30 | expect(error.environment_data).to include({ 31 | 'arguments' => [1, 'a'], 32 | 'job_class' => 'ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper', 33 | 'queue_name' => 'default' 34 | }) 35 | if Gem::Version.new(Rails.version) >= Gem::Version.new('6') 36 | expect(error.environment_data['enqueued_at']).not_to be_nil 37 | end 38 | expect(error.environment_data['job_id']).not_to be_nil 39 | end 40 | end 41 | 42 | context 'with a normal class' do 43 | subject { worker_class.new.delay.raising_method } 44 | 45 | let(:worker_class) do 46 | Struct.new(:name) do 47 | def raising_method 48 | raise StandardError, 'Something happened!!!!' 49 | end 50 | end 51 | end 52 | let(:worker) { worker_class.new } 53 | 54 | it 'tracks exceptions on failing workers' do 55 | expect { subject rescue StandardError }.to change(Error, :count).by(1) 56 | end 57 | 58 | it 'tracks the correct data' do 59 | subject rescue StandardError 60 | 61 | error = Error.last 62 | 63 | expect(error.environment_data).to eq({ 64 | 'attempts' => 0, 65 | 'job_class' => 'Delayed::PerformableMethod' 66 | }) 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/middleware/exception_hunter/sidekiq_hunter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'sidekiq' 2 | require 'exception_hunter/middleware/sidekiq_hunter' 3 | 4 | module ExceptionHunter 5 | module Middleware 6 | describe SidekiqHunter do 7 | describe '#call' do 8 | subject { SidekiqHunter.new.call(nil, worker_context, nil) { worker.call } } 9 | let(:worker_context) do 10 | job_data.merge('args' => [worker_arguments]) 11 | end 12 | let(:job_data) do 13 | { 14 | 'queue' => 'default', 15 | 'other_stuff' => 'not to track' 16 | } 17 | end 18 | let(:worker_arguments) do 19 | { 20 | 'job_class' => 'SomeJob', 21 | 'job_id' => '63cbbe43-8dd2-40c3-9790-9f90009588ba', 22 | 'arguments' => ['143', 'User', 42], 23 | 'enqueued_at' => '2020-05-18T02:23:30Z', 24 | 'much_more_data' => 'not to track' 25 | } 26 | end 27 | 28 | context 'when the worker raises an exception' do 29 | let(:worker) { -> { raise 'Some error' } } 30 | 31 | it 're-raises the exception' do 32 | expect { subject }.to raise_error(RuntimeError, 'Some error') 33 | end 34 | 35 | it 'tracks the error' do 36 | expect { subject rescue nil } .to change(Error, :count).by(1) 37 | end 38 | 39 | it 'tracks data from the context' do 40 | subject rescue nil 41 | 42 | error = Error.last 43 | expect(error.environment_data).to eq({ 44 | 'queue' => 'default', 45 | 'job_class' => 'SomeJob', 46 | 'job_id' => '63cbbe43-8dd2-40c3-9790-9f90009588ba', 47 | 'arguments' => ['143', 'User', 42], 48 | 'enqueued_at' => '2020-05-18T02:23:30Z' 49 | }) 50 | end 51 | 52 | it 'adds the tag Worker to the error group' do 53 | subject rescue nil 54 | 55 | error = Error.last 56 | expect(error.error_group.tags).to eq(['Worker']) 57 | end 58 | end 59 | 60 | context 'when the worker is retrying' do 61 | context 'with a tracked retry' do 62 | let(:job_data) do 63 | { 64 | 'queue' => 'default', 65 | 'retry_count' => '3' 66 | } 67 | end 68 | 69 | it 'tracks the error' do 70 | expect { subject rescue nil } .to change(Error, :count).by(1) 71 | end 72 | end 73 | 74 | context 'with un un-tracked retry' do 75 | let(:job_data) do 76 | { 77 | 'queue' => 'default', 78 | 'retry_count' => '2' 79 | } 80 | end 81 | 82 | it 'does not create an error' do 83 | expect { subject rescue nil } .not_to change(Error, :count) 84 | end 85 | end 86 | end 87 | 88 | context 'when the worker does not raise an exception' do 89 | let(:worker) { -> {} } 90 | 91 | it 'does not create an error' do 92 | expect { subject rescue nil } .not_to change(Error, :count) 93 | end 94 | end 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/notifiers/slack_notifier_spec.rb: -------------------------------------------------------------------------------- 1 | describe ExceptionHunter::Notifiers::SlackNotifier do 2 | let(:error_group) { create(:error_group) } 3 | let(:error) { create(:error, error_group: error_group) } 4 | let(:slack_notifier) { ExceptionHunter::Notifiers::SlackNotifier.new(error, notifier) } 5 | 6 | let(:notifier) do 7 | { 8 | name: :slack, 9 | options: { 10 | webhook: 'test_webhook' 11 | } 12 | } 13 | end 14 | 15 | let(:notification_message) do 16 | { 17 | blocks: [ 18 | { 19 | type: 'section', 20 | text: { 21 | type: 'mrkdwn', 22 | text: error_message 23 | } 24 | } 25 | ] 26 | } 27 | end 28 | 29 | let(:error_message) do 30 | "*#{error.class_name}*: #{error.message}. \n" \ 31 | "<#{ExceptionHunter::Engine.routes.url_helpers.error_url(error.error_group)}|Click to see the error>" 32 | end 33 | 34 | before do 35 | ExceptionHunter::Engine.configure do |config| 36 | config.routes.default_url_options = { host: 'localhost:3000' } 37 | end 38 | end 39 | 40 | describe '#notify' do 41 | subject do 42 | slack_notifier.notify 43 | end 44 | 45 | context 'sends notification to slack' do 46 | it 'calls Slack::Notifier' do 47 | expect_any_instance_of(Slack::Notifier) 48 | .to receive(:ping) 49 | .with(notification_message) 50 | 51 | subject 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/presenters/exception_hunter/dashboard_presenter_spec.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | describe DashboardPresenter do 3 | describe '#current_tab' do 4 | it 'sets a default value when the tab is not valid' do 5 | presenter = DashboardPresenter.new('some_random_tab') 6 | expect(presenter.current_tab).to eq(DashboardPresenter::DEFAULT_TAB) 7 | end 8 | 9 | it 'sets a default value when the tab is nil' do 10 | presenter = DashboardPresenter.new(nil) 11 | expect(presenter.current_tab).to eq(DashboardPresenter::DEFAULT_TAB) 12 | end 13 | end 14 | 15 | describe '#partial_for_tab' do 16 | it 'returns a value for each valid tab' do 17 | DashboardPresenter::TABS.each do |tab| 18 | presenter = DashboardPresenter.new(tab) 19 | expect(presenter.partial_for_tab).not_to be_nil 20 | end 21 | end 22 | end 23 | 24 | describe '#tab_active?' do 25 | it 'returns true when the tab is active' do 26 | presenter = DashboardPresenter.new(DashboardPresenter::TOTAL_ERRORS_TAB) 27 | expect(presenter.tab_active?(DashboardPresenter::TOTAL_ERRORS_TAB)).to be true 28 | end 29 | 30 | it 'returns false when the tab is not active' do 31 | presenter = DashboardPresenter.new(DashboardPresenter::TOTAL_ERRORS_TAB) 32 | expect(presenter.tab_active?(DashboardPresenter::CURRENT_MONTH_TAB)).to be false 33 | end 34 | end 35 | 36 | describe '#errors_count' do 37 | it 'returns a value for each valid tab' do 38 | presenter = DashboardPresenter.new(DashboardPresenter::CURRENT_MONTH_TAB) 39 | DashboardPresenter::TABS.each do |tab| 40 | expect(presenter.errors_count(tab)).not_to be_nil 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/presenters/exception_hunter/error_group_presenter_spec.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHunter 2 | describe ErrorGroupPresenter do 3 | describe '.wrap_collection' do 4 | let(:error_groups) { create_list(:error_group, 3) } 5 | 6 | it 'returns an array of ErrorGroupPresenters' do 7 | expect(ErrorGroupPresenter.wrap_collection(error_groups)).to all(be_an ErrorGroupPresenter) 8 | end 9 | end 10 | 11 | describe '.format_occurrence_day' do 12 | let(:day) { Date.new(2020, 5, 30) } 13 | 14 | it 'returns the last occurrence date formatted as a string' do 15 | expect(ErrorGroupPresenter.format_occurrence_day(day)).to eq('Saturday, May 30') 16 | end 17 | 18 | context 'when the last occurrence happened yesterday' do 19 | let(:day) { Date.yesterday } 20 | 21 | it 'returns Yesterday instead of the date formatted as a string' do 22 | expect(ErrorGroupPresenter.format_occurrence_day(day)).to eq('Yesterday') 23 | end 24 | end 25 | end 26 | 27 | describe '#show_for_day?' do 28 | subject { presenter.show_for_day?(day) } 29 | let(:presenter) { ErrorGroupPresenter.new(error_group) } 30 | let(:error_group) { create(:error_group) } 31 | let(:day) { Date.yesterday } 32 | 33 | context 'when the last occurrence is on the day' do 34 | before do 35 | create(:error, occurred_at: day, error_group: error_group) 36 | end 37 | 38 | it 'returns true' do 39 | expect(subject).to be true 40 | end 41 | end 42 | 43 | context 'when the last occurrence is not on the day' do 44 | before do 45 | create(:error, occurred_at: day - 1.day, error_group: error_group) 46 | end 47 | 48 | it 'returns false' do 49 | expect(subject).to be false 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/presenters/exception_hunter/error_presenter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | module ExceptionHunter 4 | describe ErrorPresenter do 5 | let(:error_presenter) { ErrorPresenter.new(error) } 6 | 7 | describe '#environment_data' do 8 | subject { error_presenter.environment_data } 9 | 10 | context 'when the error has environment data' do 11 | let(:error) do 12 | create(:error, 13 | environment_data: { 14 | 'something' => 123, 15 | 'something_else' => 'abcd', 16 | 'params' => { 17 | 'name' => 'John' 18 | } 19 | }) 20 | end 21 | 22 | it 'returns the error environment data without the params' do 23 | expect(subject).to eq({ 'something' => 123, 'something_else' => 'abcd' }) 24 | end 25 | end 26 | 27 | context 'when the error does not have environment data' do 28 | let(:error) { create(:error, environment_data: nil) } 29 | 30 | it 'returns an empty hash' do 31 | expect(subject).to eq({}) 32 | end 33 | end 34 | end 35 | 36 | describe '#tracked_params' do 37 | subject { error_presenter.tracked_params } 38 | 39 | context 'when the environment data has tracked params' do 40 | let(:error) do 41 | create(:error, 42 | environment_data: { 43 | 'something' => 123, 44 | 'something_else' => 'abcd', 45 | 'params' => { 46 | 'name' => 'John' 47 | } 48 | }) 49 | end 50 | 51 | it 'returns those tracked params' do 52 | expect(subject).to eq({ 'name' => 'John' }) 53 | end 54 | end 55 | 56 | context 'when the environment data does not have tracked params' do 57 | let(:error) { create(:error, environment_data: { 'something' => 123 }) } 58 | 59 | it { is_expected.to be_nil } 60 | end 61 | end 62 | 63 | describe '#backtrace' do 64 | subject { error_presenter.backtrace } 65 | 66 | context 'when the line matches the expected format' do 67 | let(:error) do 68 | create(:error, backtrace: 69 | [ 70 | "activesupport (3.0.7) lib/active_support/whiny_nil.rb:48:in `method_missing'", 71 | "actionpack (3.0.7) lib/action_view/template.rb:135:in `block in render'" 72 | ]) 73 | end 74 | 75 | it 'returns the line presented' do 76 | expect(subject).to all be_a(ErrorPresenter::BacktraceLine) 77 | end 78 | 79 | it 'returns the different parts of the line' do 80 | built_lines = subject.map do |line| 81 | "#{line.path}/#{line.file_name}:#{line.line_number}:in `#{line.method_call}'" 82 | end 83 | 84 | expect(built_lines).to match_array(error.backtrace) 85 | end 86 | end 87 | 88 | # this is just to prevent unknown failures, no cases 89 | # have been found where this would actually happen 90 | context 'when the line does not match the expected format' do 91 | let(:error) do 92 | create(:error, backtrace: 93 | [ 94 | 'some unexpected path path', 95 | 'will this even happen? better be safe' 96 | ]) 97 | end 98 | 99 | it 'returns the line' do 100 | expect(subject).to match_array(error.backtrace) 101 | end 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | require 'spec_helper' 3 | ENV['RAILS_ENV'] ||= 'test' 4 | 5 | require File.expand_path('../spec/dummy/config/environment', __dir__) 6 | ActiveRecord::Migrator.migrations_paths = [File.expand_path('../spec/dummy/db/migrate', __dir__)] 7 | ActiveRecord::Migrator.migrations_paths << File.expand_path('../db/migrate', __dir__) 8 | 9 | # Prevent database truncation if the environment is production 10 | abort('The Rails environment is running in production mode!') if Rails.env.production? 11 | require 'rspec/rails' 12 | require 'factory_bot_rails' 13 | 14 | require 'support/controller_routes' 15 | require 'support/devise_request_spec_helpers' 16 | # Add additional requires below this line. Rails is not loaded until this point! 17 | 18 | # Requires supporting ruby files with custom matchers and macros, etc, in 19 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 20 | # run as spec files by default. This means that files in spec/support that end 21 | # in _spec.rb will both be required and run as specs, causing the specs to be 22 | # run twice. It is recommended that you do not name files matching this glob to 23 | # end with _spec.rb. You can configure this pattern with the --pattern 24 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 25 | # 26 | # The following line is provided for convenience purposes. It has the downside 27 | # of increasing the boot-up time by auto-requiring all files in the support 28 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 29 | # require only the support files necessary. 30 | # 31 | # Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f } 32 | 33 | # Checks for pending migrations and applies them before tests are run. 34 | # If you are not using ActiveRecord, you can remove these lines. 35 | begin 36 | ActiveRecord::Migration.maintain_test_schema! 37 | rescue ActiveRecord::PendingMigrationError => e 38 | puts e.to_s.strip 39 | exit 1 40 | end 41 | RSpec.configure do |config| 42 | config.include FactoryBot::Syntax::Methods 43 | config.include ControllerRoutes, type: :controller 44 | config.include ControllerRoutes, type: :routing 45 | config.include DeviseRequestSpecHelpers, type: :request 46 | config.include ExceptionHunter::Engine.routes.url_helpers 47 | 48 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 49 | # examples within a transaction, remove the following line or assign false 50 | # instead of true. 51 | config.use_transactional_fixtures = true 52 | 53 | # RSpec Rails can automatically mix in different behaviours to your tests 54 | # based on their file location, for example enabling you to call `get` and 55 | # `post` in specs under `spec/controllers`. 56 | # 57 | # You can disable this behaviour by removing the line below, and instead 58 | # explicitly tag your specs with their type, e.g.: 59 | # 60 | # RSpec.describe UsersController, :type => :controller do 61 | # # ... 62 | # end 63 | # 64 | # The different available types are documented in the features, such as in 65 | # https://relishapp.com/rspec/rspec-rails/docs 66 | config.infer_spec_type_from_file_location! 67 | 68 | # Filter lines from Rails gems in backtraces. 69 | config.filter_rails_from_backtrace! 70 | # arbitrary gems may also be filtered via: 71 | # config.filter_gems_from_backtrace("gem name") 72 | end 73 | 74 | require 'shoulda/matchers' 75 | 76 | Shoulda::Matchers.configure do |config| 77 | config.integrate do |with| 78 | with.test_framework :rspec 79 | with.library :rails 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/requests/exception_hunter/exceptions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | module ExceptionHunter 4 | describe 'GET #raising_endpoint' do 5 | subject { get raising_endpoint_path, params: params } 6 | let(:params) do 7 | { 8 | 'some_id' => 3, 9 | 'some_data' => { 10 | 'name' => 'John', 11 | 'email' => 'invalid-email.com' 12 | } 13 | } 14 | end 15 | 16 | it 'creates a record to track the error' do 17 | expect { subject }.to change { Error.count }.by(1) 18 | end 19 | 20 | describe 'tracked data' do 21 | let(:error) { Error.last } 22 | 23 | before do 24 | subject 25 | end 26 | 27 | it 'tracks the correct class name' do 28 | expect(error.class_name).to eq(ArgumentError.to_s) 29 | end 30 | 31 | it 'tracks the error message' do 32 | expect(error.message).to eq('You should not have called me') 33 | end 34 | 35 | it 'tracks the occurrence time' do 36 | expect(error.occurred_at).to be_within(1.second).of(Time.now) 37 | end 38 | 39 | it 'tracks the environment data' do 40 | expect(error.environment_data).to include( 41 | 'HTTP_HOST' => 'www.example.com', 42 | 'PATH_INFO' => '/raising_endpoint', 43 | 'QUERY_STRING' => 'some_id=3&some_data[name]=John&some_data[email]=invalid-email.com', 44 | 'REQUEST_METHOD' => 'GET', 45 | 'REQUEST_URI' => '/raising_endpoint' 46 | ) 47 | end 48 | end 49 | end 50 | 51 | describe 'POST #broken_post' do 52 | subject { post broken_post_path(some_query_param: 'I go in the URL'), params: params } 53 | let(:params) do 54 | { 55 | 'some_id' => 3, 56 | 'some_data' => { 57 | 'name' => 'John', 58 | 'email' => 'invalid-email.com' 59 | } 60 | } 61 | end 62 | let(:error) { Error.last } 63 | 64 | before do 65 | subject 66 | end 67 | 68 | it 'tracks the params' do 69 | expect(error.environment_data['params']).to eq({ 70 | 'some_id' => '3', 71 | 'some_query_param' => 'I go in the URL', 72 | 'action' => 'broken_post', 73 | 'controller' => 'exception', 74 | 'some_data' => { 75 | 'name' => 'John', 76 | 'email' => 'invalid-email.com' 77 | } 78 | }) 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/support/controller_routes.rb: -------------------------------------------------------------------------------- 1 | module ControllerRoutes 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | routes { ::ExceptionHunter::Engine.routes } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/devise_request_spec_helpers.rb: -------------------------------------------------------------------------------- 1 | module DeviseRequestSpecHelpers 2 | include Warden::Test::Helpers 3 | 4 | def sign_in(resource_or_scope, resource = nil) 5 | resource ||= resource_or_scope 6 | scope = Devise::Mapping.find_scope!(resource_or_scope) 7 | login_as(resource, scope: scope) 8 | end 9 | 10 | def sign_out(resource_or_scope) 11 | scope = Devise::Mapping.find_scope!(resource_or_scope) 12 | logout(scope) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/tasks/purge_errors_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | Rails.application.load_tasks 4 | 5 | describe 'exception_hunter:purge_errors' do 6 | it 'calls the ErrorReaper purge method' do 7 | expect(::ExceptionHunter::ErrorReaper).to receive(:purge) 8 | 9 | Rake::Task['exception_hunter:purge_errors'].invoke 10 | end 11 | end 12 | --------------------------------------------------------------------------------