diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 17b27aa95..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,207 +0,0 @@ -version: 2 -references: - repo_restore_cache: &repo_restore_cache - restore_cache: - keys: - - repo-{{ .Environment.CIRCLE_SHA1 }} - repo_save_cache: &repo_save_cache - save_cache: - key: repo-{{ .Environment.CIRCLE_SHA1 }}-{{ epoch }} - paths: - - ~/airbrake - bundle_install: &bundle_install - run: - name: Install Bundler dependencies - command: bundle install --path ~/airbrake/vendor/bundle --jobs 15 - appraisal_install: &appraisal_install - run: - name: Install Appraisal dependencies - command: bundle exec appraisal install --jobs 15 - unit: &unit - run: - name: Run unit tests - command: bundle exec rake spec:unit - rails41: &rails41 - run: - name: Test Rails 4.1 - command: bundle exec appraisal rails-4.1 rake spec:integration:rails - rails42: &rails42 - run: - name: Test Rails 4.2 - command: bundle exec appraisal rails-4.2 rake spec:integration:rails - rails50: &rails50 - run: - name: Test Rails 5.0 - command: bundle exec appraisal rails-5.0 rake spec:integration:rails - rails51: &rails51 - run: - name: Test Rails 5.1 - command: bundle exec appraisal rails-5.1 rake spec:integration:rails - rails52: &rails52 - run: - name: Test Rails 5.2 - command: bundle exec appraisal rails-5.2 rake spec:integration:rails - rails60: &rails60 - run: - name: Test Rails 6.0 - command: bundle exec appraisal rails-6.0 rake spec:integration:rails - sinatra: &sinatra - run: - name: Test Sinatra - command: bundle exec appraisal sinatra rake spec:integration:sinatra - rack: &rack - run: - name: Test Rack - command: bundle exec appraisal rack rake spec:integration:rack -jobs: - lint: - docker: - - image: circleci/ruby:2.7 - auth: - username: $DOCKERHUB_USER - password: $DOCKERHUB_PASSWORD - working_directory: ~/airbrake - steps: - - checkout - - <<: *repo_save_cache - - <<: *bundle_install - - run: - name: Run RuboCop linting - command: bundle exec rubocop --parallel - - run: - name: Verify all @since tag versions start with letter 'v' - command: | - ! egrep -r "# @since [0-9]" lib - "ruby-2.3": - docker: - - image: circleci/ruby:2.3 - auth: - username: $DOCKERHUB_USER - password: $DOCKERHUB_PASSWORD - working_directory: ~/airbrake - steps: - - <<: *repo_restore_cache - - <<: *bundle_install - - <<: *appraisal_install - - <<: *unit - - <<: *rails41 - - <<: *rails42 - - <<: *rails50 - - <<: *rails51 - - <<: *rails52 - - <<: *sinatra - - <<: *rack - "ruby-2.4": - docker: - - image: circleci/ruby:2.4 - auth: - username: $DOCKERHUB_USER - password: $DOCKERHUB_PASSWORD - working_directory: ~/airbrake - steps: - - <<: *repo_restore_cache - - <<: *bundle_install - - <<: *appraisal_install - - <<: *unit - - <<: *rails42 - - <<: *rails50 - - <<: *rails51 - - <<: *rails52 - - <<: *sinatra - - <<: *rack - "ruby-2.5": - docker: - - image: circleci/ruby:2.5 - auth: - username: $DOCKERHUB_USER - password: $DOCKERHUB_PASSWORD - working_directory: ~/airbrake - steps: - - <<: *repo_restore_cache - - <<: *bundle_install - - <<: *appraisal_install - - <<: *unit - - <<: *rails42 - - <<: *rails51 - - <<: *rails52 - - <<: *rails60 - - <<: *sinatra - - <<: *rack - "ruby-2.6": - docker: - - image: circleci/ruby:2.6 - auth: - username: $DOCKERHUB_USER - password: $DOCKERHUB_PASSWORD - working_directory: ~/airbrake - steps: - - <<: *repo_restore_cache - - <<: *bundle_install - - <<: *appraisal_install - - <<: *unit - - <<: *rails42 - - <<: *rails50 - - <<: *rails51 - - <<: *rails52 - - <<: *rails60 - - <<: *sinatra - - <<: *rack - "ruby-2.7": - docker: - - image: circleci/ruby:2.7 - auth: - username: $DOCKERHUB_USER - password: $DOCKERHUB_PASSWORD - working_directory: ~/airbrake - steps: - - <<: *repo_restore_cache - - <<: *bundle_install - - <<: *appraisal_install - - <<: *unit - - <<: *rails50 - - <<: *rails51 - - <<: *rails52 - - <<: *rails60 - - <<: *sinatra - - <<: *rack - "jruby-9.2": - docker: - - image: circleci/jruby:9.2 - auth: - username: $DOCKERHUB_USER - password: $DOCKERHUB_PASSWORD - working_directory: ~/airbrake - steps: - - <<: *repo_restore_cache - - <<: *bundle_install - - <<: *appraisal_install - - <<: *unit - - <<: *rails51 - - <<: *rails52 - - <<: *rails60 - - <<: *sinatra - - <<: *rack - -workflows: - version: 2 - build: - jobs: - - lint - - "ruby-2.3": - requires: - - lint - - "ruby-2.4": - requires: - - lint - - "ruby-2.5": - requires: - - lint - - "ruby-2.6": - requires: - - lint - - "ruby-2.7": - requires: - - lint - - "jruby-9.2": - requires: - - lint diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 525316af7..ec31adcaf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,3 +10,9 @@ updates: - dependency-name: ethon versions: - 0.13.0 +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + time: "19:00" + timezone: US/Central diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..fe0766058 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,64 @@ +name: airbrake + +on: [push] + +permissions: + contents: read + +jobs: + test: + strategy: + matrix: + os: [ubuntu-latest] + ruby: [2.6, 2.7, 3.0, 3.1, jruby] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v3 + + - name: Update package list + run: sudo apt-get update + + - name: Install cURL Headers + run: sudo apt-get install libcurl4 libcurl4-openssl-dev + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - name: Rubocop lint + run: bundle exec rubocop + + - name: Install Bundler dependencies + run: bundle install + + - name: Install Appraisal dependencies + run: bundle exec appraisal install + + - name: Display Ruby version + run: ruby -v + + - name: Unit tests + run: bundle exec rake spec:unit + + - name: Test Rails 5.2 + if: ${{ matrix.ruby != '3.0' && matrix.ruby != '3.1' }} + run: bundle exec appraisal rails-5.2 rake spec:integration:rails + + - name: Test Rails 6.0 + run: bundle exec appraisal rails-6.0 rake spec:integration:rails + + - name: Test Rails 6.1 + run: bundle exec appraisal rails-6.1 rake spec:integration:rails + + - name: Test Rails 7.0 + if: ${{ matrix.ruby != '2.6' && matrix.ruby != 'jruby' }} + run: bundle exec appraisal rails-7.0 rake spec:integration:rails + + - name: Test Sinatra + run: bundle exec appraisal sinatra rake spec:integration:sinatra + + - name: Test Rack + run: bundle exec appraisal rack rake spec:integration:rack diff --git a/.rubocop.yml b/.rubocop.yml index 560391fc3..94f7bd474 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,9 +1,13 @@ # Explanations of all possible options: # https://github.com/bbatsov/rubocop/blob/master/config/default.yml AllCops: - TargetRubyVersion: 2.3 + TargetRubyVersion: 2.6 DisplayCopNames: true DisplayStyleGuide: true + NewCops: enable + Exclude: + - 'gemfiles/*.gemfile' + - 'vendor/**/*' Metrics/MethodLength: Max: 25 @@ -41,6 +45,9 @@ Style/FrozenStringLiteralComment: Metrics/BlockLength: CountComments: false Max: 25 + IgnoredMethods: + - describe + - context Exclude: - 'Rakefile' - '**/*.rake' diff --git a/Appraisals b/Appraisals index 8d41fc41b..3ffd88298 100644 --- a/Appraisals +++ b/Appraisals @@ -1,106 +1,75 @@ # frozen_string_literal: true -if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7.0') && RUBY_ENGINE != 'jruby' - appraise 'rails-4.1' do - gem 'rails', '~> 4.1.16' - gem 'warden', '~> 1.2.3' - - gem 'activerecord-jdbcsqlite3-adapter', '~> 1.3.18', platforms: :jruby - gem 'sqlite3', '~> 1.3.11', platforms: %i[mri rbx] - - gem 'resque', '~> 1.25.2' - gem 'resque_spec', github: 'airbrake/resque_spec' - - gem 'delayed_job_active_record', '~> 4.1.0' - - gem 'mime-types', '~> 3.1' - gem 'sprockets', '~> 3.7' - gem 'rack', '~> 1' - end - - appraise 'rails-4.2' do - gem 'rails', '~> 4.2.10' - gem 'warden', '~> 1.2.3' +# Rails 5 doesn't work on Ruby 3+. +if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.0.0') + appraise 'rails-5.2' do + gem 'rails', '~> 5.2.0' + gem 'warden', '~> 1.2.6' + gem 'rack', '~> 2.0' - gem 'activerecord-jdbcsqlite3-adapter', '~> 1.3.18', platforms: :jruby - gem 'sqlite3', '~> 1.3.11', platforms: %i[mri rbx] + gem 'activerecord-jdbcsqlite3-adapter', '~> 52.0', platforms: :jruby + gem 'sqlite3', '~> 1.4', platforms: %i[mri rbx] - gem 'resque', '~> 1.25.2' + gem 'resque', '~> 1.26' gem 'resque_spec', github: 'airbrake/resque_spec' - gem 'delayed_job_active_record', '~> 4.1.0' + gem 'delayed', '~> 0.4' gem 'mime-types', '~> 3.1' - gem 'sprockets', '~> 3.7' - gem 'rack', '~> 1' end end -appraise 'rails-5.0' do - gem 'rails', '~> 5.0.7' - gem 'warden', '~> 1.2.3' - - gem 'activerecord-jdbcsqlite3-adapter', '~> 1.3.18', platforms: :jruby - gem 'sqlite3', '~> 1.3.11', platforms: %i[mri rbx] - - gem 'resque', '~> 1.25.2' - gem 'resque_spec', github: 'airbrake/resque_spec' - - gem 'delayed_job_active_record', '~> 4.1.0' - - gem 'mime-types', '~> 3.1' - gem 'sprockets', '~> 3.7' -end - -appraise 'rails-5.1' do - gem 'rails', '~> 5.1.4' +appraise 'rails-6.0' do + gem 'rails', '~> 6.0.4.1' gem 'warden', '~> 1.2.6' + gem 'rack', '~> 2.0' - gem 'activerecord-jdbcsqlite3-adapter', '~> 51.0', platforms: :jruby - gem 'sqlite3', '~> 1.3.11', platforms: %i[mri rbx] + gem 'activerecord-jdbcsqlite3-adapter', '~> 60.1', platforms: :jruby + gem 'sqlite3', '~> 1.4', platforms: %i[mri rbx] gem 'resque', '~> 1.26' gem 'resque_spec', github: 'airbrake/resque_spec' - gem 'delayed_job', github: 'collectiveidea/delayed_job' - gem 'delayed_job_active_record', '~> 4.1' + gem 'delayed', '~> 0.4' gem 'mime-types', '~> 3.1' - gem 'sprockets', '~> 3.7' end -appraise 'rails-5.2' do - gem 'rails', '~> 5.2.0' +appraise 'rails-6.1' do + gem 'rails', '~> 6.1.4.1' gem 'warden', '~> 1.2.6' gem 'rack', '~> 2.0' - gem 'activerecord-jdbcsqlite3-adapter', '~> 52.0', platforms: :jruby - gem 'sqlite3', '~> 1.3.11', platforms: %i[mri rbx] + gem 'activerecord-jdbcsqlite3-adapter', + github: 'jruby/activerecord-jdbc-adapter', + branch: '61-stable', + platforms: :jruby + gem 'sqlite3', '~> 1.4', platforms: %i[mri rbx] gem 'resque', '~> 1.26' gem 'resque_spec', github: 'airbrake/resque_spec' - gem 'delayed_job', github: 'collectiveidea/delayed_job' - gem 'delayed_job_active_record', '~> 4.1' + gem 'delayed', '~> 0.4' gem 'mime-types', '~> 3.1' end -# Rails 6.0+ supports only modern Rubies (2.5+) -if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.5') - appraise 'rails-6.0' do - gem 'rails', '~> 6.0.2' +if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7.0') + appraise 'rails-7.0' do + gem 'rails', '~> 7.0.1' gem 'warden', '~> 1.2.6' gem 'rack', '~> 2.0' - gem 'activerecord-jdbcsqlite3-adapter', '~> 60.1', platforms: :jruby + gem 'activerecord-jdbcsqlite3-adapter', + github: 'jruby/activerecord-jdbc-adapter', + branch: '61-stable', + platforms: :jruby gem 'sqlite3', '~> 1.4', platforms: %i[mri rbx] gem 'resque', '~> 1.26' gem 'resque_spec', github: 'airbrake/resque_spec' - gem 'delayed_job', github: 'collectiveidea/delayed_job' - gem 'delayed_job_active_record', '~> 4.1' + gem 'delayed', '~> 0.4' gem 'mime-types', '~> 3.1' end diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f504c70..837e541e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,70 @@ Airbrake Changelog ### master +### [v13.0.2][v13.0.2] (May 20, 2022) + +* Fixed support of APM on Rails 7+, where the reported time of a performance + breakdowns and queries malformed, resulting in the complete rejection of the + performance breakdowns or queries by the backend. This improves on the fix + introduced in v13.0.1 + ([#1227](https://github.com/airbrake/airbrake/issues/1227)) + +### [v13.0.1][v13.0.1] (May 13, 2022) + +* Fixed support of APM on Rails 7+, where the reported time of a route was + malformed, resulting in the complete rejection of the route stats by the + backend ([#1223](https://github.com/airbrake/airbrake/issues/1223)) +* Fixed bug where Rails 6+ apps that don't require ActiveRecord crash when + Airbrake is installed + ([#1224](https://github.com/airbrake/airbrake/pull/1224)) + +### [v13.0.0][v13.0.0] (January 18, 2022) + +Breaking changes: + +* Dropped support for Ruby 2.5 + ([#1208](https://github.com/airbrake/airbrake/issues/1208)) + +Bug fixes: + +* Fixed APM not working on Rails 7 due to ``NoMethodError (undefined method + `glob?' for nil:NilClass)`` + ([#1211](https://github.com/airbrake/airbrake/issues/1211)) + +### [v12.0.0][v12.0.0] (September 22, 2021) + +Breaking changes: + +* Dropped support for Ruby 2.3 + ([#1180](https://github.com/airbrake/airbrake/issues/1180)) +* Dropped support for Ruby 2.4 + ([#1180](https://github.com/airbrake/airbrake/issues/1180)) + +Maintenance: + +* Bumped `airbrake-ruby` requirement to `~> 6.0` + ([#1179](https://github.com/airbrake/airbrake/pull/1179)) + +Other changes: + +* Rails generator no longer embeds project id & project key into the generated + initializer file. Set environment variables instead. + + Before: + + ```sh + % rails g airbrake PROJECT_ID PROJECT_KEY + ``` + + After: + + ```sh + export AIRBRAKE_PROJECT_ID= + export AIRBRAKE_PROJECT_KEY= + + rails g airbrake + ``` + ### [v11.0.3][v11.0.3] (May 13, 2021) * Fixed `Sneakers` integration when 3rd party code monkey-patches @@ -258,12 +322,12 @@ Features: ### [v9.2.2][v9.2.2] (May 10, 2019) -* Rails: started attaching Rack request and User info to the resource object, +* Rails: started attaching Rack request and User info to the metric object, which is accessible through performance hooks: ```ruby - Airbrake.add_performance_filter do |resource| - if resource.stash.key?(:user) + Airbrake.add_performance_filter do |metric| + if metric.stash.key?(:user) # custom logic end end @@ -948,3 +1012,7 @@ Features: [v11.0.1]: https://github.com/airbrake/airbrake/releases/tag/v11.0.1 [v11.0.2]: https://github.com/airbrake/airbrake/releases/tag/v11.0.2 [v11.0.3]: https://github.com/airbrake/airbrake/releases/tag/v11.0.3 +[v12.0.0]: https://github.com/airbrake/airbrake/releases/tag/v12.0.0 +[v13.0.0]: https://github.com/airbrake/airbrake/releases/tag/v13.0.0 +[v13.0.1]: https://github.com/airbrake/airbrake/releases/tag/v13.0.1 +[v13.0.2]: https://github.com/airbrake/airbrake/releases/tag/v13.0.2 diff --git a/Gemfile b/Gemfile index f34491897..144688c3b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,5 @@ source 'https://rubygems.org' gemspec -gem 'rubocop', '= 0.81', require: false +gem 'rubocop', '~> 1.21', require: false +gem 'sneakers', github: 'jondot/sneakers', ref: '31d0cb25dc5bbcfb0749567e9e0f80e6353fb66b' diff --git a/LICENSE.md b/LICENSE.md index 114f60f9c..3522d9ba9 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,7 @@ The MIT License =============== -Copyright © 2021 Airbrake Technologies, Inc. +Copyright © 2022 Airbrake Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in diff --git a/README.md b/README.md index 4b56b0731..3cbd213f1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Airbrake ======== -[![Circle Build Status](https://circleci.com/gh/airbrake/airbrake.png?style=shield)](https://circleci.com/gh/airbrake/airbrake) +[![Build Status](https://github.com/airbrake/airbrake/workflows/airbrake/badge.svg)](https://github.com/airbrake/airbrake/actions) [![Code Climate](https://codeclimate.com/github/airbrake/airbrake.svg)](https://codeclimate.com/github/airbrake/airbrake) [![Gem Version](https://badge.fury.io/rb/airbrake.svg)](http://badge.fury.io/rb/airbrake) [![Documentation Status](http://inch-ci.org/github/airbrake/airbrake.svg?branch=master)](http://inch-ci.org/github/airbrake/airbrake) @@ -47,26 +47,26 @@ The list of integrations that are available in this gem includes: * [Heroku support][heroku-docs] (as an [add-on][heroku-addon]) * Web frameworks - * Rails[[link](#rails)] - * Sinatra[[link](#sinatra)] - * Rack applications[[link](#rack)] + * [Rails](#rails) + * [Sinatra](#sinatra) + * [Rack applications](#rack) * Job processing libraries - * ActiveJob[[link](#activejob)] - * Resque[[link](#resque)] - * Sidekiq[[link](#sidekiq)] - * DelayedJob[[link](#delayedjob)] - * Shoryuken[[link](#shoryuken)] - * Sneakers[[link](#sneakers)] + * [ActiveJob](#activejob) + * [Resque](#resque) + * [Sidekiq](#sidekiq) + * [DelayedJob](#delayedjob) + * [Shoryuken](#shoryuken) + * [Sneakers](#sneakers) * Other libraries - * ActionCable[[link](#actioncable)] - * Rake[[link](#rake)] - * Logger[[link](#logger)] -* Plain Ruby scripts[[link](#plain-ruby-scripts)] + * [ActionCable](#actioncable) + * [Rake](#rake) + * [Logger](#logger) +* [Plain Ruby scripts](#plain-ruby-scripts) Deployment tracking: -* Using Capistrano[[link](#capistrano)] -* Using the Rake task[[link](#rake-task)] +* Using [Capistrano](#capistrano) +* Using the [Rake task](#rake-task) Installation ------------ @@ -94,17 +94,21 @@ Configuration #### Integration -To integrate Airbrake with your Rails application, you need to know -your [project id and project key][project-idkey]. Invoke the following command -and replace `PROJECT_ID` and `PROJECT_KEY` with your values: +To integrate Airbrake with your Rails application, you need to know your +[project id and project key][project-idkey]. Set `AIRBRAKE_PROJECT_ID` & +`AIRBRAKE_PROJECT_KEY` environment variables with your project's values and +generate the Airbrake config: ```bash -rails g airbrake PROJECT_ID PROJECT_KEY +export AIRBRAKE_PROJECT_ID= +export AIRBRAKE_PROJECT_KEY= + +rails g airbrake ``` -[Heroku add-on][heroku-addon] users can omit specifying the key and the id and -invoke the command without arguments (Heroku add-on's environment variables will -be used) ([Heroku add-on docs][heroku-docs]): +[Heroku add-on][heroku-addon] users can omit specifying the key and the +id. Heroku add-on's environment variables will be used ([Heroku add-on +docs][heroku-docs]): ```bash rails g airbrake @@ -488,7 +492,7 @@ wrap your logger in Airbrake's decorator class: require 'airbrake/logger' # Create a normal logger -logger = Logger.new(STDOUT) +logger = Logger.new($stdout) # Wrap it logger = Airbrake::AirbrakeLogger.new(logger) @@ -647,7 +651,7 @@ Then, invoke it like shown in the example for Rails. Supported Rubies ---------------- -* CRuby >= 2.3.0 +* CRuby >= 2.6.0 * JRuby >= 9k Contact @@ -698,8 +702,8 @@ bundle exec appraisal rails-4.2 rake spec:integration:rails bundle exec appraisal sinatra rake spec:integration:sinatra ``` -Pro tip: [CircleCI config](/.circleci/config.yml) has the list of all -integration tests and commands to invoke them. +Pro tip: [GitHub Actions config](/.github/workflows/test.yml) has the list of +all the integration tests and commands to invoke them. [airbrake.io]: https://airbrake.io [airbrake-ruby]: https://github.com/airbrake/airbrake-ruby diff --git a/airbrake.gemspec b/airbrake.gemspec index f5f7abd04..64e117d4f 100644 --- a/airbrake.gemspec +++ b/airbrake.gemspec @@ -3,7 +3,6 @@ require './lib/airbrake/version' Gem::Specification.new do |s| s.name = 'airbrake' s.version = Airbrake::AIRBRAKE_VERSION.dup - s.date = Time.now.strftime('%Y-%m-%d') s.summary = < 5.1' + s.metadata = { + 'rubygems_mfa_required' => 'true', + } + + s.add_dependency 'airbrake-ruby', '~> 6.0' s.add_development_dependency 'rspec', '~> 3' s.add_development_dependency 'rspec-wait', '~> 0' @@ -40,24 +43,18 @@ DESC s.add_development_dependency 'rack', '~> 2' s.add_development_dependency 'webmock', '~> 3' s.add_development_dependency 'amq-protocol' - s.add_development_dependency 'sneakers', '~> 2' - s.add_development_dependency 'rack-test', '= 0.6.3' - s.add_development_dependency 'redis', '= 4.1.4' - s.add_development_dependency 'sidekiq', '~> 5' - s.add_development_dependency 'curb', '~> 0.9' if RUBY_ENGINE == 'ruby' + s.add_development_dependency 'rack-test', '~> 1.1' + s.add_development_dependency 'redis', '~> 4.5' + s.add_development_dependency 'sidekiq', '~> 6' + s.add_development_dependency 'curb', '~> 1.0' if RUBY_ENGINE == 'ruby' s.add_development_dependency 'excon', '~> 0.64' - s.add_development_dependency 'http', '~> 2.2' + s.add_development_dependency 'http', '~> 5.0' s.add_development_dependency 'httpclient', '~> 2.8' s.add_development_dependency 'typhoeus', '~> 1.3' - # Fixes build failure with ethon v0.13.0 - # https://app.circleci.com/pipelines/github/airbrake/airbrake/158/workflows/f22d902f-f0bb-449b-8b95-2a0ac76047f2 - s.add_development_dependency 'ethon', '= 0.12.0' - # Fixes build failure with public_suffix v3 # https://circleci.com/gh/airbrake/airbrake-ruby/889 s.add_development_dependency 'public_suffix', '~> 4.0', '< 5.0' - # redis-namespace > 1.6.0 wants Ruby >= 2.4. - s.add_development_dependency 'redis-namespace', '= 1.6.0' + s.add_development_dependency 'redis-namespace', '~> 1.8' end diff --git a/gemfiles/rack.gemfile b/gemfiles/rack.gemfile index b30d6525d..fb5db120a 100644 --- a/gemfiles/rack.gemfile +++ b/gemfiles/rack.gemfile @@ -2,7 +2,8 @@ source "https://rubygems.org" -gem "rubocop", "= 0.55", require: false +gem "rubocop", "~> 1.21", require: false +gem "sneakers", github: "jondot/sneakers", ref: "31d0cb25dc5bbcfb0749567e9e0f80e6353fb66b" gem "warden", "~> 1.2.6" gemspec path: "../" diff --git a/gemfiles/rails_4.0.gemfile b/gemfiles/rails_4.0.gemfile deleted file mode 100644 index b2b99eeed..000000000 --- a/gemfiles/rails_4.0.gemfile +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rubocop", "= 0.51", require: false -gem "rails", "~> 4.0.13" -gem "warden", "~> 1.2.3" -gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.18", platforms: :jruby -gem "sqlite3", "~> 1.3.11", platforms: [:mri, :rbx] -gem "resque", "~> 1.25.2" -gem "resque_spec", github: "airbrake/resque_spec" -gem "delayed_job_active_record", "~> 4.1.0" -gem "mime-types", "~> 3.1" - -gemspec path: "../" diff --git a/gemfiles/rails_4.1.gemfile b/gemfiles/rails_4.1.gemfile deleted file mode 100644 index 0cab4cbaa..000000000 --- a/gemfiles/rails_4.1.gemfile +++ /dev/null @@ -1,17 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rubocop", "= 0.55", require: false -gem "rails", "~> 4.1.16" -gem "warden", "~> 1.2.3" -gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.18", platforms: :jruby -gem "sqlite3", "~> 1.3.11", platforms: [:mri, :rbx] -gem "resque", "~> 1.25.2" -gem "resque_spec", github: "airbrake/resque_spec" -gem "delayed_job_active_record", "~> 4.1.0" -gem "mime-types", "~> 3.1" -gem "sprockets", "~> 3.7" -gem "rack", "~> 1" - -gemspec path: "../" diff --git a/gemfiles/rails_4.2.gemfile b/gemfiles/rails_4.2.gemfile deleted file mode 100644 index 5776db595..000000000 --- a/gemfiles/rails_4.2.gemfile +++ /dev/null @@ -1,17 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rubocop", "= 0.55", require: false -gem "rails", "~> 4.2.10" -gem "warden", "~> 1.2.3" -gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.18", platforms: :jruby -gem "sqlite3", "~> 1.3.11", platforms: [:mri, :rbx] -gem "resque", "~> 1.25.2" -gem "resque_spec", github: "airbrake/resque_spec" -gem "delayed_job_active_record", "~> 4.1.0" -gem "mime-types", "~> 3.1" -gem "sprockets", "~> 3.7" -gem "rack", "~> 1" - -gemspec path: "../" diff --git a/gemfiles/rails_5.0.gemfile b/gemfiles/rails_5.0.gemfile deleted file mode 100644 index 1869db61f..000000000 --- a/gemfiles/rails_5.0.gemfile +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rubocop", "= 0.55", require: false -gem "rails", "~> 5.0.7" -gem "warden", "~> 1.2.3" -gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.18", platforms: :jruby -gem "sqlite3", "~> 1.3.11", platforms: [:mri, :rbx] -gem "resque", "~> 1.25.2" -gem "resque_spec", github: "airbrake/resque_spec" -gem "delayed_job_active_record", "~> 4.1.0" -gem "mime-types", "~> 3.1" -gem "sprockets", "~> 3.7" - -gemspec path: "../" diff --git a/gemfiles/rails_5.1.gemfile b/gemfiles/rails_5.1.gemfile deleted file mode 100644 index 3dd5ffd32..000000000 --- a/gemfiles/rails_5.1.gemfile +++ /dev/null @@ -1,17 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "rubocop", "= 0.55", require: false -gem "rails", "~> 5.1.4" -gem "warden", "~> 1.2.6" -gem "activerecord-jdbcsqlite3-adapter", "~> 51.0", platforms: :jruby -gem "sqlite3", "~> 1.3.11", platforms: [:mri, :rbx] -gem "resque", "~> 1.26" -gem "resque_spec", github: "airbrake/resque_spec" -gem "delayed_job", github: "collectiveidea/delayed_job" -gem "delayed_job_active_record", "~> 4.1" -gem "mime-types", "~> 3.1" -gem "sprockets", "~> 3.7" - -gemspec path: "../" diff --git a/gemfiles/rails_5.2.gemfile b/gemfiles/rails_5.2.gemfile index 1da3cd83f..efc496180 100644 --- a/gemfiles/rails_5.2.gemfile +++ b/gemfiles/rails_5.2.gemfile @@ -2,12 +2,13 @@ source "https://rubygems.org" -gem "rubocop", "= 0.55", require: false +gem "rubocop", "~> 1.21", require: false +gem "sneakers", github: "jondot/sneakers", ref: "31d0cb25dc5bbcfb0749567e9e0f80e6353fb66b" gem "rails", "~> 5.2.0" gem "warden", "~> 1.2.6" gem "rack", "~> 2.0" gem "activerecord-jdbcsqlite3-adapter", "~> 52.0", platforms: :jruby -gem "sqlite3", "~> 1.3.11", platforms: [:mri, :rbx] +gem "sqlite3", "~> 1.4", platforms: [:mri, :rbx] gem "resque", "~> 1.26" gem "resque_spec", github: "airbrake/resque_spec" gem "delayed_job", github: "collectiveidea/delayed_job" diff --git a/gemfiles/rails_6.0.gemfile b/gemfiles/rails_6.0.gemfile index 7e241d218..1d23b2155 100644 --- a/gemfiles/rails_6.0.gemfile +++ b/gemfiles/rails_6.0.gemfile @@ -2,16 +2,16 @@ source "https://rubygems.org" -gem "rubocop", "= 0.55", require: false -gem "rails", "~> 6.0.2" +gem "rubocop", "~> 1.21", require: false +gem "sneakers", github: "jondot/sneakers", ref: "31d0cb25dc5bbcfb0749567e9e0f80e6353fb66b" +gem "rails", "~> 6.0.4.1" gem "warden", "~> 1.2.6" gem "rack", "~> 2.0" gem "activerecord-jdbcsqlite3-adapter", "~> 60.1", platforms: :jruby gem "sqlite3", "~> 1.4", platforms: [:mri, :rbx] gem "resque", "~> 1.26" gem "resque_spec", github: "airbrake/resque_spec" -gem "delayed_job", github: "collectiveidea/delayed_job" -gem "delayed_job_active_record", "~> 4.1" +gem "delayed", "~> 0.4" gem "mime-types", "~> 3.1" gemspec path: "../" diff --git a/gemfiles/rails_6.1.gemfile b/gemfiles/rails_6.1.gemfile new file mode 100644 index 000000000..a035e1e42 --- /dev/null +++ b/gemfiles/rails_6.1.gemfile @@ -0,0 +1,17 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rubocop", "~> 1.21", require: false +gem "sneakers", github: "jondot/sneakers", ref: "31d0cb25dc5bbcfb0749567e9e0f80e6353fb66b" +gem "rails", "~> 6.1.4.1" +gem "warden", "~> 1.2.6" +gem "rack", "~> 2.0" +gem "activerecord-jdbcsqlite3-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "61-stable", platforms: :jruby +gem "sqlite3", "~> 1.4", platforms: [:mri, :rbx] +gem "resque", "~> 1.26" +gem "resque_spec", github: "airbrake/resque_spec" +gem "delayed", "~> 0.4" +gem "mime-types", "~> 3.1" + +gemspec path: "../" diff --git a/gemfiles/rails_7.0.gemfile b/gemfiles/rails_7.0.gemfile new file mode 100644 index 000000000..82e44a3fc --- /dev/null +++ b/gemfiles/rails_7.0.gemfile @@ -0,0 +1,17 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rubocop", "~> 1.21", require: false +gem "sneakers", github: "jondot/sneakers", ref: "31d0cb25dc5bbcfb0749567e9e0f80e6353fb66b" +gem "rails", "~> 7.0.1" +gem "warden", "~> 1.2.6" +gem "rack", "~> 2.0" +gem "activerecord-jdbcsqlite3-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "61-stable", platforms: :jruby +gem "sqlite3", "~> 1.4", platforms: [:mri, :rbx] +gem "resque", "~> 1.26" +gem "resque_spec", github: "airbrake/resque_spec" +gem "delayed", "~> 0.4" +gem "mime-types", "~> 3.1" + +gemspec path: "../" diff --git a/gemfiles/sinatra.gemfile b/gemfiles/sinatra.gemfile index a87e20a15..df8d0686b 100644 --- a/gemfiles/sinatra.gemfile +++ b/gemfiles/sinatra.gemfile @@ -2,7 +2,8 @@ source "https://rubygems.org" -gem "rubocop", "= 0.55", require: false +gem "rubocop", "~> 1.21", require: false +gem "sneakers", github: "jondot/sneakers", ref: "31d0cb25dc5bbcfb0749567e9e0f80e6353fb66b" gem "sinatra", "~> 2" gem "warden", "~> 1.2.6" diff --git a/lib/airbrake/capistrano/capistrano2.rb b/lib/airbrake/capistrano/capistrano2.rb index 01cca28eb..d242eaafa 100644 --- a/lib/airbrake/capistrano/capistrano2.rb +++ b/lib/airbrake/capistrano/capistrano2.rb @@ -21,7 +21,7 @@ def self.load_into(config) RAILS_ENV=#{fetch(:rails_env, nil)} \ bundle exec rake airbrake:deploy \ - USERNAME=#{Shellwords.shellescape(ENV['USER'] || ENV['USERNAME'])} \ + USERNAME=#{Shellwords.shellescape(ENV.fetch('USER', nil) || ENV.fetch('USERNAME', nil))} \ ENVIRONMENT=#{fetch(:airbrake_env, fetch(:rails_env, 'production'))} \ REVISION=#{current_revision.strip} \ REPOSITORY=#{repository} \ diff --git a/lib/airbrake/delayed_job.rb b/lib/airbrake/delayed_job.rb index 31fa47cbc..76d07afe8 100644 --- a/lib/airbrake/delayed_job.rb +++ b/lib/airbrake/delayed_job.rb @@ -7,41 +7,39 @@ module Plugins class Airbrake < ::Delayed::Plugin callbacks do |lifecycle| lifecycle.around(:invoke_job) do |job, *args, &block| - begin - timing = ::Airbrake::Benchmark.measure do - # Forward the call to the next callback in the callback chain - block.call(job, *args) - end - rescue Exception => exception # rubocop:disable Lint/RescueException - params = job.as_json + timing = ::Airbrake::Benchmark.measure do + # Forward the call to the next callback in the callback chain + block.call(job, *args) + end + rescue Exception => exception # rubocop:disable Lint/RescueException + params = job.as_json - # If DelayedJob is used through ActiveJob, it contains extra info. - if job.payload_object.respond_to?(:job_data) - params[:active_job] = job.payload_object.job_data - job_class = job.payload_object.job_data['job_class'] - end + # If DelayedJob is used through ActiveJob, it contains extra info. + if job.payload_object.respond_to?(:job_data) + params[:active_job] = job.payload_object.job_data + job_class = job.payload_object.job_data['job_class'] + end - action = job_class || job.payload_object.class.name + action = job_class || job.payload_object.class.name - ::Airbrake.notify(exception, params) do |notice| - notice[:context][:component] = 'delayed_job' - notice[:context][:action] = action - end + ::Airbrake.notify(exception, params) do |notice| + notice[:context][:component] = 'delayed_job' + notice[:context][:action] = action + end - ::Airbrake.notify_queue( - queue: action, - error_count: 1, - timing: 0.01, - ) + ::Airbrake.notify_queue( + queue: action, + error_count: 1, + timing: 0.01, + ) - raise exception - else - ::Airbrake.notify_queue( - queue: job_class || job.payload_object.class.name, - error_count: 0, - timing: timing, - ) - end + raise exception + else + ::Airbrake.notify_queue( + queue: job_class || job.payload_object.class.name, + error_count: 0, + timing: timing, + ) end end end diff --git a/lib/airbrake/logger.rb b/lib/airbrake/logger.rb index 66157960e..73f4c6066 100644 --- a/lib/airbrake/logger.rb +++ b/lib/airbrake/logger.rb @@ -9,7 +9,7 @@ module Airbrake # # @example # # Create a logger like you normally do and decorate it. - # logger = Airbrake::AirbrakeLogger.new(Logger.new(STDOUT)) + # logger = Airbrake::AirbrakeLogger.new(Logger.new($stdout)) # # # Just use the logger like you normally do. # logger.fatal('oops') @@ -24,6 +24,8 @@ class AirbrakeLogger < SimpleDelegator attr_reader :airbrake_level def initialize(logger) + super + __setobj__(logger) @airbrake_notifier = Airbrake self.level = logger.level diff --git a/lib/airbrake/rack/context_filter.rb b/lib/airbrake/rack/context_filter.rb index 3d44a9041..bc60dd812 100644 --- a/lib/airbrake/rack/context_filter.rb +++ b/lib/airbrake/rack/context_filter.rb @@ -20,14 +20,9 @@ def call(notice) context = notice[:context] context[:url] = request.url - context[:userAddr] = - if request.respond_to?(:remote_ip) - request.remote_ip - else - request.ip - end context[:userAgent] = request.user_agent + add_ip(context, request) add_framework_version(context) controller = request.env['action_controller.instance'] @@ -60,6 +55,15 @@ def framework_version } end end + + def add_ip(context, request) + context[:userAddr] = + if request.respond_to?(:remote_ip) + request.remote_ip + else + request.ip + end + end end end end diff --git a/lib/airbrake/rack/instrumentable.rb b/lib/airbrake/rack/instrumentable.rb index f46581613..725b41381 100644 --- a/lib/airbrake/rack/instrumentable.rb +++ b/lib/airbrake/rack/instrumentable.rb @@ -57,6 +57,7 @@ def self.prepend_capture_timing(klass, method_name, label) klass.module_exec do mod = __airbrake_capture_timing_module__ mod.module_exec do + # rubocop:disable Style/DocumentDynamicEvalDefinition module_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method_name}(#{args}) Airbrake::Rack.capture_timing(#{label.to_s.inspect}) do @@ -65,6 +66,7 @@ def #{method_name}(#{args}) end #{visibility} :#{method_name} RUBY + # rubocop:enable Style/DocumentDynamicEvalDefinition end prepend mod end @@ -83,6 +85,7 @@ def self.chain_capture_timing(klass, method_name, label) klass.module_exec do alias_method wrapped_method_name, method_name remove_method method_name if needs_removal + # rubocop:disable Style/DocumentDynamicEvalDefinition module_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method_name}(#{args}) Airbrake::Rack.capture_timing(#{label.to_s.inspect}) do @@ -91,6 +94,7 @@ def #{method_name}(#{args}) end #{visibility} :#{method_name} RUBY + # rubocop:enable Style/DocumentDynamicEvalDefinition end end diff --git a/lib/airbrake/rack/route_filter.rb b/lib/airbrake/rack/route_filter.rb index e531fae50..dc2b1cfd4 100644 --- a/lib/airbrake/rack/route_filter.rb +++ b/lib/airbrake/rack/route_filter.rb @@ -35,7 +35,7 @@ def rails_route(request) def sinatra_route(request) return unless (route = request.env['sinatra.route']) - route.split(' ').drop(1).join(' ') + route.split.drop(1).join(' ') end def action_dispatch_request?(request) diff --git a/lib/airbrake/rails.rb b/lib/airbrake/rails.rb index cc5bd26a6..24d3200e9 100644 --- a/lib/airbrake/rails.rb +++ b/lib/airbrake/rails.rb @@ -11,7 +11,7 @@ def self.logger level = (::Rails.logger ? ::Rails.logger.level : Logger::ERROR) if ENV['RAILS_LOG_TO_STDOUT'].present? - Logger.new(STDOUT, level: level) + Logger.new($stdout, level: level) else Logger.new(::Rails.root.join('log', 'airbrake.log'), level: level) end diff --git a/lib/airbrake/rails/app.rb b/lib/airbrake/rails/app.rb index 89cfb1e38..2bbec8639 100644 --- a/lib/airbrake/rails/app.rb +++ b/lib/airbrake/rails/app.rb @@ -40,12 +40,12 @@ def self.recognize_route(request) # Skip "catch-all" routes such as: # get '*path => 'pages#about' # - # @todo The `glob?` method was added in Rails v4.2.0.beta1. We - # should remove the `respond_to?` check once we drop old Rails - # versions support. + # Ideally, we should be using `route.glob?` but in Rails 7+ this + # call would fail with a `NoMethodError`. This is because in + # Rails 7+ the AST for the route is not kept in memory anymore. # - # https://github.com/rails/rails/commit/5460591f0226a9d248b7b4f89186bd5553e7768f - next if route.respond_to?(:glob?) && route.glob? + # See: https://github.com/rails/rails/pull/43006#discussion_r783895766 + next if route.path.spec.any?(ActionDispatch::Journey::Nodes::Star) path = if engine == ::Rails.application diff --git a/lib/airbrake/rails/event.rb b/lib/airbrake/rails/event.rb index 793106b76..47063c20b 100644 --- a/lib/airbrake/rails/event.rb +++ b/lib/airbrake/rails/event.rb @@ -10,10 +10,14 @@ class Event # @see https://github.com/rails/rails/issues/8987 HTML_RESPONSE_WILDCARD = "*/*" + # @return [Integer] + MILLISECOND = 1000 + include Airbrake::Loggable def initialize(*args) @event = ActiveSupport::Notifications::Event.new(*args) + @rails_7_or_greater = ::Rails::VERSION::MAJOR >= 7 end def method @@ -42,7 +46,15 @@ def view_runtime end def time - @event.time + # On Rails 7+ `ActiveSupport::Notifications::Event#time` returns an + # instance of Float. It represents monotonic time in milliseconds. + # Airbrake Ruby expects that the provided time is in seconds. Hence, + # we need to convert it from milliseconds to seconds. In the + # versions below Rails 7, time is an instance of Time. + # + # Relevant commit: + # https://github.com/rails/rails/commit/81d0dc90becfe0b8e7f7f26beb66c25d84b8ec7f + @rails_7_or_greater ? @event.time / MILLISECOND : @event.time end def groups diff --git a/lib/airbrake/rails/railtie.rb b/lib/airbrake/rails/railtie.rb index d3feb7a0c..586f37ba3 100644 --- a/lib/airbrake/rails/railtie.rb +++ b/lib/airbrake/rails/railtie.rb @@ -5,37 +5,10 @@ module Rails # This railtie works for any Rails application that supports railties (Rails # 3.2+ apps). It makes Airbrake Ruby work with Rails and report errors # occurring in the application automatically. - # - # rubocop:disable Metrics/BlockLength class Railtie < ::Rails::Railtie initializer('airbrake.middleware') do |app| - # Since Rails 3.2 the ActionDispatch::DebugExceptions middleware is - # responsible for logging exceptions and showing a debugging page in - # case the request is local. We want to insert our middleware after - # DebugExceptions, so we don't notify Airbrake about local requests. - - if ::Rails.version.to_i >= 5 - # Avoid the warning about deprecated strings. - # Insert after DebugExceptions, since ConnectionManagement doesn't - # exist in Rails 5 anymore. - app.config.middleware.insert_after( - ActionDispatch::DebugExceptions, - Airbrake::Rack::Middleware, - ) - elsif defined?(::ActiveRecord::ConnectionAdapters::ConnectionManagement) - # Insert after ConnectionManagement to avoid DB connection leakage: - # https://github.com/airbrake/airbrake/pull/568 - app.config.middleware.insert_after( - ::ActiveRecord::ConnectionAdapters::ConnectionManagement, - 'Airbrake::Rack::Middleware', - ) - else - # Insert after DebugExceptions for apps without ActiveRecord. - app.config.middleware.insert_after( - ActionDispatch::DebugExceptions, - 'Airbrake::Rack::Middleware', - ) - end + require 'airbrake/rails/railties/middleware_tie' + Railties::MiddlewareTie.new(app).call end rake_tasks do @@ -47,82 +20,13 @@ class Railtie < ::Rails::Railtie end initializer('airbrake.action_controller') do - ActiveSupport.on_load(:action_controller, run_once: true) do - # Patches ActionController with methods that allow us to retrieve - # interesting request data. Appends that information to notices. - require 'airbrake/rails/action_controller' - include Airbrake::Rails::ActionController - - # Cache route information for the duration of the request. - require 'airbrake/rails/action_controller_route_subscriber' - ActiveSupport::Notifications.subscribe( - 'start_processing.action_controller', - Airbrake::Rails::ActionControllerRouteSubscriber.new, - ) - - # Send route stats. - require 'airbrake/rails/action_controller_notify_subscriber' - ActiveSupport::Notifications.subscribe( - 'process_action.action_controller', - Airbrake::Rails::ActionControllerNotifySubscriber.new, - ) - - # Send performance breakdown: where a request spends its time. - require 'airbrake/rails/action_controller_performance_breakdown_subscriber' - ActiveSupport::Notifications.subscribe( - 'process_action.action_controller', - Airbrake::Rails::ActionControllerPerformanceBreakdownSubscriber.new, - ) - - require 'airbrake/rails/net_http' if defined?(Net) && defined?(Net::HTTP) - require 'airbrake/rails/curb' if defined?(Curl) && defined?(Curl::CURB_VERSION) - require 'airbrake/rails/http' if defined?(HTTP) && defined?(HTTP::Client) - require 'airbrake/rails/http_client' if defined?(HTTPClient) - require 'airbrake/rails/typhoeus' if defined?(Typhoeus) - - if defined?(Excon) - require 'airbrake/rails/excon_subscriber' - ActiveSupport::Notifications.subscribe(/excon/, Airbrake::Rails::Excon.new) - ::Excon.defaults[:instrumentor] = ActiveSupport::Notifications - end - end + require 'airbrake/rails/railties/action_controller_tie' + Railties::ActionControllerTie.new.call end initializer('airbrake.active_record') do - ActiveSupport.on_load(:active_record, run_once: true) do - # Reports exceptions occurring in some bugged ActiveRecord callbacks. - # Applicable only to the versions of Rails lower than 4.2. - if defined?(::Rails) && - Gem::Version.new(::Rails.version) <= Gem::Version.new('4.2') - require 'airbrake/rails/active_record' - include Airbrake::Rails::ActiveRecord - end - - if defined?(ActiveRecord) - # Send SQL queries. - require 'airbrake/rails/active_record_subscriber' - ActiveSupport::Notifications.subscribe( - 'sql.active_record', Airbrake::Rails::ActiveRecordSubscriber.new - ) - - # Filter out parameters from SQL body. - if ::ActiveRecord::Base.respond_to?(:connection_db_config) - # Rails 6.1+ deprecates "connection_config" in favor of - # "connection_db_config", so we need an updated call. - Airbrake.add_performance_filter( - Airbrake::Filters::SqlFilter.new( - ::ActiveRecord::Base.connection_db_config.configuration_hash[:adapter], - ), - ) - else - Airbrake.add_performance_filter( - Airbrake::Filters::SqlFilter.new( - ::ActiveRecord::Base.connection_config[:adapter], - ), - ) - end - end - end + require 'airbrake/rails/railties/active_record_tie' + Railties::ActiveRecordTie.new.call end initializer('airbrake.active_job') do @@ -146,6 +50,5 @@ class Railtie < ::Rails::Railtie end end end - # rubocop:enable Metrics/BlockLength end end diff --git a/lib/airbrake/rails/railties/action_controller_tie.rb b/lib/airbrake/rails/railties/action_controller_tie.rb new file mode 100644 index 000000000..bc050fde9 --- /dev/null +++ b/lib/airbrake/rails/railties/action_controller_tie.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'airbrake/rails/action_controller' +require 'airbrake/rails/action_controller_route_subscriber' +require 'airbrake/rails/action_controller_notify_subscriber' +require 'airbrake/rails/action_controller_performance_breakdown_subscriber' + +module Airbrake + module Rails + module Railties + # Ties Airbrake APM (routes) and HTTP clients with Rails. + # + # @api private + # @since v13.0.1 + class ActionControllerTie + def initialize + @route_subscriber = Airbrake::Rails::ActionControllerRouteSubscriber.new + @notify_subscriber = Airbrake::Rails::ActionControllerNotifySubscriber.new + @performance_breakdown_subscriber = + Airbrake::Rails::ActionControllerPerformanceBreakdownSubscriber.new + end + + def call + ActiveSupport.on_load(:action_controller, run_once: true, yield: self) do + # Patches ActionController with methods that allow us to retrieve + # interesting request data. Appends that information to notices. + ::ActionController::Base.include(Airbrake::Rails::ActionController) + + tie_routes_apm + tie_http_integrations + end + end + + private + + def tie_routes_apm + [ + # Cache route information for the duration of the request. + ['start_processing.action_controller', @route_subscriber], + + # Send route stats. + ['process_action.action_controller', @notify_subscriber], + + # Send performance breakdown: where a request spends its time. + ['process_action.action_controller', @performance_breakdown_subscriber], + ].each do |(event, callback)| + ActiveSupport::Notifications.subscribe(event, callback) + end + end + + def tie_http_integrations + tie_net_http + tie_curl + tie_http + tie_http_client + tie_typhoeus + tie_excon + end + + def tie_net_http + require 'airbrake/rails/net_http' if defined?(Net) && defined?(Net::HTTP) + end + + def tie_curl + require 'airbrake/rails/curb' if defined?(Curl) && defined?(Curl::CURB_VERSION) + end + + def tie_http + require 'airbrake/rails/http' if defined?(HTTP) && defined?(HTTP::Client) + end + + def tie_http_client + require 'airbrake/rails/http_client' if defined?(HTTPClient) + end + + def tie_typhoeus + require 'airbrake/rails/typhoeus' if defined?(Typhoeus) + end + + def tie_excon + return unless defined?(Excon) + + require 'airbrake/rails/excon_subscriber' + ActiveSupport::Notifications.subscribe(/excon/, Airbrake::Rails::Excon.new) + ::Excon.defaults[:instrumentor] = ActiveSupport::Notifications + end + end + end + end +end diff --git a/lib/airbrake/rails/railties/active_record_tie.rb b/lib/airbrake/rails/railties/active_record_tie.rb new file mode 100644 index 000000000..78bd20369 --- /dev/null +++ b/lib/airbrake/rails/railties/active_record_tie.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'airbrake/rails/active_record' +require 'airbrake/rails/active_record_subscriber' + +module Airbrake + module Rails + module Railties + # Ties Airbrake APM (queries) with Rails. + # + # @api private + # @since v13.0.1 + class ActiveRecordTie + def initialize + @active_record_subscriber = Airbrake::Rails::ActiveRecordSubscriber.new + end + + def call + ActiveSupport.on_load(:active_record, run_once: true, yield: self) do + tie_activerecord_callback_fix + tie_activerecord_apm + end + end + + private + + def tie_activerecord_callback_fix + # Reports exceptions occurring in some bugged ActiveRecord callbacks. + # Applicable only to the versions of Rails lower than 4.2. + return unless defined?(::Rails) + return if Gem::Version.new(::Rails.version) > Gem::Version.new('4.2') + + ActiveRecord::Base.include(Airbrake::Rails::ActiveRecord) + end + + def tie_activerecord_apm + # Some Rails apps don't use ActiveRecord. + return unless defined?(::ActiveRecord) + + # However, some dependencies might still require it, so we need an + # extra check. Apps that don't need ActiveRecord will likely have no + # AR configurations defined. We will skip APM integration in that + # case. See: https://github.com/airbrake/airbrake/issues/1222 + configurations = ::ActiveRecord::Base.configurations + return unless configurations.any? + + # Send SQL queries. + ActiveSupport::Notifications.subscribe( + 'sql.active_record', + @active_record_subscriber, + ) + + # Filter out parameters from SQL body. + sql_filter = Airbrake::Filters::SqlFilter.new( + detect_activerecord_adapter(configurations), + ) + Airbrake.add_performance_filter(sql_filter) + end + + # Rails 6+ introduces the `configs_for` API instead of the deprecated + # `#[]`, so we need an updated call. + def detect_activerecord_adapter(configurations) + unless configurations.respond_to?(:configs_for) + return configurations[::Rails.env]['adapter'] + end + + cfg = configurations.configs_for(env_name: ::Rails.env).first + # Rails 7+ API : Rails 6 API. + cfg.respond_to?(:adapter) ? cfg.adapter : cfg.config['adapter'] + end + end + end + end +end diff --git a/lib/airbrake/rails/railties/middleware_tie.rb b/lib/airbrake/rails/railties/middleware_tie.rb new file mode 100644 index 000000000..3c5bee8d1 --- /dev/null +++ b/lib/airbrake/rails/railties/middleware_tie.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Airbrake + module Rails + module Railties + # Ties Airbrake Rails Middleware with Rails (error sending). + # + # Since Rails 3.2 the ActionDispatch::DebugExceptions middleware is + # responsible for logging exceptions and showing a debugging page in case + # the request is local. We want to insert our middleware after + # DebugExceptions, so we don't notify Airbrake about local requests. + # + # @api private + # @since v13.0.1 + class MiddlewareTie + def initialize(app) + @app = app + @middleware = app.config.middleware + end + + def call + return tie_rails_5_or_above if ::Rails.version.to_i >= 5 + + if defined?(::ActiveRecord::ConnectionAdapters::ConnectionManagement) + return tie_rails_4_or_below_with_active_record + end + + tie_rails_4_or_below_with_active_record + end + + private + + # Avoid the warning about deprecated strings. + # Insert after DebugExceptions, since ConnectionManagement doesn't + # exist in Rails 5 anymore. + def tie_rails_5_or_above + @middleware.insert_after( + ActionDispatch::DebugExceptions, + Airbrake::Rack::Middleware, + ) + end + + # Insert after ConnectionManagement to avoid DB connection leakage: + # https://github.com/airbrake/airbrake/pull/568 + def tie_rails_4_or_below_with_active_record + @middleware.insert_after( + ::ActiveRecord::ConnectionAdapters::ConnectionManagement, + 'Airbrake::Rack::Middleware', + ) + end + + # Insert after DebugExceptions for apps without ActiveRecord. + def tie_rails_4_or_below_without_active_record + @middleware.insert_after( + ActionDispatch::DebugExceptions, + 'Airbrake::Rack::Middleware', + ) + end + end + end + end +end diff --git a/lib/airbrake/rake/tasks.rb b/lib/airbrake/rake/tasks.rb index 0de505416..b4eef44e8 100644 --- a/lib/airbrake/rake/tasks.rb +++ b/lib/airbrake/rake/tasks.rb @@ -53,11 +53,11 @@ raise Airbrake::Error, 'airbrake-ruby is not configured' unless Airbrake.configured? deploy_params = { - environment: ENV['ENVIRONMENT'], - username: ENV['USERNAME'], - revision: ENV['REVISION'], - repository: ENV['REPOSITORY'], - version: ENV['VERSION'], + environment: ENV.fetch('ENVIRONMENT', nil), + username: ENV.fetch('USERNAME', nil), + revision: ENV.fetch('REVISION', nil), + repository: ENV.fetch('REPOSITORY', nil), + version: ENV.fetch('VERSION', nil), } promise = Airbrake.notify_deploy(deploy_params) promise.then do @@ -68,7 +68,7 @@ desc 'Install a Heroku deploy hook to notify Airbrake of deploys' task :install_heroku_deploy_hook do - app = ENV['HEROKU_APP'] + app = ENV.fetch('HEROKU_APP', nil) config = Bundler.with_clean_env do `heroku config --shell#{" --app #{app}" if app}` @@ -89,7 +89,7 @@ " environment will be used." end - unless (repo = ENV['REPOSITORY_URL']) + unless (repo = ENV.fetch('REPOSITORY_URL', nil)) repo = `git remote get-url origin 2>/dev/null`.chomp if repo.empty? puts "Airbrake couldn't identify your app's repository." diff --git a/lib/airbrake/shoryuken.rb b/lib/airbrake/shoryuken.rb index 03d3c02d5..e86ae42b6 100644 --- a/lib/airbrake/shoryuken.rb +++ b/lib/airbrake/shoryuken.rb @@ -5,10 +5,8 @@ module Shoryuken # Provides integration with Shoryuken. class ErrorHandler # rubocop:disable Lint/RescueException - def call(worker, queue, _sqs_msg, body) - timing = Airbrake::Benchmark.measure do - yield - end + def call(worker, queue, _sqs_msg, body, &block) + timing = Airbrake::Benchmark.measure(&block) rescue Exception => exception notify_airbrake(exception, worker, queue, body) Airbrake.notify_queue( diff --git a/lib/airbrake/sidekiq.rb b/lib/airbrake/sidekiq.rb index 05c1c96fb..0f088a296 100644 --- a/lib/airbrake/sidekiq.rb +++ b/lib/airbrake/sidekiq.rb @@ -6,10 +6,8 @@ module Airbrake module Sidekiq # Provides integration with Sidekiq v2+. class ErrorHandler - def call(_worker, context, _queue) - timing = Airbrake::Benchmark.measure do - yield - end + def call(_worker, context, _queue, &block) + timing = Airbrake::Benchmark.measure(&block) rescue Exception => exception # rubocop:disable Lint/RescueException notify_airbrake(exception, context) Airbrake.notify_queue( @@ -38,7 +36,7 @@ def notify_airbrake(exception, context) # @return [String] job's name. When ActiveJob is present, retrieve # job_class. When used directly, use worker's name def action(context) - klass = context['class'] || context[:job] && context[:job]['class'] + klass = context['class'] || (context[:job] && context[:job]['class']) return klass unless context[:job] && context[:job]['args'].first.is_a?(Hash) return klass unless (job_class = context[:job]['args'].first['job_class']) diff --git a/lib/airbrake/sneakers.rb b/lib/airbrake/sneakers.rb index 035b7a72d..7d4e22bad 100644 --- a/lib/airbrake/sneakers.rb +++ b/lib/airbrake/sneakers.rb @@ -48,24 +48,22 @@ module Worker define_method( ::Sneakers::Worker.method_defined?(:process_work) ? :process_work : :do_work, ) do |delivery_info, metadata, msg, handler| - begin - timing = Airbrake::Benchmark.measure do - super(delivery_info, metadata, msg, handler) - end - rescue Exception => exception # rubocop:disable Lint/RescueException - Airbrake.notify_queue( - queue: self.class.to_s, - error_count: 1, - timing: 0.01, - ) - raise exception - else - Airbrake.notify_queue( - queue: self.class.to_s, - error_count: 0, - timing: timing, - ) + timing = Airbrake::Benchmark.measure do + super(delivery_info, metadata, msg, handler) end + rescue Exception => exception # rubocop:disable Lint/RescueException + Airbrake.notify_queue( + queue: self.class.to_s, + error_count: 1, + timing: 0.01, + ) + raise exception + else + Airbrake.notify_queue( + queue: self.class.to_s, + error_count: 0, + timing: timing, + ) end end end diff --git a/lib/airbrake/version.rb b/lib/airbrake/version.rb index 05796c8b3..df319d852 100644 --- a/lib/airbrake/version.rb +++ b/lib/airbrake/version.rb @@ -3,5 +3,5 @@ # We use Semantic Versioning v2.0.0 # More information: http://semver.org/ module Airbrake - AIRBRAKE_VERSION = '11.0.3' + AIRBRAKE_VERSION = '13.0.2' end diff --git a/lib/generators/airbrake_generator.rb b/lib/generators/airbrake_generator.rb index afb3f5b3c..1b1e252f1 100644 --- a/lib/generators/airbrake_generator.rb +++ b/lib/generators/airbrake_generator.rb @@ -3,22 +3,19 @@ # Creates the Airbrake initializer file for Rails apps. # # @example Invokation from terminal -# rails generate airbrake PROJECT_KEY PROJECT_ID [NAME] +# rails generate airbrake [NAME] # class AirbrakeGenerator < Rails::Generators::Base # Adds current directory to source paths, so we can find the template file. source_root File.expand_path(__dir__) - argument :project_id, required: false - argument :project_key, required: false - # Makes the NAME option optional, which allows to subclass from Base, so we # can pass arguments to the ERB template. # - # @see http://asciicasts.com/episodes/218-making-generators-in-rails-3 + # @see https://asciicasts.com/episodes/218-making-generators-in-rails-3.html argument :name, type: :string, default: 'application' - desc 'Configures the Airbrake notifier with your project id and project key' + desc 'Configures the Airbrake notifier' def generate_layout template 'airbrake_initializer.rb.erb', 'config/initializers/airbrake.rb' end diff --git a/lib/generators/airbrake_initializer.rb.erb b/lib/generators/airbrake_initializer.rb.erb index 3f43d32f5..23c83eafc 100644 --- a/lib/generators/airbrake_initializer.rb.erb +++ b/lib/generators/airbrake_initializer.rb.erb @@ -1,80 +1,80 @@ # frozen_string_literal: true -# Airbrake is an online tool that provides robust exception tracking in your Rails -# applications. In doing so, it allows you to easily review errors, tie an error -# to an individual piece of code, and trace the cause back to recent -# changes. Airbrake enables for easy categorization, searching, and prioritization -# of exceptions so that when errors occur, your team can quickly determine the -# root cause. +# Airbrake is an online tool that provides robust exception tracking in your +# Rails applications. In doing so, it allows you to easily review errors, tie an +# error to an individual piece of code, and trace the cause back to recent +# changes. Airbrake enables for easy categorization, searching, and +# prioritization of exceptions so that when errors occur, your team can quickly +# determine the root cause. # # Configuration details: # https://github.com/airbrake/airbrake-ruby#configuration -Airbrake.configure do |c| - # You must set both project_id & project_key. To find your project_id and - # project_key navigate to your project's General Settings and copy the values - # from the right sidebar. - # https://github.com/airbrake/airbrake-ruby#project_id--project_key -<% if project_id -%> - c.project_id = <%= project_id %> -<% else -%> - c.project_id = ENV['AIRBRAKE_PROJECT_ID'] -<% end -%> -<% if project_key -%> - c.project_key = '<%= project_key %>' -<% else -%> - c.project_key = ENV['AIRBRAKE_API_KEY'] -<% end -%> +if (project_id = ENV['AIRBRAKE_PROJECT_ID']) && + project_key = (ENV['AIRBRAKE_PROJECT_KEY'] || ENV['AIRBRAKE_API_KEY']) + Airbrake.configure do |c| + # You must set both project_id & project_key. To find your project_id and + # project_key navigate to your project's General Settings and copy the + # values from the right sidebar. + # https://github.com/airbrake/airbrake-ruby#project_id--project_key + c.project_id = project_id + c.project_key = project_key - # Configures the root directory of your project. Expects a String or a - # Pathname, which represents the path to your project. Providing this option - # helps us to filter out repetitive data from backtrace frames and link to - # GitHub files from our dashboard. - # https://github.com/airbrake/airbrake-ruby#root_directory - c.root_directory = Rails.root + # Configures the root directory of your project. Expects a String or a + # Pathname, which represents the path to your project. Providing this option + # helps us to filter out repetitive data from backtrace frames and link to + # GitHub files from our dashboard. + # https://github.com/airbrake/airbrake-ruby#root_directory + c.root_directory = Rails.root - # By default, Airbrake Ruby outputs to STDOUT. In Rails apps it makes sense to - # use the Rails' logger. - # https://github.com/airbrake/airbrake-ruby#logger - c.logger = Airbrake::Rails.logger + # By default, Airbrake Ruby outputs to STDOUT. In Rails apps it makes sense + # to use the Rails' logger. + # https://github.com/airbrake/airbrake-ruby#logger + c.logger = Airbrake::Rails.logger - # Configures the environment the application is running in. Helps the Airbrake - # dashboard to distinguish between exceptions occurring in different - # environments. - # NOTE: This option must be set in order to make the 'ignore_environments' - # option work. - # https://github.com/airbrake/airbrake-ruby#environment - c.environment = Rails.env + # Configures the environment the application is running in. Helps the + # Airbrake dashboard to distinguish between exceptions occurring in + # different environments. + # NOTE: This option must be set in order to make the 'ignore_environments' + # option work. + # https://github.com/airbrake/airbrake-ruby#environment + c.environment = Rails.env - # Setting this option allows Airbrake to filter exceptions occurring in - # unwanted environments such as :test. - # NOTE: This option *does not* work if you don't set the 'environment' option. - # https://github.com/airbrake/airbrake-ruby#ignore_environments - c.ignore_environments = %w[test] + # Setting this option allows Airbrake to filter exceptions occurring in + # unwanted environments such as :test. NOTE: This option *does not* work if + # you don't set the 'environment' option. + # https://github.com/airbrake/airbrake-ruby#ignore_environments + c.ignore_environments = %w[test] - # A list of parameters that should be filtered out of what is sent to - # Airbrake. By default, all "password" attributes will have their contents - # replaced. - # https://github.com/airbrake/airbrake-ruby#blocklist_keys - c.blocklist_keys = [/password/i, /authorization/i] + # A list of parameters that should be filtered out of what is sent to + # Airbrake. By default, all "password" attributes will have their contents + # replaced. + # https://github.com/airbrake/airbrake-ruby#blocklist_keys + c.blocklist_keys = [/password/i, /authorization/i] - # Alternatively, you can integrate with Rails' filter_parameters. - # Read more: https://goo.gl/gqQ1xS - # c.blocklist_keys = Rails.application.config.filter_parameters -end + # Alternatively, you can integrate with Rails' filter_parameters. + # Read more: https://goo.gl/gqQ1xS + # c.blocklist_keys = Rails.application.config.filter_parameters + end -# A filter that collects request body information. Enable it if you are sure you -# don't send sensitive information to Airbrake in your body (such as passwords). -# https://github.com/airbrake/airbrake#requestbodyfilter -# Airbrake.add_filter(Airbrake::Rack::RequestBodyFilter.new) + # A filter that collects request body information. Enable it if you are sure you + # don't send sensitive information to Airbrake in your body (such as passwords). + # https://github.com/airbrake/airbrake#requestbodyfilter + # Airbrake.add_filter(Airbrake::Rack::RequestBodyFilter.new) -# Attaches thread & fiber local variables along with general thread information. -# Airbrake.add_filter(Airbrake::Filters::ThreadFilter.new) + # Attaches thread & fiber local variables along with general thread information. + # Airbrake.add_filter(Airbrake::Filters::ThreadFilter.new) -# Attaches loaded dependencies to the notice object -# (under context/versions/dependencies). -# Airbrake.add_filter(Airbrake::Filters::DependencyFilter.new) + # Attaches loaded dependencies to the notice object + # (under context/versions/dependencies). + # Airbrake.add_filter(Airbrake::Filters::DependencyFilter.new) -# If you want to convert your log messages to Airbrake errors, we offer an -# integration with the Logger class from stdlib. -# https://github.com/airbrake/airbrake#logger -# Rails.logger = Airbrake::AirbrakeLogger.new(Rails.logger) + # If you want to convert your log messages to Airbrake errors, we offer an + # integration with the Logger class from stdlib. + # https://github.com/airbrake/airbrake#logger + # Rails.logger = Airbrake::AirbrakeLogger.new(Rails.logger) +else + Rails.logger.warn( + "#{__FILE__}: Airbrake project id or project key is not set. " \ + "Skipping Airbrake configuration" + ) +end diff --git a/spec/apps/rack/dummy_app.rb b/spec/apps/rack/rack_app.rb similarity index 91% rename from spec/apps/rack/dummy_app.rb rename to spec/apps/rack/rack_app.rb index a3e6f2605..4ee0f4129 100644 --- a/spec/apps/rack/dummy_app.rb +++ b/spec/apps/rack/rack_app.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -DummyApp = Rack::Builder.new do +RackApp = Rack::Builder.new do use Rack::ShowExceptions use Airbrake::Rack::Middleware use Warden::Manager diff --git a/spec/apps/rails/dummy_app.rb b/spec/apps/rails/dummy_app.rb index 538408245..36e396e2d 100644 --- a/spec/apps/rails/dummy_app.rb +++ b/spec/apps/rails/dummy_app.rb @@ -10,7 +10,7 @@ class DummyApp < Rails::Application config.secret_key_base = '62773890cad9d9d584b57320f8612f8f7378a90aadcabc6ee' # Configure a logger, without it the tests can't run. - vsn = Rails.version.split('').values_at(0, 2).join('') + vsn = Rails.version.chars.values_at(0, 2).join log_path = File.join(File.dirname(__FILE__), 'logs', "#{vsn}.log") config.logger = Logger.new(log_path) Rails.logger = config.logger @@ -73,21 +73,18 @@ def raise_error_after_rollback end end -# ActiveJob. -if Gem::Version.new(Rails.version) >= Gem::Version.new('4.2') - class BingoJob < ActiveJob::Base - queue_as :bingo +class BingoJob < ActiveJob::Base + queue_as :bingo - class BingoWrapper - def initialize(bingo) - @bingo = bingo - end + class BingoWrapper + def initialize(bingo) + @bingo = bingo end + end - def perform(*_args) - @wrapper = BingoWrapper.new(self) - raise AirbrakeTestError, 'active_job error' - end + def perform(*_args) + @wrapper = BingoWrapper.new(self) + raise AirbrakeTestError, 'active_job error' end end @@ -147,47 +144,47 @@ def breakdown end def breakdown_view_only - render 'dummy/breakdown.html.erb' + render 'dummy/breakdown', format: [:erb] end def breakdown_http Net::HTTP.get('example.com', '/') - render 'dummy/breakdown_http.html.erb' + render 'dummy/breakdown_http', format: [:erb] end def breakdown_curl_http Curl.get('example.com') - render 'dummy/breakdown_curl_http.html.erb' + render 'dummy/breakdown_curl_http', format: [:erb] end def breakdown_curl_http_easy Curl::Easy.perform('example.com') - render 'dummy/breakdown_curl_http_easy.html.erb' + render 'dummy/breakdown_curl_http_easy', format: [:erb] end def breakdown_curl_http_multi Curl::Multi.get(['example.com']) - render 'dummy/breakdown_curl_http_multi.html.erb' + render 'dummy/breakdown_curl_http_multi', format: [:erb] end def breakdown_excon Excon.get('http://example.com') - render 'dummy/breakdown_excon.html.erb' + render 'dummy/breakdown_excon', format: [:erb] end def breakdown_http_rb HTTP.get('http://example.com') - render 'dummy/breakdown_http_rb.html.erb' + render 'dummy/breakdown_http_rb', format: [:erb] end def breakdown_http_client HTTPClient.new.get('http://example.com') - render 'dummy/breakdown_http_client.html.erb' + render 'dummy/breakdown_http_client', format: [:erb] end def breakdown_typhoeus Typhoeus.get('example.com') - render 'dummy/breakdown_typhoeus.html.erb' + render 'dummy/breakdown_typhoeus', format: [:erb] end def notify_airbrake_helper @@ -234,8 +231,8 @@ def delayed_job # Modified version of: https://goo.gl/q8uCJq migration_template = File.open( File.join( - $LOAD_PATH.grep(/delayed_job/)[0], - 'generators/delayed_job/templates/migration.rb', + $LOAD_PATH.grep(/delayed/)[0], + 'generators/delayed/templates/migration.rb', ), ) diff --git a/spec/integration/rack/rack_spec.rb b/spec/integration/rack/rack_spec.rb index 83a1107cf..52a57c6bd 100644 --- a/spec/integration/rack/rack_spec.rb +++ b/spec/integration/rack/rack_spec.rb @@ -3,7 +3,7 @@ require 'integration/shared_examples/rack_examples' RSpec.describe "Rack integration specs" do - let(:app) { DummyApp } + let(:app) { RackApp } include_examples 'rack examples' diff --git a/spec/integration/rails/rails_spec.rb b/spec/integration/rails/rails_spec.rb index 762081dce..efbe6b0f1 100644 --- a/spec/integration/rails/rails_spec.rb +++ b/spec/integration/rails/rails_spec.rb @@ -64,11 +64,8 @@ def force_http_libs_prepend shared_examples "context payload content" do |route| let(:user) do - OpenStruct.new( - id: 1, - email: 'qa@example.com', - username: 'qa-dept', - ) + stub_const('User', Struct.new(:id, :email, :username)) + User.new(1, 'qa@example.com', 'qa-dept') end before do @@ -108,7 +105,7 @@ def force_http_libs_prepend end it "includes params" do - action = route[1..-1] + action = route[1..] body = /"context":{.*"params":{.*"controller":"dummy","action":"#{action}".*}/ expect(a_request(:post, endpoint).with(body: body)).to have_been_made end @@ -223,11 +220,8 @@ def force_http_libs_prepend describe "user extraction" do context "when Warden is not available but 'current_user' is defined" do let(:user) do - OpenStruct.new( - id: 1, - email: 'qa@example.com', - username: 'qa-dept', - ) + stub_const('User', Struct.new(:id, :email, :username)) + User.new(1, 'qa@example.com', 'qa-dept') end before do @@ -252,6 +246,7 @@ def force_http_libs_prepend describe "request performance hook" do before do allow(Airbrake).to receive(:notify) + allow(Airbrake).to receive(:notify_request) allow(config).to receive(:performance_stats).and_return(true) end @@ -267,7 +262,7 @@ def force_http_libs_prepend it "defaults to 500 when status code for exception returns 0" do allow(ActionDispatch::ExceptionWrapper) - .to receive(:status_code_for_exception).and_return(0) + .to receive(:status_code).and_return(0) expect(Airbrake).to receive(:notify_request).with( hash_including( @@ -278,6 +273,36 @@ def force_http_libs_prepend ) head '/crash' end + + context "when Rails version is below 7" do + before do + if Rails::VERSION::MAJOR >= 7 + skip("This test requires Rails 6 or lower, you're on Rails #{Rails.version}") + end + end + + it "reports time as an instance of Time" do + head '/crash' + expect(Airbrake).to have_received(:notify_request).with( + hash_including(time: an_instance_of(Time)), + ) + end + end + + context "when Rails version is 7+" do + before do + if Rails::VERSION::MAJOR < 7 + skip("This test requires Rails 7+, you're on Rails #{Rails.version}") + end + end + + it "reports time as an instance of Float" do + head '/crash' + expect(Airbrake).to have_received(:notify_request).with( + hash_including(time: an_instance_of(Float)), + ) + end + end end describe "query performance hook" do @@ -429,7 +454,7 @@ def force_http_libs_prepend it "includes the http breakdown" do expect(Airbrake).to receive(:notify_performance_breakdown).with( - hash_including(groups: { http: be > 0 }), + hash_including(groups: { view: be > 0, http: be > 0 }), an_instance_of(Hash), ) get '/breakdown_excon' @@ -444,7 +469,7 @@ def force_http_libs_prepend it "includes the http breakdown" do expect(Airbrake).to receive(:notify_performance_breakdown).with( - hash_including(groups: { http: be > 0 }), + hash_including(groups: { view: be > 0, http: be > 0 }), an_instance_of(Hash), ) get '/breakdown_http_rb' @@ -465,7 +490,7 @@ def force_http_libs_prepend it "includes the http breakdown" do expect(Airbrake).to receive(:notify_performance_breakdown).with( - hash_including(groups: { http: be > 0 }), + hash_including(groups: { view: be > 0, http: be > 0 }), an_instance_of(Hash), ) get '/breakdown_http_client' @@ -478,9 +503,16 @@ def force_http_libs_prepend stub_request(:get, 'http://example.com').to_return(body: '') end + before do + # On JRuby 9.2.19.0 this fails with a SIGSEGV in JVM: + # https://bit.ly/3Everoa + # This is somehow related to libcurl.so. + skip('SIGSEGV on JRuby 9.2.19.0') if Airbrake::JRUBY + end + it "includes the http breakdown" do expect(Airbrake).to receive(:notify_performance_breakdown).with( - hash_including(groups: { http: be > 0 }), + hash_including(groups: { view: be > 0, http: be > 0 }), an_instance_of(Hash), ) get '/breakdown_typhoeus' @@ -490,7 +522,8 @@ def force_http_libs_prepend context "when current user is logged in" do let(:user) do - OpenStruct.new(id: 1, email: 'qa@example.com', username: 'qa-dept') + stub_const('User', Struct.new(:id, :email, :username)) + User.new(1, 'qa@example.com', 'qa-dept') end before do diff --git a/spec/integration/rails/rake_spec.rb b/spec/integration/rails/rake_spec.rb index 85ef3578e..5bff56433 100644 --- a/spec/integration/rails/rake_spec.rb +++ b/spec/integration/rails/rake_spec.rb @@ -41,7 +41,7 @@ it "includes a timestamp" do expected_notice = a_notice_with( - %i[params rake_task timestamp], /20\d\d\-\d\d-\d\d.+/ + %i[params rake_task timestamp], /20\d\d-\d\d-\d\d.+/ ) expect(Airbrake).to receive(:notify_sync).with(expected_notice) end diff --git a/spec/integration/shared_examples/rack_examples.rb b/spec/integration/shared_examples/rack_examples.rb index 1925ef5ba..670e17f33 100644 --- a/spec/integration/shared_examples/rack_examples.rb +++ b/spec/integration/shared_examples/rack_examples.rb @@ -40,13 +40,8 @@ describe "user payload" do let(:user) do - OpenStruct.new( - id: 1, - email: 'qa@example.com', - username: 'qa-dept', - first_name: 'John', - last_name: 'Doe', - ) + stub_const('User', Struct.new(:id, :email, :username, :first_name, :last_name)) + User.new(1, 'qa@example.com', 'qa-dept', 'John', 'Doe') end before { login_as(user) } diff --git a/spec/integration/sinatra/sinatra_spec.rb b/spec/integration/sinatra/sinatra_spec.rb index 632770a08..2fbc5e3e6 100644 --- a/spec/integration/sinatra/sinatra_spec.rb +++ b/spec/integration/sinatra/sinatra_spec.rb @@ -25,7 +25,7 @@ get '/crash' sleep 2 - body = %r("context":{.*"route":"\/crash".*}) + body = %r("context":{.*"route":"/crash".*}) expect(a_request(:post, endpoint).with(body: body)).to have_been_made end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6b3c274c2..4fe35e7ef 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -97,7 +97,7 @@ # Don't load the Rack app since we want to test Sinatra if it's loaded. raise LoadError if defined?(Sinatra) - require 'apps/rack/dummy_app' + require 'apps/rack/rack_app' rescue LoadError puts '** Skipped Rack specs' end diff --git a/spec/support/matchers/a_notice_with.rb b/spec/support/matchers/a_notice_with.rb index 518c2869d..fb401d645 100644 --- a/spec/support/matchers/a_notice_with.rb +++ b/spec/support/matchers/a_notice_with.rb @@ -5,12 +5,7 @@ payload = notice[access_keys.shift] break(false) unless payload - actual_val = - if payload.respond_to?(:dig) - payload.dig(*access_keys) - else - dig_pre_23(payload, *access_keys) - end + actual_val = payload.dig(*access_keys) if expected_val.is_a?(Regexp) actual_val =~ expected_val @@ -18,15 +13,4 @@ actual_val == expected_val end end - - # TODO: Use the normal "dig" version once we support Ruby 2.3 and above. - def dig_pre_23(hash, *keys) - v = hash[keys.shift] - while keys.any? - return unless v.is_a?(Hash) - - v = v[keys.shift] - end - v - end end diff --git a/spec/unit/rack/instrumentable_spec.rb b/spec/unit/rack/instrumentable_spec.rb index bc09f2288..ddaacd11d 100644 --- a/spec/unit/rack/instrumentable_spec.rb +++ b/spec/unit/rack/instrumentable_spec.rb @@ -36,12 +36,16 @@ def method_with!(val); end def method_with?(val); end airbrake_capture_timing :method_with? + # rubocop:disable Layout/EmptyLinesAroundAttributeAccessor attr_writer :method_with airbrake_capture_timing :method_with= + # rubocop:enable Layout/EmptyLinesAroundAttributeAccessor + # rubocop:disable Lint/UselessMethodDefinition def ==(other) super end + # rubocop:enable Lint/UselessMethodDefinition airbrake_capture_timing :== def method_with_block @@ -65,25 +69,33 @@ def writer_with_everything=(a, b = nil, *args, foo:, bar: nil) prepend( Module.new do + # rubocop:disable Lint/UselessMethodDefinition def prepended_method!(*args, **kw_args) super end + # rubocop:enable Lint/UselessMethodDefinition + # rubocop:disable Lint/UselessMethodDefinition def prepended_writer=(*args, **kw_args) super end + # rubocop:enable Lint/UselessMethodDefinition protected + # rubocop:disable Lint/UselessMethodDefinition def a_prepended_protected_method super end + # rubocop:enable Lint/UselessMethodDefinition private + # rubocop:disable Lint/UselessMethodDefinition def a_prepended_private_method super end + # rubocop:enable Lint/UselessMethodDefinition end, ) @@ -236,12 +248,12 @@ def a_prepended_private_method; end end it "attaches timing for a method with all arg types" do - klass.new.send('method_with_everything!', 1, 2, 3, foo: 4, bar: 5) {} + klass.new.send('method_with_everything!', 1, 2, 3, foo: 4, bar: 5) { 1 } expect(groups).to match('method_with_everything!' => be > 0) end it "attaches timing for a writer method with all arg types" do - klass.new.send('writer_with_everything=', 1, 2, 3, foo: 4, bar: 5) {} + klass.new.send('writer_with_everything=', 1, 2, 3, foo: 4, bar: 5) { 1 } expect(groups).to match('writer_with_everything=' => be > 0) end @@ -251,7 +263,7 @@ def a_prepended_private_method; end end it "attaches timing for a prepended writer method with all arg types" do - klass.new.send('prepended_writer=', 1, 2, 3, foo: 4, bar: 5) {} + klass.new.send('prepended_writer=', 1, 2, 3, foo: 4, bar: 5) { 1 } expect(groups).to match('prepended_writer=' => be > 0) end diff --git a/spec/unit/rack/rack_spec.rb b/spec/unit/rack/rack_spec.rb index c48806143..5549d3b67 100644 --- a/spec/unit/rack/rack_spec.rb +++ b/spec/unit/rack/rack_spec.rb @@ -8,7 +8,7 @@ context "when request store doesn't have any routes" do it "doesn't store timing" do - described_class.capture_timing('operation') {} + described_class.capture_timing('operation') { 1 } expect(Airbrake::Rack::RequestStore.store).to be_empty end @@ -39,9 +39,9 @@ end it "doesn't attach any timings" do - described_class.capture_timing('operation 1') {} - described_class.capture_timing('operation 2') {} - described_class.capture_timing('operation 3') {} + described_class.capture_timing('operation 1') { 1 } + described_class.capture_timing('operation 2') { 2 } + described_class.capture_timing('operation 3') { 3 } expect(routes['/about'][:groups]).to be_empty end @@ -54,9 +54,9 @@ end it "attaches all timings for different operations to the request store" do - described_class.capture_timing('operation 1') {} - described_class.capture_timing('operation 2') {} - described_class.capture_timing('operation 3') {} + described_class.capture_timing('operation 1') { 1 } + described_class.capture_timing('operation 2') { 2 } + described_class.capture_timing('operation 3') { 3 } expect(routes['/about'][:groups]).to match( 'operation 1' => be > 0, diff --git a/spec/unit/rack/user_spec.rb b/spec/unit/rack/user_spec.rb index db05fc150..11f2d73f5 100644 --- a/spec/unit/rack/user_spec.rb +++ b/spec/unit/rack/user_spec.rb @@ -4,13 +4,8 @@ let(:endpoint) { 'https://api.airbrake.io/api/v3/projects/113743/notices' } let(:user) do - OpenStruct.new( - id: 1, - email: 'qa@example.com', - username: 'qa-dept', - first_name: 'Bingo', - last_name: 'Bongo', - ) + stub_const('User', Struct.new(:id, :email, :username, :first_name, :last_name)) + User.new(1, 'qa@example.com', 'qa-dept', 'Bingo', 'Bongo') end def env_for(url, opts = {}) @@ -190,7 +185,7 @@ def current_user end context "when Rack user doesn't contain any of the expect fields" do - let(:user_data) { described_class.new(OpenStruct.new).as_json } + let(:user_data) { described_class.new({}).as_json } it "is empty" do expect(user_data).to be_empty diff --git a/spec/unit/rails/action_controller_notify_subscriber_spec.rb b/spec/unit/rails/action_controller_notify_subscriber_spec.rb index 16b4509c0..a7d70708e 100644 --- a/spec/unit/rails/action_controller_notify_subscriber_spec.rb +++ b/spec/unit/rails/action_controller_notify_subscriber_spec.rb @@ -3,6 +3,8 @@ require 'airbrake/rails/action_controller_notify_subscriber' RSpec.describe Airbrake::Rails::ActionControllerNotifySubscriber do + subject(:subscriber) { described_class.new } + after { Airbrake::Rack::RequestStore.clear } describe "#call" do @@ -10,12 +12,13 @@ before do allow(Airbrake::Rails::Event).to receive(:new).and_return(event) + allow(Airbrake).to receive(:notify_request) end context "when there are no routes in the request store" do it "doesn't notify requests" do - expect(Airbrake).not_to receive(:notify_request) - subject.call([]) + subscriber.call([]) + expect(Airbrake).not_to have_received(:notify_request) end end @@ -38,31 +41,39 @@ end it "doesn't notify requests" do - expect(Airbrake).not_to receive(:notify_request) - subject.call([]) + subscriber.call([]) + expect(Airbrake).not_to have_received(:notify_request) end end context "and when the Airbrake config enables performance stats" do + let(:timestamp) { Time.now.to_f } + before do allow(Airbrake::Config.instance) .to receive(:performance_stats).and_return(true) + + allow(event).to receive(:method).and_return('GET') + allow(event).to receive(:status_code).and_return(200) + allow(event).to receive(:time).and_return(timestamp) + allow(event).to receive(:duration).and_return(1.234) end it "sends request info to Airbrake" do - expect(Airbrake).to receive(:notify_request).with( + subscriber.call([]) + + expect(Airbrake).to have_received(:notify_request).with( hash_including( method: 'GET', route: '/test-route', status_code: 200, + time: timestamp, ), ) - expect(event).to receive(:method).and_return('GET') - expect(event).to receive(:status_code).and_return(200) - expect(event).to receive(:time).and_return(Time.now) - expect(event).to receive(:duration).and_return(1.234) - - subject.call([]) + expect(event).to have_received(:method) + expect(event).to have_received(:status_code) + expect(event).to have_received(:time) + expect(event).to have_received(:duration) end end end diff --git a/spec/unit/rails/action_controller_performance_breakdown_subscriber_spec.rb b/spec/unit/rails/action_controller_performance_breakdown_subscriber_spec.rb index fadfeef5d..7f9264857 100644 --- a/spec/unit/rails/action_controller_performance_breakdown_subscriber_spec.rb +++ b/spec/unit/rails/action_controller_performance_breakdown_subscriber_spec.rb @@ -95,7 +95,7 @@ } end - it "sends request info as resource stash" do + it "sends request info as metric stash" do expect(Airbrake).to receive(:notify_performance_breakdown).with( an_instance_of(Hash), hash_including(request: request), @@ -113,7 +113,7 @@ ) end - it "sends user info as resource stash" do + it "sends user info as metric stash" do expect(Airbrake).to receive(:notify_performance_breakdown).with( an_instance_of(Hash), hash_including(user: { 'id' => 1, 'name' => 'Arthur' }), diff --git a/spec/unit/rails/action_controller_route_subscriber_spec.rb b/spec/unit/rails/action_controller_route_subscriber_spec.rb index ec5432f79..d60b22ed6 100644 --- a/spec/unit/rails/action_controller_route_subscriber_spec.rb +++ b/spec/unit/rails/action_controller_route_subscriber_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'ostruct' require 'airbrake/rails/action_controller_route_subscriber' RSpec.describe Airbrake::Rails::ActionControllerRouteSubscriber do diff --git a/spec/unit/rake/tasks_spec.rb b/spec/unit/rake/tasks_spec.rb index 18251325d..813bacf0a 100644 --- a/spec/unit/rake/tasks_spec.rb +++ b/spec/unit/rake/tasks_spec.rb @@ -11,7 +11,7 @@ def wait_for_a_request_with_body(body) before do stub_request(:post, endpoint).to_return(status: 201, body: '{}') - allow(STDOUT).to receive(:puts).and_return(nil) + allow($stdout).to receive(:puts).and_return(nil) end describe "airbrake:deploy" do @@ -62,7 +62,7 @@ def wait_for_a_request_with_body(body) describe "parsing environment variables" do it "does not raise when an env variable value contains '='" do - heroku_config = airbrake_vars + "URL=https://airbrake.io/docs?key=11\n" + heroku_config = "#{airbrake_vars} URL=https://airbrake.io/docs?key=11\n" expect(Bundler).to receive(:with_clean_env).twice.and_return(heroku_config) task.invoke diff --git a/spec/unit/shoryuken_spec.rb b/spec/unit/shoryuken_spec.rb index e7bcaeced..59bdb7b4b 100644 --- a/spec/unit/shoryuken_spec.rb +++ b/spec/unit/shoryuken_spec.rb @@ -89,7 +89,7 @@ def wait_for_a_request_with_body(body) timing: an_instance_of(Float), ) - expect { subject.call(worker, queue, nil, body) {} }.not_to raise_error + expect { subject.call(worker, queue, nil, body) { 1 } }.not_to raise_error end end end