diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml deleted file mode 100644 index 5632ec94a..000000000 --- a/.buildkite/pipeline.yml +++ /dev/null @@ -1,498 +0,0 @@ -steps: - - label: ':docker: Build CI image' - timeout_in_minutes: 30 - plugins: - - docker-compose#v3.1.0: - build: ruby-maze-runner - image-repository: 855461928731.dkr.ecr.us-west-1.amazonaws.com/ruby - cache-from: ruby-maze-runner:855461928731.dkr.ecr.us-west-1.amazonaws.com/ruby:base-ruby${BRANCH_NAME} - - docker-compose#v3.1.0: - push: - - ruby-maze-runner:855461928731.dkr.ecr.us-west-1.amazonaws.com/ruby:base-ruby${BRANCH_NAME} - - ruby-maze-runner:855461928731.dkr.ecr.us-west-1.amazonaws.com/ruby:base-ruby-latest - - - wait - - - label: ':ruby: Ruby 1.9 unit tests' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-unit-tests - use-aliases: true - env: - RUBY_TEST_VERSION: "1.9.3" - BUNDLE_VERSION: "1.12.0" - - - label: ':ruby: Ruby 2.7 unit tests' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-unit-tests - use-aliases: true - env: - RUBY_TEST_VERSION: "2.7" - GEMSETS: "test sidekiq coverage" - - label: ':ruby: Ruby 2.7 linting' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-unit-tests - use-aliases: true - env: - RUBY_TEST_VERSION: "2.7" - GEMSETS: "test rubocop" - command: "bundle exec ./bin/rubocop lib/" - - - label: ':ruby: Ruby 2.7 plain tests' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/plain_features/", "--tags", "not @wip"] - env: - RUBY_TEST_VERSION: "2.7" - - - label: ':rails: Rails 6 Ruby 2.7 tests' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/rails_features/", "--tags", "@rails6 and not @wip"] - env: - RUBY_TEST_VERSION: "2.7" - RAILS_VERSION: "6" - - - label: ':rails: Rails 7 Ruby 2.7 tests' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/rails_features/", "--tags", "@rails7 and not @wip"] - env: - RUBY_TEST_VERSION: "2.7" - RAILS_VERSION: "7" - - label: ':rails: Rails integrations Ruby 2.7 tests' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/rails_features/", "--tags", "@rails_integrations"] - env: - RUBY_TEST_VERSION: "2.7" - RAILS_VERSION: "_integrations" - - - label: ':construction: Delayed job tests' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/delayed_job.feature", "--tags", "not @wip"] - env: - RUBY_TEST_VERSION: "2.5" - - - label: ':sidekiq: Sidekiq 6 tests' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/sidekiq.feature", "--tags", "not @wip"] - env: - RUBY_TEST_VERSION: "2.5" - SIDEKIQ_VERSION: "6" - - - wait - - - label: ':ruby: JRuby unit tests' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: jruby-unit-tests - use-aliases: true - concurrency: 4 - concurrency_group: 'ruby/unit-tests' - - - label: ':ruby: Ruby {{matrix}} unit tests' - matrix: - - '2.0' - - '2.1' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-unit-tests - use-aliases: true - env: - RUBY_TEST_VERSION: "{{matrix}}" - BUNDLE_VERSION: "1.12.0" - concurrency: 4 - concurrency_group: 'ruby/unit-tests' - - - label: ':ruby: Ruby {{matrix}} unit tests' - matrix: - - '2.2' - - '2.3' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-unit-tests - use-aliases: true - env: - RUBY_TEST_VERSION: "{{matrix}}" - BUNDLE_VERSION: "1.12.0" - GEMSETS: "test sidekiq" - concurrency: 4 - concurrency_group: 'ruby/unit-tests' - - - label: ':ruby: Ruby {{matrix}} unit tests' - matrix: - - '2.4' - - '2.5' - - '2.6' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-unit-tests - use-aliases: true - env: - RUBY_TEST_VERSION: "{{matrix}}" - GEMSETS: "test sidekiq" - concurrency: 4 - concurrency_group: 'ruby/unit-tests' - - - label: ':ruby: Ruby {{matrix}} unit tests' - matrix: - - '3.0' - - '3.1' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-unit-tests - use-aliases: true - env: - RUBY_TEST_VERSION: "{{matrix}}" - concurrency: 4 - concurrency_group: 'ruby/unit-tests' - - - label: ':ruby: Ruby {{matrix}} plain tests' - matrix: - - "1.9.3" - - "2.0" - - "2.1" - - "2.2" - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/plain_features", "--tags", "not @wip"] - env: - RUBY_TEST_VERSION: "{{matrix}}" - BUNDLE_VERSION: "1.12.0" - concurrency: 8 - concurrency_group: 'ruby/slow-maze-runner-tests' - - - label: ':ruby: Ruby {{matrix}} plain tests' - matrix: - - '2.3' - - '2.4' - - '2.5' - - '2.6' - - '3.0' - - '3.1' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/plain_features/", "--tags", "not @wip"] - env: - RUBY_TEST_VERSION: "{{matrix}}" - concurrency: 8 - concurrency_group: 'ruby/slow-maze-runner-tests' - - - label: ':sidekiq: Sidekiq {{matrix}} tests' - matrix: - - '2' - - '3' - - '4' - - '5' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/sidekiq.feature", "--tags", "not @wip"] - env: - RUBY_TEST_VERSION: "2.5" - SIDEKIQ_VERSION: "{{matrix}}" - concurrency: 4 - concurrency_group: 'ruby/integrations-maze-runner-tests' - - - label: ':rails: Rails 3 Ruby {{matrix}} tests' - matrix: - - '2.0' - - '2.1' - - '2.2' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/rails_features/", "--tags", "@rails3 and not @wip"] - env: - RUBY_TEST_VERSION: "{{matrix}}" - RAILS_VERSION: "3" - BUNDLE_VERSION: "1.12.0" - concurrency: 8 - concurrency_group: 'ruby/slow-maze-runner-tests' - - - label: ':rails: Rails 3 Ruby {{matrix}} tests' - matrix: - - '2.3' - - '2.4' - - '2.5' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/rails_features/", "--tags", "@rails3 and not @wip"] - env: - RUBY_TEST_VERSION: "{{matrix}}" - RAILS_VERSION: "3" - concurrency: 8 - concurrency_group: 'ruby/slow-maze-runner-tests' - - - label: ':rails: Rails 4 Ruby 2.2 tests' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/rails_features/", "--tags", "@rails4 and not @wip"] - env: - RUBY_TEST_VERSION: "2.2" - RAILS_VERSION: "4" - BUNDLE_VERSION: "1.12.0" - concurrency: 8 - concurrency_group: 'ruby/slow-maze-runner-tests' - - - label: ':rails: Rails 4 Ruby 2.3 tests' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/rails_features/", "--tags", "@rails4 and not @wip"] - env: - RUBY_TEST_VERSION: "2.3" - RAILS_VERSION: "4" - concurrency: 8 - concurrency_group: 'ruby/slow-maze-runner-tests' - - - label: ':rails: Rails 5 Ruby {{matrix}} tests' - matrix: - - '2.2' - - '2.3' - - '2.4' - - '2.5' - - '2.6' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/rails_features/", "--tags", "@rails5 and not @wip"] - env: - RUBY_TEST_VERSION: "{{matrix}}" - RAILS_VERSION: "5" - concurrency: 8 - concurrency_group: 'ruby/slow-maze-runner-tests' - - - label: ':rails: Rails 6 Ruby {{matrix}} tests' - matrix: - - '2.5' - - '2.6' - - '3.0' - - '3.1' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/rails_features/", "--tags", "@rails6 and not @wip"] - env: - RUBY_TEST_VERSION: "{{matrix}}" - RAILS_VERSION: "6" - concurrency: 8 - concurrency_group: 'ruby/slow-maze-runner-tests' - - - label: ':rails: Rails 7 Ruby {{matrix}} tests' - matrix: - - '3.0' - - '3.1' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/rails_features/", "--tags", "@rails7 and not @wip"] - env: - RUBY_TEST_VERSION: "{{matrix}}" - RAILS_VERSION: "7" - - - label: ':rails: Rails integrations Ruby 3.0 tests' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ["features/rails_features/", "--tags", "@rails_integrations"] - env: - RUBY_TEST_VERSION: "3.0" - RAILS_VERSION: "_integrations" - concurrency: 8 - concurrency_group: 'ruby/slow-maze-runner-tests' - - - label: ':clipboard: Rake Ruby {{matrix}} tests' - matrix: - - '1.9.3' - - '2.0' - - '2.1' - - '2.2' - - '2.3' - - '2.4' - - '2.5' - - '2.6' - - '2.7' - - '3.0' - - '3.1' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ['features/rake.feature', '--tags', 'not @wip'] - env: - RUBY_TEST_VERSION: "{{matrix}}" - concurrency: 4 - concurrency_group: 'ruby/integrations-maze-runner-tests' - - - label: ':postbox: Mailman Ruby {{matrix}} tests' - matrix: - - '2.0' - - '2.1' - - '2.2' - - '2.3' - - '2.4' - - '2.5' - - '2.6' - - '2.7' - - '3.0' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ['features/mailman.feature', '--tags', 'not @wip'] - env: - RUBY_TEST_VERSION: "{{matrix}}" - concurrency: 4 - concurrency_group: 'ruby/integrations-maze-runner-tests' - - - label: ':key: Que 0.14 Ruby {{matrix}} tests' - matrix: - - '2.0' - - '2.1' - - '2.2' - - '2.3' - - '2.4' - - '2.5' - - '2.6' - - '2.7' - - '3.0' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ['features/que.feature', '--tags', 'not @wip'] - env: - RUBY_TEST_VERSION: '{{matrix}}' - QUE_VERSION: '0.14' - concurrency: 4 - concurrency_group: 'ruby/integrations-maze-runner-tests' - - - label: ':key: Que 1.x Ruby {{matrix}} tests' - matrix: - - '2.5' - - '2.6' - - '2.7' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ['features/que.feature', '--tags', 'not @wip'] - env: - RUBY_TEST_VERSION: '{{matrix}}' - QUE_VERSION: '1' - concurrency: 4 - concurrency_group: 'ruby/integrations-maze-runner-tests' - - - label: ':bed: Rack 1 Ruby {{matrix}} tests' - matrix: - - '1.9.3' - - '2.0' - - '2.1' - - '2.2' - - '2.3' - - '2.4' - - '2.5' - - '2.6' - - '2.7' - - '3.0' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ['features/rack.feature', '--tags', 'not @wip'] - env: - RUBY_TEST_VERSION: '{{matrix}}' - RACK_VERSION: '1' - concurrency: 4 - concurrency_group: 'ruby/integrations-maze-runner-tests' - - - label: ':bed: Rack 2 Ruby {{matrix}} tests' - matrix: - - '2.2' - - '2.3' - - '2.4' - - '2.5' - - '2.6' - - '2.7' - - '3.0' - - '3.1' - timeout_in_minutes: 30 - plugins: - docker-compose#v3.1.0: - run: ruby-maze-runner - use-aliases: true - command: ['features/rack.feature', '--tags', 'not @wip'] - env: - RUBY_TEST_VERSION: '{{matrix}}' - RACK_VERSION: '2' - concurrency: 4 - concurrency_group: 'ruby/integrations-maze-runner-tests' - - - name: ':copyright: License Audit' - plugins: - docker-compose#v3.7.0: - run: license_finder - command: /bin/bash -lc '/scan/scripts/license_finder.sh' diff --git a/.github/workflows/license-audit.yml b/.github/workflows/license-audit.yml new file mode 100644 index 000000000..6c3b1644e --- /dev/null +++ b/.github/workflows/license-audit.yml @@ -0,0 +1,24 @@ +name: license audit + +on: [push, pull_request] + +jobs: + license-audit: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: fetch decisions.yml + run: | + curl https://raw.githubusercontent.com/bugsnag/license-audit/master/config/decision_files/global.yml -o config/decisions.yml + curl https://raw.githubusercontent.com/bugsnag/license-audit/master/config/decision_files/bugsnag-ruby.yml >> config/decisions.yml + + - name: run license finder + # for some reason license finder doesn't run without a login shell (-l) + run: > + docker run -v $PWD:/scan licensefinder/license_finder /bin/bash -lc " + cd /scan && + bundle install && + license_finder --decisions-file config/decisions.yml + " diff --git a/.github/workflows/maze-runner.yml b/.github/workflows/maze-runner.yml new file mode 100644 index 000000000..979e2d265 --- /dev/null +++ b/.github/workflows/maze-runner.yml @@ -0,0 +1,149 @@ +name: maze-runner + +on: [push, pull_request] + +jobs: + rake-maze-runner: + strategy: + fail-fast: false + matrix: + ruby-version: ['1.9', '3.1'] + + uses: ./.github/workflows/run-maze-runner.yml + with: + features: features/rake.feature + ruby-version: ${{ matrix.ruby-version }} + + mailman-maze-runner: + strategy: + fail-fast: false + matrix: + ruby-version: ['2.0', '3.0'] + + uses: ./.github/workflows/run-maze-runner.yml + with: + features: features/mailman.feature + ruby-version: ${{ matrix.ruby-version }} + + rack-maze-runner: + strategy: + fail-fast: false + matrix: + include: + - ruby-version: '1.9' + rack-version: '1' + - ruby-version: '3.0' + rack-version: '1' + - ruby-version: '2.2' + rack-version: '2' + - ruby-version: '3.1' + rack-version: '2' + - ruby-version: '2.4' + rack-version: '3' + - ruby-version: '3.1' + rack-version: '3' + + uses: ./.github/workflows/run-maze-runner.yml + with: + features: features/rack.feature + ruby-version: ${{ matrix.ruby-version }} + rack-version: ${{ matrix.rack-version }} + + que-maze-runner: + strategy: + fail-fast: false + matrix: + include: + - ruby-version: '2.0' + que-version: '0.14' + - ruby-version: '3.1' + que-version: '0.14' + - ruby-version: '2.5' + que-version: '1' + - ruby-version: '2.7' + que-version: '1' + + uses: ./.github/workflows/run-maze-runner.yml + with: + features: features/que.feature + ruby-version: ${{ matrix.ruby-version }} + que-version: ${{ matrix.que-version }} + + sidekiq-maze-runner: + strategy: + fail-fast: false + matrix: + ruby-version: ['2.5'] + sidekiq-version: ['2', '3', '4', '5', '6'] + + uses: ./.github/workflows/run-maze-runner.yml + with: + features: features/sidekiq.feature + ruby-version: ${{ matrix.ruby-version }} + sidekiq-version: ${{ matrix.sidekiq-version }} + + delayed-job-maze-runner: + strategy: + fail-fast: false + matrix: + ruby-version: ['2.5'] + + uses: ./.github/workflows/run-maze-runner.yml + with: + features: features/delayed_job.feature + ruby-version: ${{ matrix.ruby-version }} + + rails-3-4-5-maze-runner: + strategy: + fail-fast: false + matrix: + ruby-version: ['2.2', '2.5'] + rails-version: ['3', '4', '5'] + include: + - ruby-version: '2.0' + rails-version: '3' + - ruby-version: '2.6' + rails-version: '5' + exclude: + - ruby-version: '2.2' + rails-version: '3' + - ruby-version: '2.5' + rails-version: '5' + + uses: ./.github/workflows/run-maze-runner.yml + with: + features: features/rails_features/ --tags @rails${{ matrix.rails-version }} + ruby-version: ${{ matrix.ruby-version }} + rails-version: ${{ matrix.rails-version }} + + rails-6-7-integrations-maze-runner: + strategy: + fail-fast: false + matrix: + ruby-version: ['2.7', '3.1'] + rails-version: ['6', '7', '_integrations'] + include: + - ruby-version: '2.5' + rails-version: '6' + exclude: + - ruby-version: '2.7' + rails-version: '6' + - ruby-version: '3.1' + rails-version: '_integrations' + + uses: ./.github/workflows/run-maze-runner.yml + with: + features: features/rails_features/ --tags @rails${{ matrix.rails-version }} + ruby-version: ${{ matrix.ruby-version }} + rails-version: ${{ matrix.rails-version }} + + plain-maze-runner: + strategy: + fail-fast: false + matrix: + ruby-version: ['1.9', '2.0', '2.1', '2.2', '2.3', '2.4', '2.5', '2.6', '2.7', '3.0', '3.1'] + + uses: ./.github/workflows/run-maze-runner.yml + with: + features: features/plain_features/ + ruby-version: ${{ matrix.ruby-version }} diff --git a/.github/workflows/run-maze-runner.yml b/.github/workflows/run-maze-runner.yml new file mode 100644 index 000000000..9a64c5030 --- /dev/null +++ b/.github/workflows/run-maze-runner.yml @@ -0,0 +1,53 @@ +name: run-maze-runner + +on: + workflow_call: + inputs: + features: + required: true + type: string + ruby-version: + required: true + type: string + rack-version: + required: false + type: string + que-version: + required: false + type: string + rails-version: + required: false + type: string + sidekiq-version: + required: false + type: string + +jobs: + maze-runner: + runs-on: ubuntu-latest + + env: + BUNDLE_GEMFILE: Gemfile-maze-runner + + steps: + - uses: actions/checkout@v3 + + - name: Install libcurl4-openssl-dev and net-tools + run: | + sudo apt-get update + sudo apt-get install libcurl4-openssl-dev net-tools + + - name: install Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + bundler-cache: true + + - run: bundle exec maze-runner ${{ inputs.features }} --no-source + env: + NETWORK_NAME: notwerk + RUBY_TEST_VERSION: ${{ inputs.ruby-version }} + RACK_VERSION: ${{ inputs.rack-version }} + QUE_VERSION: ${{ inputs.que-version }} + RAILS_VERSION: ${{ inputs.rails-version }} + SIDEKIQ_VERSION: ${{ inputs.sidekiq-version }} diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml new file mode 100644 index 000000000..252af86d8 --- /dev/null +++ b/.github/workflows/test-package.yml @@ -0,0 +1,51 @@ +name: test + +on: [push, pull_request] + +jobs: + specs: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + ruby-version: ['2.2', '2.3', '2.4', '2.5', '2.6', '2.7', '3.0', '3.1'] + optional-groups: ['test sidekiq'] + include: + - ruby-version: '1.9' + optional-groups: 'test' + - ruby-version: '2.0' + optional-groups: 'test' + - ruby-version: '2.1' + optional-groups: 'test' + - ruby-version: 'jruby' + optional-groups: 'test' + + steps: + - uses: actions/checkout@v3 + + - name: install Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + + - run: bundle install --with "${{ matrix.optional-groups }}" --binstubs + + - run: ./bin/rake spec + + linting: + runs-on: ubuntu-latest + + env: + BUNDLE_WITH: rubocop + + steps: + - uses: actions/checkout@v3 + + - name: install Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + bundler-cache: true + + - run: bundle exec rubocop lib/ diff --git a/.gitignore b/.gitignore index 9458c940f..fe9799131 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ doc # bundler .bundle Gemfile.lock +Gemfile-maze-runner.lock # jeweler generated pkg @@ -52,4 +53,4 @@ vendor bin # For Bugsnag-Maze-Runner: -maze_output \ No newline at end of file +maze_output diff --git a/.yardopts b/.yardopts index b1c9cc2b6..b8575d80c 100644 --- a/.yardopts +++ b/.yardopts @@ -4,6 +4,7 @@ --no-private --protected --title "bugsnag-ruby API Documentation" +--embed-mixins lib/**/*.rb - README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e88acb5f5..27d996621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Changelog ========= +## v6.26.0 (1 December 2022) + +### Enhancements + +* Add support for feature flags & experiments. For more information, please see https://docs.bugsnag.com/product/features-experiments + | [#758](https://github.com/bugsnag/bugsnag-ruby/pull/758) + ## v6.24.2 (21 January 2022) ### Fixes diff --git a/Gemfile b/Gemfile index a574155f9..6a736ccce 100644 --- a/Gemfile +++ b/Gemfile @@ -28,6 +28,11 @@ group :test, optional: true do gem 'webrick' if ruby_version >= Gem::Version.new('3.0.0') gem 'rexml', '< 3.2.5' if ruby_version == Gem::Version.new('2.0.0') + + if ruby_version >= Gem::Version.new('2.2.0') && ruby_version < Gem::Version.new('2.4.0') + gem 'minitest', '< 5.16.0' + gem 'connection_pool', '< 2.3.0' + end end group :coverage, optional: true do @@ -35,25 +40,32 @@ group :coverage, optional: true do gem 'coveralls' end -group :rubocop, optional: true do - gem 'rubocop', '~> 1.0.0' +if ruby_version >= Gem::Version.new('2.4.0') + group :rubocop, optional: true do + gem 'rubocop', '~> 1.0.0' + end end -group :sidekiq, optional: true do - gem 'sidekiq', '~> 5.2.7' - - if ruby_version < Gem::Version.new('2.3.0') - # redis 4.1.2 dropped support for Ruby 2.2 - gem 'redis', '4.1.1' - elsif ruby_version < Gem::Version.new('2.4.0') - # redis 4.5.0 dropped support for Ruby 2.3 - gem 'redis', '< 4.5.0' - else - gem 'redis' - end +if ruby_version >= Gem::Version.new('2.2.0') + group :sidekiq, optional: true do + gem 'sidekiq', '~> 5.2.7' - # rack 2.2.0 dropped support for Ruby 2.2 - gem 'rack', ruby_version < Gem::Version.new('2.3.0') ? '< 2.2.0' : '~> 2.2' + if ruby_version < Gem::Version.new('2.3.0') + # redis 4.1.2 dropped support for Ruby 2.2 + gem 'redis', '4.1.1' + elsif ruby_version < Gem::Version.new('2.4.0') + # redis 4.5.0 dropped support for Ruby 2.3 + gem 'redis', '< 4.5.0' + else + gem 'redis' + end + + # rack 2.2.0 dropped support for Ruby 2.2 + gem 'rack', ruby_version < Gem::Version.new('2.3.0') ? '< 2.2.0' : '~> 2.2' + + # rack-protection 3.0.0 requires Ruby 2.6+ + gem 'rack-protection', '< 3.0.0' if ruby_version < Gem::Version.new('2.6.0') + end end gemspec diff --git a/Gemfile-maze-runner b/Gemfile-maze-runner new file mode 100644 index 000000000..05bb7a68b --- /dev/null +++ b/Gemfile-maze-runner @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', tag: 'v7.6.0' diff --git a/TESTING.md b/TESTING.md index e10187cc3..fdc540b07 100644 --- a/TESTING.md +++ b/TESTING.md @@ -29,30 +29,12 @@ End to end tests are written in cucumber-style `.feature` files, and need Ruby-b Maze runner's CLI and the test fixtures are containerised so you'll need Docker (and Docker Compose) to run them. -__Note: only Bugsnag employees can run the end-to-end tests.__ We have dedicated test infrastructure and private BrowserStack credentials which can't be shared outside of the organisation. - -##### Authenticating with the private container registry - -You'll need to set the credentials for the aws profile in order to access the private docker registry: - -``` -aws configure --profile=opensource -``` - -Subsequently you'll need to run the following commmand to authenticate with the registry: - -``` -aws ecr get-login-password --profile=opensource | docker login --username AWS --password-stdin 855461928731.dkr.ecr.us-west-1.amazonaws.com -``` - -__Your session will periodically expire__, so you'll need to run this command to re-authenticate when that happens. - ### Running the end to end tests -Once registered with the remote repository, build the test container: +Install Maze Runner: ``` -docker-compose build ruby-maze-runner +$ BUNDLE_GEMFILE=Gemfile-maze-runner bundle install ``` Configure the tests to be run in the following way: @@ -61,10 +43,13 @@ Configure the tests to be run in the following way: - If testing rails, set the rails version to be tested using the environment variable `RAILS_VERSION` e.g. `RAILS_VERSION=3` - If testing sidekiq, set the version to be tested using the environment variable `SIDEKIQ_VERSION`, e.g. `SIDEKIQ_VERSION=2` -When running the end-to-end tests, you'll want to restrict the feature files run to the specific test features for the platform. This is done using the Cucumber CLI syntax at the end of the `docker-compose run ruby-maze-runner` command, i.e: +When running the end-to-end tests, you'll want to restrict the feature files run to the specific test features for the platform. This is done using the Cucumber CLI syntax, i.e: ``` -RUBY_TEST_VERSION=2.6 RAILS_VERSION=6 docker-compose run --use-aliases ruby-maze-runner features/rails_features --tags "@rails6" +RUBY_TEST_VERSION=2.6 \ + RAILS_VERSION=6 \ + BUNDLE_GEMFILE=Gemfile-maze-runner \ + bundle exec maze-runner features/rails_features --tags "@rails6" ``` - Plain ruby tests should target `features/plain_features` @@ -75,7 +60,10 @@ RUBY_TEST_VERSION=2.6 RAILS_VERSION=6 docker-compose run --use-aliases ruby-maze In order to target specific features the exact `.feature` file can be specified, i.e: ``` -RUBY_TEST_VERSION=2.6 RAILS_VERSION=6 docker-compose run --use-aliases ruby-maze-runner features/rails_features/app_version.feature --tags "@rails6" +RUBY_TEST_VERSION=2.6 \ + RAILS_VERSION=6 \ + BUNDLE_GEMFILE=Gemfile-maze-runner \ + bundle exec maze-runner features/rails_features/app_version.feature --tags "@rails6" ``` In order to avoid running flakey or unfinished tests, the tag `"not @wip"` can be added to the tags option. This is recommended for all CI runs. If a tag is already specified, this should be added using the `and` keyword, e.g. `--tags "@rails6 and not @wip"` diff --git a/VERSION b/VERSION index 21128ab39..961b1c8ec 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.24.2 +6.25.0 diff --git a/bugsnag.gemspec b/bugsnag.gemspec index e56504df4..3a0338738 100644 --- a/bugsnag.gemspec +++ b/bugsnag.gemspec @@ -18,5 +18,13 @@ Gem::Specification.new do |s| ] s.require_paths = ["lib"] s.required_ruby_version = '>= 1.9.2' - s.add_runtime_dependency 'concurrent-ruby', '~> 1.0' + + ruby_version = Gem::Version.new(RUBY_VERSION.dup) + + if ruby_version < Gem::Version.new('2.2.0') + # concurrent-ruby 1.1.10 requires Ruby 2.2+ + s.add_runtime_dependency 'concurrent-ruby', '~> 1.0', '< 1.1.10' + else + s.add_runtime_dependency 'concurrent-ruby', '~> 1.0' + end end diff --git a/dockerfiles/Dockerfile.audit b/dockerfiles/Dockerfile.audit deleted file mode 100644 index 5a19c1a96..000000000 --- a/dockerfiles/Dockerfile.audit +++ /dev/null @@ -1,5 +0,0 @@ -FROM licensefinder/license_finder - -WORKDIR /scan - -CMD /scan/scripts/license_finder.sh diff --git a/features/delayed_job.feature b/features/delayed_job.feature index c94c31c3c..0c7f31b46 100644 --- a/features/delayed_job.feature +++ b/features/delayed_job.feature @@ -2,8 +2,8 @@ Feature: Bugsnag detects errors in Delayed job workers Scenario: An unhandled RuntimeError sends a report with arguments Given I run the service "delayed_job" with the command "bundle exec rake delayed_job_tests:fail_with_args" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is true And the event "severity" equals "error" And the event "context" equals "TestModel.fail_with_args" @@ -17,13 +17,13 @@ Scenario: An unhandled RuntimeError sends a report with arguments And the event "metaData.job.max_attempts" equals 1 And the event "metaData.job.payload.display_name" equals "TestModel.fail_with_args" And the event "metaData.job.payload.method_name" equals "fail_with_args" - And the payload field "events.0.metaData.job.payload.args.0" equals "Test" + And the event "metaData.job.payload.args.0" equals "Test" And the event "device.runtimeVersions.delayed_job" matches "\d+\.\d+\.\d+" Scenario: A handled exception sends a report Given I run the service "delayed_job" with the command "bundle exec rake delayed_job_tests:notify_with_args" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is false And the event "severity" equals "warning" And the event "context" equals "TestModel.notify_with_args" @@ -36,13 +36,13 @@ Scenario: A handled exception sends a report And the event "metaData.job.max_attempts" equals 1 And the event "metaData.job.payload.display_name" equals "TestModel.notify_with_args" And the event "metaData.job.payload.method_name" equals "notify_with_args" - And the payload field "events.0.metaData.job.payload.args.0" equals "Test" + And the event "metaData.job.payload.args.0" equals "Test" And the event "device.runtimeVersions.delayed_job" matches "\d+\.\d+\.\d+" Scenario: The report context uses the class name if no display name is available Given I run the service "delayed_job" with the command "bundle exec rake delayed_job_tests:report_context" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is true And the event "severity" equals "error" And the event "context" equals "TestReportContextJob" diff --git a/features/fixtures/delayed_job/app/Rakefile b/features/fixtures/delayed_job/app/Rakefile index efcae1adb..5a4d14a1e 100644 --- a/features/fixtures/delayed_job/app/Rakefile +++ b/features/fixtures/delayed_job/app/Rakefile @@ -20,9 +20,9 @@ namespace :delayed_job_tests do end def run_delayed_job_test(command) - fork do - system("rake jobs:work") - end + # queue the job with rails' runner system("rails runner #{command}") - Process.wait + + # run the queued jobs and exit + system("rake jobs:workoff") end diff --git a/features/fixtures/docker-compose.yml b/features/fixtures/docker-compose.yml index 396a73fbf..ab99ac022 100644 --- a/features/fixtures/docker-compose.yml +++ b/features/fixtures/docker-compose.yml @@ -78,27 +78,20 @@ services: - CALLBACK_INITIATOR restart: "no" - rack1: + rack: build: - context: rack1 - args: - - RUBY_TEST_VERSION - environment: - - BUGSNAG_API_KEY - - BUGSNAG_ENDPOINT - - BUGSNAG_METADATA_FILTERS - restart: "no" - - rack2: - build: - context: rack2 + context: rack args: - RUBY_TEST_VERSION + - RACK_VERSION environment: - BUGSNAG_API_KEY - BUGSNAG_ENDPOINT - BUGSNAG_METADATA_FILTERS restart: "no" + ports: + - target: 3000 + published: 7251 rails3: build: @@ -134,6 +127,9 @@ services: - USE_DEFAULT_AUTO_CAPTURE_SESSIONS - ADD_REQUEST_ON_ERROR restart: "no" + ports: + - target: 3000 + published: 7253 rails4: build: @@ -171,6 +167,9 @@ services: - USE_DEFAULT_AUTO_CAPTURE_SESSIONS - ADD_REQUEST_ON_ERROR restart: "no" + ports: + - target: 3000 + published: 7254 rails5: build: @@ -208,6 +207,9 @@ services: - USE_DEFAULT_AUTO_CAPTURE_SESSIONS - ADD_REQUEST_ON_ERROR restart: "no" + ports: + - target: 3000 + published: 7255 rails6: build: @@ -249,6 +251,9 @@ services: default: aliases: - rails6 + ports: + - target: 3000 + published: 7256 rails7: build: @@ -290,6 +295,9 @@ services: default: aliases: - rails7 + ports: + - target: 3000 + published: 7257 rails_integrations: build: @@ -302,6 +310,7 @@ services: environment: - BUGSNAG_API_KEY - BUGSNAG_ENDPOINT + - BUGSNAG_SESSION_ENDPOINT - DB_USERNAME=postgres - DB_PASSWORD=test_password - DB_HOST=postgres diff --git a/features/fixtures/rack1/.dockerignore b/features/fixtures/rack/.dockerignore similarity index 100% rename from features/fixtures/rack1/.dockerignore rename to features/fixtures/rack/.dockerignore diff --git a/features/fixtures/rack2/Dockerfile b/features/fixtures/rack/Dockerfile similarity index 82% rename from features/fixtures/rack2/Dockerfile rename to features/fixtures/rack/Dockerfile index a485c56c1..5083cde99 100644 --- a/features/fixtures/rack2/Dockerfile +++ b/features/fixtures/rack/Dockerfile @@ -6,6 +6,10 @@ COPY temp-bugsnag-lib ./ WORKDIR /usr/src/app COPY app/Gemfile /usr/src/app/ + +ARG RACK_VERSION +ENV RACK_VERSION $RACK_VERSION + RUN bundle install COPY app/ /usr/src/app diff --git a/features/fixtures/rack/app/Gemfile b/features/fixtures/rack/app/Gemfile new file mode 100644 index 000000000..1f1fa5fa3 --- /dev/null +++ b/features/fixtures/rack/app/Gemfile @@ -0,0 +1,11 @@ +source 'https://rubygems.org' + +gem 'bugsnag', path: '/bugsnag' +gem 'rack', "~> #{ENV['RACK_VERSION']}" +gem 'webrick' if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('3.0.0') + +# Some functionality provided by Rack was moved to the 'rackup' gem in Rack v3 +# Specifically the test app uses Rack::Server, which is now Rackup::Server +if ENV['RACK_VERSION'] == '3' + gem 'rackup', '~> 0.2.3' +end diff --git a/features/fixtures/rack/app/app.rb b/features/fixtures/rack/app/app.rb new file mode 100644 index 000000000..847c7435f --- /dev/null +++ b/features/fixtures/rack/app/app.rb @@ -0,0 +1,74 @@ +require 'bugsnag' +require 'rack' +require 'json' + +$clear_all_flags = false + +Bugsnag.configure do |config| + config.api_key = ENV['BUGSNAG_API_KEY'] + config.endpoint = ENV['BUGSNAG_ENDPOINT'] + + if ENV.key?('BUGSNAG_METADATA_FILTERS') + config.meta_data_filters = JSON.parse(ENV['BUGSNAG_METADATA_FILTERS']) + end + + config.add_on_error(proc do |event| + event.add_feature_flags([ + Bugsnag::FeatureFlag.new('from config 1'), + Bugsnag::FeatureFlag.new('from config 2', 'abc xyz'), + ]) + + event.clear_feature_flag('should be removed!') + + if $clear_all_flags + event.clear_feature_flags + end + end) +end + +class BugsnagTests + def call(env) + req = Rack::Request.new(env) + + $clear_all_flags = !!req.params['clear_all_flags'] + + case req.env['REQUEST_PATH'] + when '/unhandled' + raise 'Unhandled error' + when '/handled' + begin + raise 'Handled error' + rescue StandardError => e + Bugsnag.notify(e) + end + when '/feature-flags/unhandled' + Bugsnag.add_feature_flag('a', '1') + + Bugsnag.add_feature_flags([ + Bugsnag::FeatureFlag.new('b'), + Bugsnag::FeatureFlag.new('c', '3'), + ]) + + Bugsnag.add_feature_flag('d') + Bugsnag.add_feature_flag('should be removed!') + + raise 'Unhandled error' + when '/feature-flags/handled' + Bugsnag.add_feature_flag('x') + + Bugsnag.add_feature_flags([ + Bugsnag::FeatureFlag.new('y', '1234'), + Bugsnag::FeatureFlag.new('z'), + ]) + + Bugsnag.add_feature_flag('should be removed!') + + Bugsnag.notify(RuntimeError.new('oh no')) + end + + res = Rack::Response.new + res.finish + end +end + +Rack::Server.start(app: Bugsnag::Rack.new(BugsnagTests.new), Host: '0.0.0.0', Port: 3000) diff --git a/features/fixtures/rack1/Dockerfile b/features/fixtures/rack1/Dockerfile deleted file mode 100644 index a485c56c1..000000000 --- a/features/fixtures/rack1/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -ARG RUBY_TEST_VERSION -FROM ruby:$RUBY_TEST_VERSION - -WORKDIR /bugsnag -COPY temp-bugsnag-lib ./ - -WORKDIR /usr/src/app -COPY app/Gemfile /usr/src/app/ -RUN bundle install - -COPY app/ /usr/src/app - -CMD ["bundle", "exec", "ruby", "app.rb"] diff --git a/features/fixtures/rack1/app/Gemfile b/features/fixtures/rack1/app/Gemfile deleted file mode 100644 index 58bbd2883..000000000 --- a/features/fixtures/rack1/app/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -source 'https://rubygems.org' - -gem 'bugsnag', path: '/bugsnag' -gem 'rack', '~> 1' -gem 'webrick' if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('3.0.0') diff --git a/features/fixtures/rack1/app/app.rb b/features/fixtures/rack1/app/app.rb deleted file mode 100644 index 41d0f2eca..000000000 --- a/features/fixtures/rack1/app/app.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'bugsnag' -require 'rack' -require 'json' - -Bugsnag.configure do |config| - config.api_key = ENV['BUGSNAG_API_KEY'] - config.endpoint = ENV['BUGSNAG_ENDPOINT'] - - if ENV.key?('BUGSNAG_METADATA_FILTERS') - config.meta_data_filters = JSON.parse(ENV['BUGSNAG_METADATA_FILTERS']) - end -end - -class BugsnagTests - def call(env) - req = Rack::Request.new(env) - - case req.env['REQUEST_PATH'] - when '/unhandled' - raise 'Unhandled error' - when '/handled' - begin - raise 'Handled error' - rescue StandardError => e - Bugsnag.notify(e) - end - end - - res = Rack::Response.new - res.finish - end -end - -Rack::Server.start(app: Bugsnag::Rack.new(BugsnagTests.new), Host: '0.0.0.0', Port: 3000) diff --git a/features/fixtures/rack2/.dockerignore b/features/fixtures/rack2/.dockerignore deleted file mode 100644 index 87a58ba13..000000000 --- a/features/fixtures/rack2/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -# Ignore the lock file so that we always use an up to date version. This won't -# exist unless the fixtures are run manually anyway. -app/Gemfile.lock diff --git a/features/fixtures/rack2/app/Gemfile b/features/fixtures/rack2/app/Gemfile deleted file mode 100644 index 3d2908859..000000000 --- a/features/fixtures/rack2/app/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -source 'https://rubygems.org' - -gem 'bugsnag', path: '/bugsnag' -gem 'rack', '~> 2' -gem 'webrick' if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('3.0.0') diff --git a/features/fixtures/rack2/app/app.rb b/features/fixtures/rack2/app/app.rb deleted file mode 100644 index 41d0f2eca..000000000 --- a/features/fixtures/rack2/app/app.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'bugsnag' -require 'rack' -require 'json' - -Bugsnag.configure do |config| - config.api_key = ENV['BUGSNAG_API_KEY'] - config.endpoint = ENV['BUGSNAG_ENDPOINT'] - - if ENV.key?('BUGSNAG_METADATA_FILTERS') - config.meta_data_filters = JSON.parse(ENV['BUGSNAG_METADATA_FILTERS']) - end -end - -class BugsnagTests - def call(env) - req = Rack::Request.new(env) - - case req.env['REQUEST_PATH'] - when '/unhandled' - raise 'Unhandled error' - when '/handled' - begin - raise 'Handled error' - rescue StandardError => e - Bugsnag.notify(e) - end - end - - res = Rack::Response.new - res.finish - end -end - -Rack::Server.start(app: Bugsnag::Rack.new(BugsnagTests.new), Host: '0.0.0.0', Port: 3000) diff --git a/features/fixtures/rails3/app/app/controllers/feature_flags_controller.rb b/features/fixtures/rails3/app/app/controllers/feature_flags_controller.rb new file mode 100644 index 000000000..eaab63b1e --- /dev/null +++ b/features/fixtures/rails3/app/app/controllers/feature_flags_controller.rb @@ -0,0 +1,33 @@ +class FeatureFlagsController < ActionController::Base + protect_from_forgery + + before_bugsnag_notify :add_feature_flags + + def unhandled + Bugsnag.add_feature_flag('unhandled') + + raise 'oh no' + end + + def handled + Bugsnag.add_feature_flag('handled') + + Bugsnag.notify(RuntimeError.new('ahhh')) + + render json: {} + end + + private + + def add_feature_flags(event) + params['flags'].each do |key, value| + event.add_feature_flag(key, value) + end + + if params.key?('clear_all_flags') + event.add_metadata(:clear_all_flags, :a, 1) + else + event.clear_feature_flag('should be removed!') + end + end +end diff --git a/features/fixtures/rails3/app/config/initializers/bugsnag.rb b/features/fixtures/rails3/app/config/initializers/bugsnag.rb index ca5b93ca7..0aabfa4c8 100644 --- a/features/fixtures/rails3/app/config/initializers/bugsnag.rb +++ b/features/fixtures/rails3/app/config/initializers/bugsnag.rb @@ -1,7 +1,7 @@ Bugsnag.configure do |config| config.api_key = ENV["BUGSNAG_API_KEY"] || ENV["BUGSNAG_API_KEY"] config.endpoint = ENV["BUGSNAG_ENDPOINT"] || ENV["BUGSNAG_ENDPOINT"] - config.session_endpoint = ENV["BUGSNAG_ENDPOINT"] || ENV["BUGSNAG_ENDPOINT"] + config.session_endpoint = ENV["BUGSNAG_SESSION_ENDPOINT"] || ENV["BUGSNAG_SESSION_ENDPOINT"] config.app_type = ENV["BUGSNAG_APP_TYPE"] if ENV.include? "BUGSNAG_APP_TYPE" config.app_version = ENV["BUGSNAG_APP_VERSION"] if ENV.include? "BUGSNAG_APP_VERSION" config.auto_notify = ENV["BUGSNAG_AUTO_NOTIFY"] != "false" @@ -32,4 +32,15 @@ report.request[:params][:another_thing] = "hi" end) end + + config.add_on_error(proc do |event| + event.add_feature_flags([ + Bugsnag::FeatureFlag.new('from config 1'), + Bugsnag::FeatureFlag.new('from config 2', 'abc xyz'), + ]) + + if event.metadata.key?(:clear_all_flags) + event.clear_feature_flags + end + end) end diff --git a/features/fixtures/rails3/app/config/routes.rb b/features/fixtures/rails3/app/config/routes.rb index 4c1e0dfee..8b2c8769e 100644 --- a/features/fixtures/rails3/app/config/routes.rb +++ b/features/fixtures/rails3/app/config/routes.rb @@ -15,5 +15,6 @@ get "/send_environment/(:action)", controller: 'send_environment' get "/warden/(:action)", controller: 'warden' get "/breadcrumbs/(:action)", controller: 'breadcrumbs' + get "/features/(:action)", controller: 'feature_flags' get "/(:action)", controller: 'application' end diff --git a/features/fixtures/rails4/app/app/controllers/feature_flags_controller.rb b/features/fixtures/rails4/app/app/controllers/feature_flags_controller.rb new file mode 100644 index 000000000..eaab63b1e --- /dev/null +++ b/features/fixtures/rails4/app/app/controllers/feature_flags_controller.rb @@ -0,0 +1,33 @@ +class FeatureFlagsController < ActionController::Base + protect_from_forgery + + before_bugsnag_notify :add_feature_flags + + def unhandled + Bugsnag.add_feature_flag('unhandled') + + raise 'oh no' + end + + def handled + Bugsnag.add_feature_flag('handled') + + Bugsnag.notify(RuntimeError.new('ahhh')) + + render json: {} + end + + private + + def add_feature_flags(event) + params['flags'].each do |key, value| + event.add_feature_flag(key, value) + end + + if params.key?('clear_all_flags') + event.add_metadata(:clear_all_flags, :a, 1) + else + event.clear_feature_flag('should be removed!') + end + end +end diff --git a/features/fixtures/rails4/app/config/initializers/bugsnag.rb b/features/fixtures/rails4/app/config/initializers/bugsnag.rb index ca5b93ca7..0aabfa4c8 100644 --- a/features/fixtures/rails4/app/config/initializers/bugsnag.rb +++ b/features/fixtures/rails4/app/config/initializers/bugsnag.rb @@ -1,7 +1,7 @@ Bugsnag.configure do |config| config.api_key = ENV["BUGSNAG_API_KEY"] || ENV["BUGSNAG_API_KEY"] config.endpoint = ENV["BUGSNAG_ENDPOINT"] || ENV["BUGSNAG_ENDPOINT"] - config.session_endpoint = ENV["BUGSNAG_ENDPOINT"] || ENV["BUGSNAG_ENDPOINT"] + config.session_endpoint = ENV["BUGSNAG_SESSION_ENDPOINT"] || ENV["BUGSNAG_SESSION_ENDPOINT"] config.app_type = ENV["BUGSNAG_APP_TYPE"] if ENV.include? "BUGSNAG_APP_TYPE" config.app_version = ENV["BUGSNAG_APP_VERSION"] if ENV.include? "BUGSNAG_APP_VERSION" config.auto_notify = ENV["BUGSNAG_AUTO_NOTIFY"] != "false" @@ -32,4 +32,15 @@ report.request[:params][:another_thing] = "hi" end) end + + config.add_on_error(proc do |event| + event.add_feature_flags([ + Bugsnag::FeatureFlag.new('from config 1'), + Bugsnag::FeatureFlag.new('from config 2', 'abc xyz'), + ]) + + if event.metadata.key?(:clear_all_flags) + event.clear_feature_flags + end + end) end diff --git a/features/fixtures/rails4/app/config/routes.rb b/features/fixtures/rails4/app/config/routes.rb index 8fee25c36..970a02a67 100644 --- a/features/fixtures/rails4/app/config/routes.rb +++ b/features/fixtures/rails4/app/config/routes.rb @@ -19,4 +19,5 @@ get "/breadcrumbs/(:action)", controller: 'breadcrumbs' get "/mongo/(:action)", controller: 'mongo' get "/active_job/(:action)", controller: 'active_job' + get "/features/(:action)", controller: 'feature_flags' end diff --git a/features/fixtures/rails5/app/app/controllers/feature_flags_controller.rb b/features/fixtures/rails5/app/app/controllers/feature_flags_controller.rb new file mode 100644 index 000000000..1433d5867 --- /dev/null +++ b/features/fixtures/rails5/app/app/controllers/feature_flags_controller.rb @@ -0,0 +1,31 @@ +class FeatureFlagsController < ActionController::Base + protect_from_forgery + + before_bugsnag_notify :add_feature_flags + + def unhandled + Bugsnag.add_feature_flag('unhandled') + + raise 'oh no' + end + + def handled + Bugsnag.add_feature_flag('handled') + + Bugsnag.notify(RuntimeError.new('ahhh')) + end + + private + + def add_feature_flags(event) + params['flags'].each do |key, value| + event.add_feature_flag(key, value) + end + + if params.key?('clear_all_flags') + event.add_metadata(:clear_all_flags, :a, 1) + else + event.clear_feature_flag('should be removed!') + end + end +end diff --git a/features/fixtures/rails5/app/config/initializers/bugsnag.rb b/features/fixtures/rails5/app/config/initializers/bugsnag.rb index 258d13b59..5d26472f9 100644 --- a/features/fixtures/rails5/app/config/initializers/bugsnag.rb +++ b/features/fixtures/rails5/app/config/initializers/bugsnag.rb @@ -1,7 +1,7 @@ Bugsnag.configure do |config| config.api_key = ENV["BUGSNAG_API_KEY"] || ENV["BUGSNAG_API_KEY"] config.endpoint = ENV["BUGSNAG_ENDPOINT"] || ENV["BUGSNAG_ENDPOINT"] - config.session_endpoint = ENV["BUGSNAG_ENDPOINT"] || ENV["BUGSNAG_ENDPOINT"] + config.session_endpoint = ENV["BUGSNAG_SESSION_ENDPOINT"] || ENV["BUGSNAG_SESSION_ENDPOINT"] config.app_type = ENV["BUGSNAG_APP_TYPE"] if ENV.include? "BUGSNAG_APP_TYPE" config.app_version = ENV["BUGSNAG_APP_VERSION"] if ENV.include? "BUGSNAG_APP_VERSION" config.auto_notify = ENV["BUGSNAG_AUTO_NOTIFY"] != "false" @@ -32,4 +32,15 @@ report.request[:params][:another_thing] = "hi" end) end + + config.add_on_error(proc do |event| + event.add_feature_flags([ + Bugsnag::FeatureFlag.new('from config 1'), + Bugsnag::FeatureFlag.new('from config 2', 'abc xyz'), + ]) + + if event.metadata.key?(:clear_all_flags) + event.clear_feature_flags + end + end) end diff --git a/features/fixtures/rails5/app/config/routes.rb b/features/fixtures/rails5/app/config/routes.rb index f153a9eda..3c0f75436 100644 --- a/features/fixtures/rails5/app/config/routes.rb +++ b/features/fixtures/rails5/app/config/routes.rb @@ -65,4 +65,7 @@ get 'active_job/handled', to: 'active_job#handled' get 'active_job/unhandled', to: 'active_job#unhandled' + + get 'features/handled', to: 'feature_flags#handled' + get 'features/unhandled', to: 'feature_flags#unhandled' end diff --git a/features/fixtures/rails6/app/app/controllers/feature_flags_controller.rb b/features/fixtures/rails6/app/app/controllers/feature_flags_controller.rb new file mode 100644 index 000000000..1433d5867 --- /dev/null +++ b/features/fixtures/rails6/app/app/controllers/feature_flags_controller.rb @@ -0,0 +1,31 @@ +class FeatureFlagsController < ActionController::Base + protect_from_forgery + + before_bugsnag_notify :add_feature_flags + + def unhandled + Bugsnag.add_feature_flag('unhandled') + + raise 'oh no' + end + + def handled + Bugsnag.add_feature_flag('handled') + + Bugsnag.notify(RuntimeError.new('ahhh')) + end + + private + + def add_feature_flags(event) + params['flags'].each do |key, value| + event.add_feature_flag(key, value) + end + + if params.key?('clear_all_flags') + event.add_metadata(:clear_all_flags, :a, 1) + else + event.clear_feature_flag('should be removed!') + end + end +end diff --git a/features/fixtures/rails6/app/config/environments/rails_env.rb b/features/fixtures/rails6/app/config/environments/rails_env.rb index 2c38dc193..c3aef279f 100644 --- a/features/fixtures/rails6/app/config/environments/rails_env.rb +++ b/features/fixtures/rails6/app/config/environments/rails_env.rb @@ -53,4 +53,5 @@ config.file_watcher = ActiveSupport::EventedFileUpdateChecker config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } config.hosts << "rails6" + config.hosts << "localhost" end diff --git a/features/fixtures/rails6/app/config/initializers/bugsnag.rb b/features/fixtures/rails6/app/config/initializers/bugsnag.rb index ca5b93ca7..0aabfa4c8 100644 --- a/features/fixtures/rails6/app/config/initializers/bugsnag.rb +++ b/features/fixtures/rails6/app/config/initializers/bugsnag.rb @@ -1,7 +1,7 @@ Bugsnag.configure do |config| config.api_key = ENV["BUGSNAG_API_KEY"] || ENV["BUGSNAG_API_KEY"] config.endpoint = ENV["BUGSNAG_ENDPOINT"] || ENV["BUGSNAG_ENDPOINT"] - config.session_endpoint = ENV["BUGSNAG_ENDPOINT"] || ENV["BUGSNAG_ENDPOINT"] + config.session_endpoint = ENV["BUGSNAG_SESSION_ENDPOINT"] || ENV["BUGSNAG_SESSION_ENDPOINT"] config.app_type = ENV["BUGSNAG_APP_TYPE"] if ENV.include? "BUGSNAG_APP_TYPE" config.app_version = ENV["BUGSNAG_APP_VERSION"] if ENV.include? "BUGSNAG_APP_VERSION" config.auto_notify = ENV["BUGSNAG_AUTO_NOTIFY"] != "false" @@ -32,4 +32,15 @@ report.request[:params][:another_thing] = "hi" end) end + + config.add_on_error(proc do |event| + event.add_feature_flags([ + Bugsnag::FeatureFlag.new('from config 1'), + Bugsnag::FeatureFlag.new('from config 2', 'abc xyz'), + ]) + + if event.metadata.key?(:clear_all_flags) + event.clear_feature_flags + end + end) end diff --git a/features/fixtures/rails6/app/config/routes.rb b/features/fixtures/rails6/app/config/routes.rb index f153a9eda..3c0f75436 100644 --- a/features/fixtures/rails6/app/config/routes.rb +++ b/features/fixtures/rails6/app/config/routes.rb @@ -65,4 +65,7 @@ get 'active_job/handled', to: 'active_job#handled' get 'active_job/unhandled', to: 'active_job#unhandled' + + get 'features/handled', to: 'feature_flags#handled' + get 'features/unhandled', to: 'feature_flags#unhandled' end diff --git a/features/fixtures/rails7/app/app/controllers/feature_flags_controller.rb b/features/fixtures/rails7/app/app/controllers/feature_flags_controller.rb new file mode 100644 index 000000000..1433d5867 --- /dev/null +++ b/features/fixtures/rails7/app/app/controllers/feature_flags_controller.rb @@ -0,0 +1,31 @@ +class FeatureFlagsController < ActionController::Base + protect_from_forgery + + before_bugsnag_notify :add_feature_flags + + def unhandled + Bugsnag.add_feature_flag('unhandled') + + raise 'oh no' + end + + def handled + Bugsnag.add_feature_flag('handled') + + Bugsnag.notify(RuntimeError.new('ahhh')) + end + + private + + def add_feature_flags(event) + params['flags'].each do |key, value| + event.add_feature_flag(key, value) + end + + if params.key?('clear_all_flags') + event.add_metadata(:clear_all_flags, :a, 1) + else + event.clear_feature_flag('should be removed!') + end + end +end diff --git a/features/fixtures/rails7/app/config/initializers/bugsnag.rb b/features/fixtures/rails7/app/config/initializers/bugsnag.rb index e66cd1714..a7079f0ad 100644 --- a/features/fixtures/rails7/app/config/initializers/bugsnag.rb +++ b/features/fixtures/rails7/app/config/initializers/bugsnag.rb @@ -1,7 +1,7 @@ Bugsnag.configure do |config| config.api_key = ENV["BUGSNAG_API_KEY"] config.endpoint = ENV["BUGSNAG_ENDPOINT"] - config.session_endpoint = ENV["BUGSNAG_ENDPOINT"] + config.session_endpoint = ENV["BUGSNAG_SESSION_ENDPOINT"] config.app_type = ENV["BUGSNAG_APP_TYPE"] if ENV.include? "BUGSNAG_APP_TYPE" config.app_version = ENV["BUGSNAG_APP_VERSION"] if ENV.include? "BUGSNAG_APP_VERSION" config.auto_notify = ENV["BUGSNAG_AUTO_NOTIFY"] != "false" @@ -32,4 +32,15 @@ report.request[:params][:another_thing] = "hi" end) end + + config.add_on_error(proc do |event| + event.add_feature_flags([ + Bugsnag::FeatureFlag.new('from config 1'), + Bugsnag::FeatureFlag.new('from config 2', 'abc xyz'), + ]) + + if event.metadata.key?(:clear_all_flags) + event.clear_feature_flags + end + end) end diff --git a/features/fixtures/rails7/app/config/routes.rb b/features/fixtures/rails7/app/config/routes.rb index f153a9eda..3c0f75436 100644 --- a/features/fixtures/rails7/app/config/routes.rb +++ b/features/fixtures/rails7/app/config/routes.rb @@ -65,4 +65,7 @@ get 'active_job/handled', to: 'active_job#handled' get 'active_job/unhandled', to: 'active_job#unhandled' + + get 'features/handled', to: 'feature_flags#handled' + get 'features/unhandled', to: 'feature_flags#unhandled' end diff --git a/features/fixtures/rails_integrations/app/Gemfile b/features/fixtures/rails_integrations/app/Gemfile index f63d67907..f0daa0f2c 100644 --- a/features/fixtures/rails_integrations/app/Gemfile +++ b/features/fixtures/rails_integrations/app/Gemfile @@ -14,8 +14,8 @@ if RUBY_VERSION >= '3.0.0' gem 'redis-namespace', github: 'resque/redis-namespace', ref: 'c31e63dc3cd5e59ef5ea394d4d46ac60d1e6f82e' end -gem 'resque', '~> 2.0.0' -gem 'sidekiq', '~> 6.1.0' +gem 'resque', '~> 2.0' +gem 'sidekiq', '~> 6.1' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 6.0.3', '>= 6.0.3.2' diff --git a/features/fixtures/rails_integrations/app/config/initializers/bugsnag.rb b/features/fixtures/rails_integrations/app/config/initializers/bugsnag.rb index 38eeba564..d0b4d3107 100644 --- a/features/fixtures/rails_integrations/app/config/initializers/bugsnag.rb +++ b/features/fixtures/rails_integrations/app/config/initializers/bugsnag.rb @@ -1,7 +1,7 @@ Bugsnag.configure do |config| config.api_key = ENV['BUGSNAG_API_KEY'] config.endpoint = ENV['BUGSNAG_ENDPOINT'] - config.session_endpoint = ENV['BUGSNAG_ENDPOINT'] + config.session_endpoint = ENV['BUGSNAG_SESSION_ENDPOINT'] config.add_on_error(proc do |report| report.add_tab(:config, { diff --git a/features/lib/fixture.rb b/features/lib/fixture.rb new file mode 100644 index 000000000..645b9ec8f --- /dev/null +++ b/features/lib/fixture.rb @@ -0,0 +1,96 @@ +class Fixture + def initialize(name, version = nil) + @name = name + @version = version + end + + def docker_service + "#{@name}#{@version}" + end + + def host + if running_in_docker? + docker_service + else + "localhost" + end + end + + def port + # the rails integrations tests run with a version of "_integrations" and + # bind to port 3000 even when running outside of docker + if running_in_docker? || @version == "_integrations" + "3000" + elsif @name == "rack" + "7251" + else + "725#{@version}" + end + end + + def version_matches?(operator, version_to_compare) + @version.to_i.send(operator, version_to_compare) + end + + def uri_for(route) + URI("http://#{host}:#{port}#{route}") + end + + def navigate_to(route, headers = {}) + uri = uri_for(route) + + make_request(uri) do |http| + request = Net::HTTP::Get.new(uri.request_uri) + + headers.each do |key, value| + request[key] = value + end + + request + end + end + + def post_form(route, form_data) + uri = uri_for(route) + + make_request(uri) do |http| + request = Net::HTTP::Post.new(uri) + request.set_form_data(form_data) + + request + end + end + + def post_json(route, json_data) + uri = uri_for(route) + + make_request(uri) do |http| + request = Net::HTTP::Post.new(uri) + request.body = JSON.generate(json_data) + request["Content-Type"] = "application/json" + + request + end + end + + private + + def make_request(uri, &block) + attempts = 0 + + begin + Net::HTTP.start(uri.host, uri.port) do |http| + request = block.call(http) + + http.request(request) + end + rescue => e + raise e if attempts > 10 + + attempts += 1 + sleep 1 + + retry + end + end +end diff --git a/features/mailman.feature b/features/mailman.feature index 60814d2f7..12b4fe307 100644 --- a/features/mailman.feature +++ b/features/mailman.feature @@ -4,8 +4,8 @@ Scenario: An unhandled RuntimeError sends a report Given I set environment variable "TARGET_EMAIL" to "emails/unhandled_error.eml" And I set environment variable "APP_PATH" to "/usr/src" And I start the service "mailman" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is true And the event "severity" equals "error" And the event "severityReason.type" equals "unhandledExceptionMiddleware" @@ -17,8 +17,8 @@ Scenario: A handled RuntimeError sends a report Given I set environment variable "TARGET_EMAIL" to "emails/handled_error.eml" And I set environment variable "APP_PATH" to "/usr/src" And I start the service "mailman" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is false And the event "severity" equals "warning" And the event "severityReason.type" equals "handledException" diff --git a/features/plain_features/add_tab.feature b/features/plain_features/add_tab.feature index 6e2218b8a..d9c73ab79 100644 --- a/features/plain_features/add_tab.feature +++ b/features/plain_features/add_tab.feature @@ -3,8 +3,8 @@ Feature: Plain add tab to metadata Scenario Outline: Metadata can be added to a report using add_tab Given I set environment variable "CALLBACK_INITIATOR" to "" When I run the service "plain-ruby" with the command "bundle exec ruby report_modification/add_tab.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "metaData.additional_metadata.foo" equals "foo" And the event "metaData.additional_metadata.bar.0" equals "b" And the event "metaData.additional_metadata.bar.1" equals "a" @@ -21,8 +21,8 @@ Scenario Outline: Metadata can be added to a report using add_tab Scenario Outline: Metadata can be added to an existing tab using add_tab Given I set environment variable "CALLBACK_INITIATOR" to "" When I run the service "plain-ruby" with the command "bundle exec ruby report_modification/add_tab_existing.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "metaData.additional_metadata.foo" equals "foo" And the event "metaData.additional_metadata.bar.0" equals "b" And the event "metaData.additional_metadata.bar.1" equals "a" @@ -41,8 +41,8 @@ Scenario Outline: Metadata can be added to an existing tab using add_tab Scenario Outline: Metadata can be overwritten using add_tab Given I set environment variable "CALLBACK_INITIATOR" to "" When I run the service "plain-ruby" with the command "bundle exec ruby report_modification/add_tab_override.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "metaData.additional_metadata.foo" equals "foo" And the event "metaData.additional_metadata.bar" equals "bar" diff --git a/features/plain_features/app_type.feature b/features/plain_features/app_type.feature index a10228d84..60efcd3e0 100644 --- a/features/plain_features/app_type.feature +++ b/features/plain_features/app_type.feature @@ -3,6 +3,6 @@ Feature: App type configuration option Scenario: The App type configuration option can be set Given I set environment variable "BUGSNAG_APP_TYPE" to "test_app" When I run the service "plain-ruby" with the command "bundle exec ruby configuration/send_handled.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" - And the event "app.type" equals "test_app" \ No newline at end of file + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier + And the event "app.type" equals "test_app" diff --git a/features/plain_features/app_version.feature b/features/plain_features/app_version.feature index aba6ec692..af70a93fd 100644 --- a/features/plain_features/app_version.feature +++ b/features/plain_features/app_version.feature @@ -3,6 +3,6 @@ Feature: App version configuration option Scenario: The App version configuration option can be set Given I set environment variable "BUGSNAG_APP_VERSION" to "9.9.8" When I run the service "plain-ruby" with the command "bundle exec ruby configuration/send_handled.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" - And the event "app.version" equals "9.9.8" \ No newline at end of file + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier + And the event "app.version" equals "9.9.8" diff --git a/features/plain_features/auto_notify.feature b/features/plain_features/auto_notify.feature index fe025d367..f366e4dcf 100644 --- a/features/plain_features/auto_notify.feature +++ b/features/plain_features/auto_notify.feature @@ -3,5 +3,4 @@ Feature: Auto notify configuration option Scenario: When Auto-notify is false notifications are not sent Given I set environment variable "BUGSNAG_AUTO_NOTIFY" to "false" When I run the service "plain-ruby" with the command "bundle exec ruby configuration/send_unhandled.rb" - And I wait for 1 second Then I should receive no requests diff --git a/features/plain_features/delivery.feature b/features/plain_features/delivery.feature index 57f2f52d9..a4a92cb6e 100644 --- a/features/plain_features/delivery.feature +++ b/features/plain_features/delivery.feature @@ -2,19 +2,19 @@ Feature: delivery_method configuration option Scenario: When the delivery_method is set to :synchronous When I run the service "plain-ruby" with the command "bundle exec ruby delivery/synchronous.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "metaData.config" matches the JSON fixture in "features/fixtures/plain/json/delivery_synchronous.json" Scenario: When the delivery_method is set to :thread_queue When I run the service "plain-ruby" with the command "bundle exec ruby delivery/threadpool.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "metaData.config" matches the JSON fixture in "features/fixtures/plain/json/delivery_threadpool.json" Scenario: When the delivery_method is set to :thread_queue in a fork When I run the service "plain-ruby" with the command "bundle exec ruby delivery/fork_threadpool.rb" - And I wait to receive 2 requests - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive 2 errors + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the exception "errorClass" equals "RuntimeError" And the event "metaData.config" matches the JSON fixture in "features/fixtures/plain/json/delivery_fork.json" diff --git a/features/plain_features/exception_data.feature b/features/plain_features/exception_data.feature index 3f16b9e8a..66db41f7b 100644 --- a/features/plain_features/exception_data.feature +++ b/features/plain_features/exception_data.feature @@ -2,8 +2,8 @@ Feature: Plain exception data Scenario Outline: An error has built in meta-data When I run the service "plain-ruby" with the command "bundle exec ruby exception_data/_meta_data.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the exception "errorClass" equals "CustomError" And the event "metaData.exception.exception_type" equals "FATAL" And the event "metaData.exception.exception_base" equals "RuntimeError" @@ -15,8 +15,8 @@ Scenario Outline: An error has built in meta-data Scenario Outline: An error has built in context When I run the service "plain-ruby" with the command "bundle exec ruby exception_data/_context.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the exception "errorClass" equals "CustomError" And the event "context" equals "IntegrationTests" @@ -27,8 +27,8 @@ Scenario Outline: An error has built in context Scenario Outline: An error has built in grouping hash When I run the service "plain-ruby" with the command "bundle exec ruby exception_data/_hash.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the exception "errorClass" equals "CustomError" And the event "groupingHash" equals "ABCDE12345" @@ -39,8 +39,8 @@ Scenario Outline: An error has built in grouping hash Scenario Outline: An error has built in user id When I run the service "plain-ruby" with the command "bundle exec ruby exception_data/_user_id.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the exception "errorClass" equals "CustomError" And the event "user.id" equals "000001" diff --git a/features/plain_features/filters.feature b/features/plain_features/filters.feature index bedf87fa5..ffe215922 100644 --- a/features/plain_features/filters.feature +++ b/features/plain_features/filters.feature @@ -2,20 +2,20 @@ Feature: Plain filtering of metadata Scenario: Metadata is filtered through the default filters When I run the service "plain-ruby" with the command "bundle exec ruby filters/default_filters.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "metaData.filter" matches the JSON fixture in "features/fixtures/plain/json/filters_default_metadata_filters.json" Scenario: Additional filters can be added to the filter list Given I set environment variable "BUGSNAG_META_DATA_FILTERS" to "filter_me" When I run the service "plain-ruby" with the command "bundle exec ruby filters/additional_filters.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "metaData.filter.filter_me" equals "[FILTERED]" Scenario: Redacted keys can also be used to filter sensitive data Given I set environment variable "BUGSNAG_REDACTED_KEYS" to "filter_me" When I run the service "plain-ruby" with the command "bundle exec ruby filters/additional_filters.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "metaData.filter.filter_me" equals "[FILTERED]" diff --git a/features/plain_features/handled_errors.feature b/features/plain_features/handled_errors.feature index ff97e32e9..3fdcc3f92 100644 --- a/features/plain_features/handled_errors.feature +++ b/features/plain_features/handled_errors.feature @@ -2,8 +2,8 @@ Feature: Plain handled errors Scenario: A rescued exception sends a report When I run the service "plain-ruby" with the command "bundle exec ruby handled/notify_exception.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is false And the event "severity" equals "warning" And the event "severityReason.type" equals "handledException" @@ -14,8 +14,8 @@ Scenario: A rescued exception sends a report Scenario: A notified string sends a report When I run the service "plain-ruby" with the command "bundle exec ruby handled/notify_string.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is false And the event "severity" equals "warning" And the event "severityReason.type" equals "handledException" @@ -26,13 +26,12 @@ Scenario: A notified string sends a report Scenario: A handled error doesn't send a report when the :skip_bugsnag flag is set When I run the service "plain-ruby" with the command "bundle exec ruby handled/ignore_exception.rb" - And I wait for 1 second Then I should receive no requests Scenario: A handled error can attach metadata in a block When I run the service "plain-ruby" with the command "bundle exec ruby handled/block_metadata.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is false And the event "severity" equals "warning" And the event "severityReason.type" equals "handledException" diff --git a/features/plain_features/ignore_classes.feature b/features/plain_features/ignore_classes.feature index 83c9dd86d..239f1062a 100644 --- a/features/plain_features/ignore_classes.feature +++ b/features/plain_features/ignore_classes.feature @@ -2,7 +2,6 @@ Feature: Plain ignore classes Scenario Outline: An errors class is in the ignore_classes array When I run the service "plain-ruby" with the command "bundle exec ruby ignore_classes/.rb" - And I wait for 1 second Then I should receive no requests Examples: diff --git a/features/plain_features/ignore_report.feature b/features/plain_features/ignore_report.feature index c5c837829..584723f71 100644 --- a/features/plain_features/ignore_report.feature +++ b/features/plain_features/ignore_report.feature @@ -3,7 +3,6 @@ Feature: Plain ignore report Scenario Outline: A reports severity can be modified Given I set environment variable "CALLBACK_INITIATOR" to "" When I run the service "plain-ruby" with the command "bundle exec ruby report_modification/ignore_report.rb" - And I wait for 1 second Then I should receive no requests Examples: diff --git a/features/plain_features/proxies.feature b/features/plain_features/proxies.feature index 496adea2b..5eacdef5b 100644 --- a/features/plain_features/proxies.feature +++ b/features/plain_features/proxies.feature @@ -1,25 +1,22 @@ Feature: proxy configuration options Scenario: Proxy settings are provided as configuration options - When I set environment variable "BUGSNAG_PROXY_HOST" to "maze-runner" - And I set environment variable "BUGSNAG_PROXY_PORT" to "9339" - And I set environment variable "BUGSNAG_PROXY_USER" to "tester" - And I set environment variable "BUGSNAG_PROXY_PASSWORD" to "testpass" + Given I configure the BUGSNAG_PROXY environment variables When I run the service "plain-ruby" with the command "bundle exec ruby configuration/proxy.rb" - Then I wait to receive a request - And the "proxy-authorization" header equals "Basic dGVzdGVyOnRlc3RwYXNz" + Then I wait to receive an error + And the error "proxy-authorization" header equals "Basic dGVzdGVyOnRlc3RwYXNz" And the event "metaData.proxy.user" equals "tester" Scenario: Proxy settings are provided as the HTTP_PROXY environment variable - Given I set environment variable "http_proxy" to "http://tester:testpass@maze-runner:9339" + Given I configure the http_proxy environment variable When I run the service "plain-ruby" with the command "bundle exec ruby configuration/proxy.rb" - Then I wait to receive a request - And the "proxy-authorization" header equals "Basic dGVzdGVyOnRlc3RwYXNz" + Then I wait to receive an error + And the error "proxy-authorization" header equals "Basic dGVzdGVyOnRlc3RwYXNz" And the event "metaData.proxy.user" equals "tester" Scenario: Proxy settings are provided as the HTTPS_PROXY environment variable - Given I set environment variable "https_proxy" to "http://tester:testpass@maze-runner:9339" + Given I configure the https_proxy environment variable When I run the service "plain-ruby" with the command "bundle exec ruby configuration/proxy.rb" - Then I wait to receive a request - And the "proxy-authorization" header equals "Basic dGVzdGVyOnRlc3RwYXNz" + Then I wait to receive an error + And the error "proxy-authorization" header equals "Basic dGVzdGVyOnRlc3RwYXNz" And the event "metaData.proxy.user" equals "tester" diff --git a/features/plain_features/release_stages.feature b/features/plain_features/release_stages.feature index a9854f505..d48fe340e 100644 --- a/features/plain_features/release_stages.feature +++ b/features/plain_features/release_stages.feature @@ -4,15 +4,14 @@ Scenario: Doesn't notify in the wrong release stage Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGE" to "stage_one" And I set environment variable "BUGSNAG_RELEASE_STAGE" to "stage_two" When I run the service "plain-ruby" with the command "bundle exec ruby configuration/send_unhandled.rb" - And I wait for 1 second Then I should receive no requests Scenario: Does notify in the correct release stage Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGE" to "stage_one" And I set environment variable "BUGSNAG_RELEASE_STAGE" to "stage_one" When I run the service "plain-ruby" with the command "bundle exec ruby configuration/send_unhandled.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is true And the event "severity" equals "error" And the event "severityReason.type" equals "unhandledException" diff --git a/features/plain_features/report_api_key.feature b/features/plain_features/report_api_key.feature index 6821c6125..ff36392fe 100644 --- a/features/plain_features/report_api_key.feature +++ b/features/plain_features/report_api_key.feature @@ -3,9 +3,9 @@ Feature: Plain report modify api key Scenario Outline: A report can have its api_key modified Given I set environment variable "CALLBACK_INITIATOR" to "" When I run the service "plain-ruby" with the command "bundle exec ruby report_modification/modify_api_key.rb" - And I wait to receive a request - Then the "Bugsnag-Api-Key" header equals "abcdefghijklmnopqrstuvwxyz123456" - And the payload field "apiKey" equals "abcdefghijklmnopqrstuvwxyz123456" + And I wait to receive an error + Then the error "Bugsnag-Api-Key" header equals "abcdefghijklmnopqrstuvwxyz123456" + And the error payload field "apiKey" equals "abcdefghijklmnopqrstuvwxyz123456" Examples: | initiator | diff --git a/features/plain_features/report_severity.feature b/features/plain_features/report_severity.feature index c96dc258c..ca7ef57be 100644 --- a/features/plain_features/report_severity.feature +++ b/features/plain_features/report_severity.feature @@ -3,8 +3,8 @@ Feature: Plain report modify severity Scenario Outline: A reports severity can be modified Given I set environment variable "CALLBACK_INITIATOR" to "" When I run the service "plain-ruby" with the command "bundle exec ruby report_modification/modify_severity.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "severity" equals "info" And the event "severityReason.type" equals "userCallbackSetSeverity" diff --git a/features/plain_features/report_stack_frames.feature b/features/plain_features/report_stack_frames.feature index ce34204e1..5569f0222 100644 --- a/features/plain_features/report_stack_frames.feature +++ b/features/plain_features/report_stack_frames.feature @@ -3,8 +3,8 @@ Feature: Plain report modify stack frames Scenario Outline: Stack frames can be removed Given I set environment variable "CALLBACK_INITIATOR" to "" When I run the service "plain-ruby" with the command "bundle exec ruby stack_frame_modification/remove_stack_frame.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the "file" of the top non-bugsnag stackframe equals "/usr/src/app/stack_frame_modification/initiators/.rb" And the "lineNumber" of stack frame 0 equals @@ -18,16 +18,16 @@ Scenario Outline: Stack frames can be removed Scenario: Stack frames can be removed from a notified string Given I set environment variable "CALLBACK_INITIATOR" to "handled_block" When I run the service "plain-ruby" with the command "bundle exec ruby stack_frame_modification/remove_stack_frame.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the "file" of the top non-bugsnag stackframe equals "/usr/src/app/stack_frame_modification/initiators/handled_block.rb" And the "lineNumber" of the top non-bugsnag stackframe equals 19 Scenario Outline: Stack frames can be marked as in project Given I set environment variable "CALLBACK_INITIATOR" to "" When I run the service "plain-ruby" with the command "bundle exec ruby stack_frame_modification/mark_frames_in_project.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the "file" of stack frame 0 equals "/usr/src/app/stack_frame_modification/initiators/.rb" And the event "exceptions.0.stacktrace.0.inProject" is null And the event "exceptions.0.stacktrace.1.inProject" is true @@ -44,8 +44,8 @@ Scenario Outline: Stack frames can be marked as in project Scenario: Stack frames can be marked as in project with a handled string Given I set environment variable "CALLBACK_INITIATOR" to "handled_block" And I run the service "plain-ruby" with the command "bundle exec ruby stack_frame_modification/mark_frames_in_project.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the "file" of the top non-bugsnag stackframe equals "/usr/src/app/stack_frame_modification/initiators/handled_block.rb" And the event "exceptions.0.stacktrace.0.inProject" is null And the event "exceptions.0.stacktrace.1.inProject" is true diff --git a/features/plain_features/report_user.feature b/features/plain_features/report_user.feature index 4f4a34fe5..4ddc5133c 100644 --- a/features/plain_features/report_user.feature +++ b/features/plain_features/report_user.feature @@ -3,8 +3,8 @@ Feature: Plain report modify user Scenario Outline: A report can have a user name, email, and id set Given I set environment variable "CALLBACK_INITIATOR" to "" When I run the service "plain-ruby" with the command "bundle exec ruby report_modification/set_user_details.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "user.name" equals "leo testman" And the event "user.email" equals "test@test.com" And the event "user.id" equals "0001" @@ -20,8 +20,8 @@ Scenario Outline: A report can have a user name, email, and id set Scenario Outline: A report can have custom info set Given I set environment variable "CALLBACK_INITIATOR" to "" And I run the service "plain-ruby" with the command "bundle exec ruby report_modification/set_custom_user_details.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "user.type" equals "amateur" And the event "user.location" equals "testville" And the event "user.details.a" equals "foo" @@ -38,8 +38,8 @@ Scenario Outline: A report can have custom info set Scenario Outline: A report can have its user info removed Given I set environment variable "CALLBACK_INITIATOR" to "" When I run the service "plain-ruby" with the command "bundle exec ruby report_modification/remove_user_details.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "user" is null Examples: diff --git a/features/plain_features/unhandled_errors.feature b/features/plain_features/unhandled_errors.feature index 6bca5787f..f2f075da4 100644 --- a/features/plain_features/unhandled_errors.feature +++ b/features/plain_features/unhandled_errors.feature @@ -2,8 +2,8 @@ Feature: Plain unhandled errors Scenario Outline: An unhandled error sends a report Given I run the service "plain-ruby" with the command " unhandled/.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is true And the event "severity" equals "error" And the event "severityReason.type" equals "unhandledException" @@ -33,7 +33,6 @@ Scenario Outline: An unhandled error sends a report Scenario Outline: An unhandled error doesn't send a report When I run the service "plain-ruby" with the command " unhandled/.rb" - And I wait for 1 second Then I should receive no requests Examples: diff --git a/features/que.feature b/features/que.feature index dbf417651..9e872f7d5 100644 --- a/features/que.feature +++ b/features/que.feature @@ -2,9 +2,9 @@ Feature: Errors are delivered to Bugsnag from Que Scenario: Que will deliver unhandled errors Given I run the service "que" with the command "bundle exec ruby app.rb unhandled" - And I run the service "que" with the command "bundle exec que ./app.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I run the service "que" with the command "timeout 5 bundle exec que ./app.rb" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is true And the event "severity" equals "error" And the event "severityReason.type" equals "unhandledExceptionMiddleware" @@ -15,9 +15,9 @@ Scenario: Que will deliver unhandled errors Scenario: Que will deliver handled errors Given I run the service "que" with the command "bundle exec ruby app.rb handled" - And I run the service "que" with the command "bundle exec que ./app.rb" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I run the service "que" with the command "timeout 5 bundle exec que ./app.rb" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is false And the event "severity" equals "warning" And the event "severityReason.type" equals "handledException" diff --git a/features/rack.feature b/features/rack.feature index b8089d57b..3827dae8c 100644 --- a/features/rack.feature +++ b/features/rack.feature @@ -3,8 +3,8 @@ Feature: Bugsnag raises errors in Rack Scenario: An unhandled RuntimeError sends a report Given I start the rack service When I navigate to the route "/unhandled?a=123&b=456" on the rack app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is true And the event "severity" equals "error" And the event "severityReason.type" equals "unhandledExceptionMiddleware" @@ -16,7 +16,6 @@ Scenario: An unhandled RuntimeError sends a report And the event "metaData.request.cookies" is null And the event "metaData.request.headers.Host" is not null And the event "metaData.request.headers.User-Agent" is not null - And the event "metaData.request.headers.Version" is not null And the event "metaData.request.httpMethod" equals "GET" And the event "metaData.request.httpVersion" matches "^HTTP/\d\.\d$" And the event "metaData.request.params.a" equals "123" @@ -27,8 +26,8 @@ Scenario: An unhandled RuntimeError sends a report Scenario: A handled RuntimeError sends a report Given I start the rack service When I navigate to the route "/handled?a=123&b=456" on the rack app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is false And the event "severity" equals "warning" And the event "severityReason.type" equals "handledException" @@ -39,7 +38,6 @@ Scenario: A handled RuntimeError sends a report And the event "metaData.request.cookies" is null And the event "metaData.request.headers.Host" is not null And the event "metaData.request.headers.User-Agent" is not null - And the event "metaData.request.headers.Version" is not null And the event "metaData.request.httpMethod" equals "GET" And the event "metaData.request.httpVersion" matches "^HTTP/\d\.\d$" And the event "metaData.request.params.a" equals "123" @@ -53,8 +51,8 @@ Scenario: A POST request with form data sends a report with the parsed request b | name | baba | | favourite_letter | z | | password | password1 | - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "metaData.request.body.name" equals "baba" And the event "metaData.request.body.favourite_letter" equals "z" And the event "metaData.request.body.password" equals "[FILTERED]" @@ -62,7 +60,6 @@ Scenario: A POST request with form data sends a report with the parsed request b And the event "metaData.request.cookies" is null And the event "metaData.request.headers.Host" is not null And the event "metaData.request.headers.User-Agent" is not null - And the event "metaData.request.headers.Version" is not null And the event "metaData.request.httpMethod" equals "POST" And the event "metaData.request.httpVersion" matches "^HTTP/\d\.\d$" And the event "metaData.request.params.a" equals "123" @@ -76,8 +73,8 @@ Scenario: A POST request with JSON sends a report with the parsed request body a | name | baba | | favourite_letter | z | | password | password1 | - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "metaData.request.body.name" equals "baba" And the event "metaData.request.body.favourite_letter" equals "z" And the event "metaData.request.body.password" equals "[FILTERED]" @@ -85,7 +82,6 @@ Scenario: A POST request with JSON sends a report with the parsed request body a And the event "metaData.request.cookies" is null And the event "metaData.request.headers.Host" is not null And the event "metaData.request.headers.User-Agent" is not null - And the event "metaData.request.headers.Version" is not null And the event "metaData.request.httpMethod" equals "POST" And the event "metaData.request.httpVersion" matches "^HTTP/\d\.\d$" And the event "metaData.request.params.a" equals "123" @@ -99,14 +95,13 @@ Scenario: A request with cookies will be filtered out by default | a | b | | c | d | | e | f | - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "metaData.request.cookie" is null And the event "metaData.request.headers.Cookie" equals "[FILTERED]" And the event "metaData.request.clientIp" is not null And the event "metaData.request.headers.Host" is not null And the event "metaData.request.headers.User-Agent" is not null - And the event "metaData.request.headers.Version" is not null And the event "metaData.request.httpMethod" equals "GET" And the event "metaData.request.httpVersion" matches "^HTTP/\d\.\d$" And the event "metaData.request.params.a" equals "123" @@ -121,8 +116,8 @@ Scenario: A request with cookies and no matching filter will set cookies in meta | a | b | | c | d | | e | f | - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "metaData.request.cookies.a" equals "b" And the event "metaData.request.cookies.c" equals "d" And the event "metaData.request.cookies.e" equals "f" @@ -130,10 +125,50 @@ Scenario: A request with cookies and no matching filter will set cookies in meta And the event "metaData.request.clientIp" is not null And the event "metaData.request.headers.Host" is not null And the event "metaData.request.headers.User-Agent" is not null - And the event "metaData.request.headers.Version" is not null And the event "metaData.request.httpMethod" equals "GET" And the event "metaData.request.httpVersion" matches "^HTTP/\d\.\d$" And the event "metaData.request.params.a" equals "123" And the event "metaData.request.params.b" equals "456" And the event "metaData.request.referer" is null And the event "metaData.request.url" ends with "/unhandled?a=123&b=456" + +Scenario: adding feature flags for an unhandled error + Given I start the rack service + When I navigate to the route "/feature-flags/unhandled" on the rack app + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier + And the event contains the following feature flags: + | featureFlag | variant | + | from config 1 | | + | from config 2 | abc xyz | + | a | 1 | + | b | | + | c | 3 | + | d | | + + Scenario: adding feature flags for a handled error + Given I start the rack service + When I navigate to the route "/feature-flags/handled" on the rack app + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier + And the event contains the following feature flags: + | featureFlag | variant | + | from config 1 | | + | from config 2 | abc xyz | + | x | | + | y | 1234 | + | z | | + +Scenario: clearing feature flags for an unhandled error + Given I start the rack service + When I navigate to the route "/feature-flags/unhandled?clear_all_flags=1" on the rack app + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier + And the event has no feature flags + + Scenario: clearing feature flags for a handled error + Given I start the rack service + When I navigate to the route "/feature-flags/handled?clear_all_flags=1" on the rack app + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier + And the event has no feature flags diff --git a/features/rails_features/active_job.feature b/features/rails_features/active_job.feature index baef4ee03..49669fe59 100644 --- a/features/rails_features/active_job.feature +++ b/features/rails_features/active_job.feature @@ -4,8 +4,8 @@ Feature: Active Job Scenario: A handled error will be delivered Given I start the rails service When I navigate to the route "/active_job/handled" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is false And the event "severity" equals "warning" And the event "context" equals "NotifyJob@default" @@ -30,8 +30,8 @@ Scenario: A handled error will be delivered Scenario: An unhandled error will be delivered Given I start the rails service When I navigate to the route "/active_job/unhandled" on the rails app - And I wait to receive 2 requests - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive 2 errors + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is true And the event "severity" equals "error" And the event "context" equals "UnhandledJob@default" @@ -51,8 +51,8 @@ Scenario: An unhandled error will be delivered And in Rails versions ">=" 5 the event "metaData.active_job.executions" equals 1 And in Rails versions ">=" 6 the event "metaData.active_job.timezone" equals "UTC" And in Rails versions ">=" 6 the event "metaData.active_job.enqueued_at" is a timestamp - When I discard the oldest request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + When I discard the oldest error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is true And the event "severity" equals "error" And the event "context" equals "UnhandledJob@default" diff --git a/features/rails_features/active_record.feature b/features/rails_features/active_record.feature index e7931f7bc..bed21bd41 100644 --- a/features/rails_features/active_record.feature +++ b/features/rails_features/active_record.feature @@ -4,8 +4,8 @@ Feature: Active Record Scenario: An unhandled error in a transaction callback will be delivered Given I start the rails service When I navigate to the route "/unhandled/error_in_active_record_callback" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is true And the exception "errorClass" equals "RuntimeError" And the exception "message" equals "Oh no!" @@ -19,8 +19,8 @@ Scenario: An unhandled error in a transaction callback will be delivered when ra Given I set environment variable "RAISE_IN_TRANSACTIONAL_CALLBACKS" to "false" And I start the rails service When I navigate to the route "/unhandled/error_in_active_record_callback" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is true And the exception "errorClass" equals "RuntimeError" And the exception "message" equals "Oh no!" diff --git a/features/rails_features/api_key.feature b/features/rails_features/api_key.feature index c4e2d62e6..5cf629fde 100644 --- a/features/rails_features/api_key.feature +++ b/features/rails_features/api_key.feature @@ -4,12 +4,12 @@ Feature: API key Scenario: Setting api_key in environment variable works Given I start the rails service When I navigate to the route "/api_key/environment" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier @rails3 @rails4 @rails5 @rails6 @rails7 Scenario Outline: Changing api_key after initializer works Given I start the rails service When I navigate to the route "/api_key/changing?api_key=c35a2a72bd230ac0aa0f52715bbdc6ac" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier with the apiKey "c35a2a72bd230ac0aa0f52715bbdc6ac" diff --git a/features/rails_features/app_type.feature b/features/rails_features/app_type.feature index a17cef519..d08822c40 100644 --- a/features/rails_features/app_type.feature +++ b/features/rails_features/app_type.feature @@ -5,8 +5,8 @@ Scenario: Setting app_type in initializer works Given I set environment variable "BUGSNAG_APP_TYPE" to "custom_app_type" And I start the rails service When I navigate to the route "/app_type/initializer" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the exception "errorClass" equals "RuntimeError" And the exception "message" starts with "handled string" And the event "metaData.request.url" ends with "/app_type/initializer" @@ -16,8 +16,8 @@ Scenario: Setting app_type in initializer works Scenario: Changing app_type after initializer works Given I start the rails service When I navigate to the route "/app_type/after?type=maze_after_initializer" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the exception "errorClass" equals "RuntimeError" And the exception "message" starts with "handled string" And the event "metaData.request.url" ends with "/app_type/after?type=maze_after_initializer" @@ -27,8 +27,8 @@ Scenario: Changing app_type after initializer works Scenario: Should default to "rails" for handled errors Given I start the rails service When I navigate to the route "/app_type/handled" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "metaData.request.url" ends with "/app_type/handled" And the event "app.type" equals "rails" @@ -36,7 +36,7 @@ Scenario: Should default to "rails" for handled errors Scenario: Should default to "rails" for unhandled errors Given I start the rails service When I navigate to the route "/app_type/unhandled" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "metaData.request.url" ends with "/app_type/unhandled" And the event "app.type" equals "rails" diff --git a/features/rails_features/app_version.feature b/features/rails_features/app_version.feature index 224828d96..ac1c612b3 100644 --- a/features/rails_features/app_version.feature +++ b/features/rails_features/app_version.feature @@ -4,8 +4,8 @@ Feature: App version configuration Scenario: App_version is nil by default Given I start the rails service When I navigate to the route "/app_version/default" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "app.version" is null @rails3 @rails4 @rails5 @rails6 @rails7 @@ -13,14 +13,14 @@ Scenario: Setting app_version in initializer works Given I set environment variable "BUGSNAG_APP_VERSION" to "1.0.0" And I start the rails service When I navigate to the route "/app_version/initializer" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "app.version" equals "1.0.0" @rails3 @rails4 @rails5 @rails6 @rails7 Scenario: Setting app_version after initializer works Given I start the rails service When I navigate to the route "/app_version/after?version=1.1.0" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "app.version" equals "1.1.0" diff --git a/features/rails_features/auto_capture_sessions.feature b/features/rails_features/auto_capture_sessions.feature index 4cf5410de..01a4d2399 100644 --- a/features/rails_features/auto_capture_sessions.feature +++ b/features/rails_features/auto_capture_sessions.feature @@ -5,15 +5,14 @@ Scenario: Auto_capture_sessions defaults to true Given I set environment variable "USE_DEFAULT_AUTO_CAPTURE_SESSIONS" to "true" And I start the rails service When I navigate to the route "/session_tracking/initializer" on the rails app - And I wait to receive a request - Then the request is valid for the session reporting API version "1.0" for the "Ruby Bugsnag Notifier" notifier + And I wait to receive a session + Then the session is valid for the session reporting API version "1.0" for the "Ruby Bugsnag Notifier" notifier @rails3 @rails4 @rails5 @rails6 @rails7 Scenario: Auto_capture_sessions can be set to false in the initializer Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "false" And I start the rails service When I navigate to the route "/session_tracking/initializer" on the rails app - And I wait for 3 seconds Then I should receive no requests @rails3 @rails4 @rails5 @rails6 @rails7 @@ -21,14 +20,14 @@ Scenario: Manual sessions are still sent if Auto_capture_sessions is false Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "false" And I start the rails service When I navigate to the route "/session_tracking/manual" on the rails app - And I wait to receive a request - Then the request is valid for the session reporting API version "1.0" for the "Ruby Bugsnag Notifier" notifier + And I wait to receive a session + Then the session is valid for the session reporting API version "1.0" for the "Ruby Bugsnag Notifier" notifier @rails3 @rails4 @rails5 @rails6 @rails7 Scenario: 100 session calls results in 100 sessions Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "false" And I start the rails service When I navigate to the route "/session_tracking/multi_sessions" on the rails app - And I wait to receive a request - Then the request is valid for the session reporting API version "1.0" for the "Ruby Bugsnag Notifier" notifier + And I wait to receive a session + Then the session is valid for the session reporting API version "1.0" for the "Ruby Bugsnag Notifier" notifier And the total sessionStarted count equals 100 diff --git a/features/rails_features/auto_notify.feature b/features/rails_features/auto_notify.feature index f56f8af84..8ff9ca2bf 100644 --- a/features/rails_features/auto_notify.feature +++ b/features/rails_features/auto_notify.feature @@ -5,7 +5,6 @@ Scenario: Auto_notify set to false in the initializer prevents unhandled error s Given I set environment variable "BUGSNAG_AUTO_NOTIFY" to "false" And I start the rails service When I navigate to the route "/auto_notify/unhandled" on the rails app - And I wait for 3 seconds Then I should receive no requests @rails3 @rails4 @rails5 @rails6 @rails7 @@ -13,8 +12,8 @@ Scenario: Auto_notify set to false in the initializer still sends handled errors Given I set environment variable "BUGSNAG_AUTO_NOTIFY" to "false" And I start the rails service When I navigate to the route "/auto_notify/handled" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is false And the exception "errorClass" equals "RuntimeError" And the exception "message" starts with "handled string" @@ -25,15 +24,14 @@ Scenario: Auto_notify set to false in the initializer still sends handled errors Scenario: Auto_notify set to false after the initializer prevents unhandled error sending Given I start the rails service When I navigate to the route "/auto_notify/unhandled_after" on the rails app - And I wait for 3 seconds Then I should receive no requests @rails3 @rails4 @rails5 @rails6 @rails7 Scenario: Auto_notify set to false after the initializer still sends handled errors Given I start the rails service When I navigate to the route "/auto_notify/handled_after" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the exception "errorClass" equals "RuntimeError" And the exception "message" starts with "handled string" And the event "unhandled" is false diff --git a/features/rails_features/before_notify.feature b/features/rails_features/before_notify.feature index 8ab377000..99249b823 100644 --- a/features/rails_features/before_notify.feature +++ b/features/rails_features/before_notify.feature @@ -4,8 +4,8 @@ Feature: Before notify callbacks Scenario: Rails before_notify controller method works on handled errors Given I start the rails service When I navigate to the route "/before_notify/handled" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the exception "errorClass" equals "RuntimeError" And the exception "message" starts with "handled string" And the event "unhandled" is false @@ -18,8 +18,8 @@ Scenario: Rails before_notify controller method works on handled errors Scenario: Rails before_notify controller method works on unhandled errors Given I start the rails service When I navigate to the route "/before_notify/unhandled" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the exception "errorClass" equals "NameError" And the exception "message" starts with "undefined local variable or method `generate_unhandled_error' for #?email=testtest@test.test" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "user.email" equals "testtest@test.test" And the event "user.name" equals "Warden User" And the event "user.first_name" equals "Warden" @@ -22,8 +22,8 @@ Scenario Outline: Devise user information is sent Given I start the rails service When I navigate to the route "/devise/create" on the rails app And I navigate to the route "/devise/" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "user.email" equals "test+test@test.test" And the event "user.name" equals "Devise User" And the event "user.first_name" equals "Devise" @@ -39,8 +39,8 @@ Scenario Outline: Clearance user information is sent Given I start the rails service When I navigate to the route "/clearance/create" on the rails app And I navigate to the route "/clearance/" on the rails app - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "user.email" equals "testtest@test.test" And the event "user.name" equals "Clearance User" And the event "user.first_name" equals "Clearance" diff --git a/features/rake.feature b/features/rake.feature index 5e3fbf421..21066e629 100644 --- a/features/rake.feature +++ b/features/rake.feature @@ -2,8 +2,8 @@ Feature: Bugsnag raises errors in Rake Scenario: An unhandled RuntimeError sends a report Given I run the service "rake" with the command "bundle exec rake unhandled" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is true And the event "severity" equals "error" And the event "severityReason.type" equals "unhandledExceptionMiddleware" @@ -13,8 +13,8 @@ Scenario: An unhandled RuntimeError sends a report Scenario: A handled RuntimeError sends a report Given I run the service "rake" with the command "bundle exec rake handled" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is false And the event "severity" equals "warning" And the event "severityReason.type" equals "handledException" diff --git a/features/sidekiq.feature b/features/sidekiq.feature index b4df3dea6..f3f398841 100644 --- a/features/sidekiq.feature +++ b/features/sidekiq.feature @@ -1,9 +1,9 @@ Feature: Bugsnag raises errors in Sidekiq workers Scenario: An unhandled RuntimeError sends a report - Given I run the service "sidekiq" with the command "bundle exec rake sidekiq_tests:unhandled_error" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + Given I run the service "sidekiq" with the command "timeout 5 bundle exec rake sidekiq_tests:unhandled_error" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is true And the event "severity" equals "error" And the event "context" equals "UnhandledError@default" @@ -13,38 +13,38 @@ Scenario: An unhandled RuntimeError sends a report And the exception "errorClass" equals "RuntimeError" And the "file" of stack frame 0 equals "/app/app.rb" And the "lineNumber" of stack frame 0 equals 44 - And the payload field "events.0.metaData.sidekiq" matches the appropriate Sidekiq unhandled payload + And the event "metaData.sidekiq" matches the appropriate Sidekiq unhandled payload And the event "metaData.sidekiq.msg.created_at" is a parsable timestamp in seconds And the event "metaData.sidekiq.msg.enqueued_at" is a parsable timestamp in seconds And the event "metaData.config.delivery_method" equals "thread_queue" Scenario: A handled RuntimeError can be notified - Given I run the service "sidekiq" with the command "bundle exec rake sidekiq_tests:handled_error" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + Given I run the service "sidekiq" with the command "timeout 5 bundle exec rake sidekiq_tests:handled_error" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is false And the event "context" equals "HandledError@default" And the event "severity" equals "warning" And the event "severityReason.type" equals "handledException" And the event "app.type" equals "sidekiq" And the exception "errorClass" equals "RuntimeError" - And the payload field "events.0.metaData.sidekiq" matches the appropriate Sidekiq handled payload + And the event "metaData.sidekiq" matches the appropriate Sidekiq handled payload And the event "metaData.sidekiq.msg.created_at" is a parsable timestamp in seconds And the event "metaData.sidekiq.msg.enqueued_at" is a parsable timestamp in seconds And the event "metaData.config.delivery_method" equals "thread_queue" Scenario: Synchronous delivery can be used Given I set environment variable "BUGSNAG_DELIVERY_METHOD" to "synchronous" - And I run the service "sidekiq" with the command "bundle exec rake sidekiq_tests:handled_error" - And I wait to receive a request - Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And I run the service "sidekiq" with the command "timeout 5 bundle exec rake sidekiq_tests:handled_error" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" notifier And the event "unhandled" is false And the event "context" equals "HandledError@default" And the event "severity" equals "warning" And the event "severityReason.type" equals "handledException" And the event "app.type" equals "sidekiq" And the exception "errorClass" equals "RuntimeError" - And the payload field "events.0.metaData.sidekiq" matches the appropriate Sidekiq handled payload + And the event "metaData.sidekiq" matches the appropriate Sidekiq handled payload And the event "metaData.sidekiq.msg.created_at" is a parsable timestamp in seconds And the event "metaData.sidekiq.msg.enqueued_at" is a parsable timestamp in seconds And the event "metaData.config.delivery_method" equals "synchronous" diff --git a/features/steps/ruby_notifier_steps.rb b/features/steps/ruby_notifier_steps.rb index 04d324c83..6c2640790 100644 --- a/features/steps/ruby_notifier_steps.rb +++ b/features/steps/ruby_notifier_steps.rb @@ -2,62 +2,33 @@ require "net/http" Then(/^the "(.+)" of the top non-bugsnag stackframe equals (\d+|".+")$/) do |element, value| - stacktrace = read_key_path(Server.current_request[:body], 'events.0.exceptions.0.stacktrace') + body = Maze::Server.errors.current[:body] + stacktrace = Maze::Helper.read_key_path(body, 'events.0.exceptions.0.stacktrace') + frame_index = stacktrace.find_index { |frame| ! /.*lib\/bugsnag.*\.rb/.match(frame["file"]) } + steps %Q{ the "#{element}" of stack frame #{frame_index} equals #{value} } end Then(/^the total sessionStarted count equals (\d+)$/) do |value| - session_counts = read_key_path(Server.current_request[:body], "sessionCounts") - total_count = session_counts.inject(0) { |count, session| count += session["sessionsStarted"] } - assert_equal(value, total_count) -end + body = Maze::Server.sessions.current[:body] + session_counts = Maze::Helper.read_key_path(body, "sessionCounts") -# Due to an ongoing discussion on whether the `payload_version` needs to be present within the headers -# and body of the payload, this step is a local replacement for the similar step present in the main -# maze-runner library. Once the discussion is resolved this step should be removed and replaced in scenarios -# with the main library version. -Then("the request is valid for the error reporting API version {string} for the {string}") do |payload_version, notifier_name| - steps %Q{ - Then the "Bugsnag-Api-Key" header equals "#{$api_key}" - And the payload field "apiKey" equals "#{$api_key}" - And the "Bugsnag-Payload-Version" header equals "#{payload_version}" - And the "Content-Type" header equals "application/json" - And the "Bugsnag-Sent-At" header is a timestamp - - And the payload field "notifier.name" equals "#{notifier_name}" - And the payload field "notifier.url" is not null - And the payload field "notifier.version" is not null - And the payload field "events" is a non-empty array - - And each element in payload field "events" has "severity" - And each element in payload field "events" has "severityReason.type" - And each element in payload field "events" has "unhandled" - And each element in payload field "events" has "exceptions" - } + total_count = session_counts.sum { |session| session["sessionsStarted"] } + assert_equal(value, total_count) end Given("I start the rails service") do - rails_version = ENV["RAILS_VERSION"] steps %Q{ - When I start the service "rails#{rails_version}" - And I wait for the host "rails#{rails_version}" to open port "3000" + When I start the service "#{RAILS_FIXTURE.docker_service}" + And I wait for the host "#{RAILS_FIXTURE.host}" to open port "#{RAILS_FIXTURE.port}" } end -# When running tests against Rails on Ruby 3, the base Maze Runner step -# "I open the url {string}" commonly flakes due to a "Errno::ECONNREFUSED" -# error, which MR doesn't rescue. The notifier request is still fired so the -# test passes when these errors are rescued and there's no risk of swallowing an -# actual failure because any assertion steps will fail if the notifier request -# isn't fired. This may become unnecessary in future, when running Rails on -# Ruby 3 is more stable When("I navigate to the route {string} on the rails app") do |route| - URI.open("http://rails#{ENV["RAILS_VERSION"]}:3000#{route}", &:read) -rescue => e - $logger.debug(e.inspect) + RAILS_FIXTURE.navigate_to(route) end When("I run {string} in the rails app") do |command| @@ -79,77 +50,51 @@ end Given("I start the rack service") do - rack_version = ENV["RACK_VERSION"] steps %Q{ - When I start the service "rack#{rack_version}" - And I wait for the host "rack#{rack_version}" to open port "3000" + When I start the service "#{RACK_FIXTURE.docker_service}" + And I wait for the host "#{RACK_FIXTURE.host}" to open port "#{RACK_FIXTURE.port}" } end When("I navigate to the route {string} on the rack app") do |route| - rack_version = ENV["RACK_VERSION"] - steps %Q{ - When I open the URL "http://rack#{rack_version}:3000#{route}" - } + RACK_FIXTURE.navigate_to(route) end When("I navigate to the route {string} on the rack app with these cookies:") do |route, data| - rack_version = ENV["RACK_VERSION"] - uri = URI("http://rack#{rack_version}:3000#{route}") - # e.g. { "a" => "b", "c" => "d" } -> "a=b;c=d" cookie = data.rows_hash.map { |key, value| "#{key}=#{value}" }.join(";") - http = Net::HTTP.new(uri.host, uri.port) - request = Net::HTTP::Get.new(uri.request_uri) - request["Cookie"] = cookie - - http.request(request) + RACK_FIXTURE.navigate_to(route, { "Cookie" => cookie }) end When("I send a POST request to {string} in the rack app with the following form data:") do |route, data| - rack_version = ENV["RACK_VERSION"] - uri = URI("http://rack#{rack_version}:3000#{route}") - - Net::HTTP.post_form(uri, data.rows_hash) + RACK_FIXTURE.post_form(route, data.rows_hash) end When("I send a POST request to {string} in the rack app with the following JSON:") do |route, data| - rack_version = ENV["RACK_VERSION"] - - Net::HTTP.post( - URI("http://rack#{rack_version}:3000#{route}"), - JSON.generate(data.rows_hash), - { "Content-Type" => "application/json" } - ) + RACK_FIXTURE.post_json(route, data.rows_hash) end -Then("the payload field {string} matches the appropriate Sidekiq handled payload") do |field| +Then("the event {string} matches the appropriate Sidekiq handled payload") do |field| # Sidekiq 2 doesn't include the "created_at" field created_at_present = ENV["SIDEKIQ_VERSION"] > "2" steps %Q{ - And the payload field "#{field}" matches the JSON fixture in "features/fixtures/sidekiq/payloads/handled_metadata_ca_#{created_at_present}.json" + And the event "#{field}" matches the JSON fixture in "features/fixtures/sidekiq/payloads/handled_metadata_ca_#{created_at_present}.json" } end -Then("the payload field {string} matches the appropriate Sidekiq unhandled payload") do |field| +Then("the event {string} matches the appropriate Sidekiq unhandled payload") do |field| # Sidekiq 2 doesn't include the "created_at" field created_at_present = ENV["SIDEKIQ_VERSION"] > "2" steps %Q{ - And the payload field "#{field}" matches the JSON fixture in "features/fixtures/sidekiq/payloads/unhandled_metadata_ca_#{created_at_present}.json" + And the event "#{field}" matches the JSON fixture in "features/fixtures/sidekiq/payloads/unhandled_metadata_ca_#{created_at_present}.json" } end -def rails_version_matches?(operator, version_to_compare) - # send the given operator as a method to the current rails version - # this will evaluate to e.g. '6.send(">=", 5)', which is the same as '6 >= 5' - ENV["RAILS_VERSION"].to_i.send(operator, version_to_compare) -end - Then("in Rails versions {string} {int} the event {string} equals {string}") do |operator, version, path, expected| - if rails_version_matches?(operator, version) + if RAILS_FIXTURE.version_matches?(operator, version) steps %Q{ And the event "#{path}" equals "#{expected}" } @@ -161,7 +106,7 @@ def rails_version_matches?(operator, version_to_compare) end Then("in Rails versions {string} {int} the event {string} equals {int}") do |operator, version, path, expected| - if rails_version_matches?(operator, version) + if RAILS_FIXTURE.version_matches?(operator, version) steps %Q{ And the event "#{path}" equals #{expected} } @@ -173,7 +118,7 @@ def rails_version_matches?(operator, version_to_compare) end Then("in Rails versions {string} {int} the event {string} matches {string}") do |operator, version, path, expected| - if rails_version_matches?(operator, version) + if RAILS_FIXTURE.version_matches?(operator, version) steps %Q{ And the event "#{path}" matches "#{expected}" } @@ -185,7 +130,7 @@ def rails_version_matches?(operator, version_to_compare) end Then("in Rails versions {string} {int} the event {string} is a timestamp") do |operator, version, path| - if rails_version_matches?(operator, version) + if RAILS_FIXTURE.version_matches?(operator, version) steps %Q{ And the event "#{path}" is a timestamp } @@ -205,3 +150,30 @@ def rails_version_matches?(operator, version_to_compare) And the event "#{path}" starts with "#{que_version}" } end + +Given("I configure the BUGSNAG_PROXY environment variables") do + host = running_in_docker? ? "maze-runner" : current_ip + + steps %Q{ + When I set environment variable "BUGSNAG_PROXY_HOST" to "#{host}" + And I set environment variable "BUGSNAG_PROXY_PORT" to "#{Maze.config.port}" + And I set environment variable "BUGSNAG_PROXY_USER" to "tester" + And I set environment variable "BUGSNAG_PROXY_PASSWORD" to "testpass" + } +end + +Given("I configure the http_proxy environment variable") do + host = running_in_docker? ? "maze-runner" : current_ip + + steps %Q{ + Given I set environment variable "http_proxy" to "http://tester:testpass@#{host}:#{Maze.config.port}" + } +end + +Given("I configure the https_proxy environment variable") do + host = running_in_docker? ? "maze-runner" : current_ip + + steps %Q{ + Given I set environment variable "https_proxy" to "https://tester:testpass@#{host}:#{Maze.config.port}" + } +end diff --git a/features/support/env.rb b/features/support/env.rb index c6121cd68..9dee7b3a1 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -1,23 +1,65 @@ +require 'os' require 'fileutils' +require_relative "./../lib/fixture" + +RACK_FIXTURE = Fixture.new("rack") +RAILS_FIXTURE = Fixture.new("rails", ENV["RAILS_VERSION"]) + +def running_in_docker? + File.exist?("/app/bugsnag.gem") +end def install_fixture_gems - throw Error.new("Bugsnag.gem not found. Is this running in a docker-container?") unless File.exist?("/app/bugsnag.gem") + if running_in_docker? + # running in docker so the gem is built already + bugsnag_gem_path = "/app/bugsnag.gem" + else + # running locally so we need to build the gem + `gem build bugsnag.gemspec -o bugsnag.gem` + bugsnag_gem_path = "#{__dir__}/../../bugsnag.gem" + end + Dir.entries('features/fixtures').reject { |entry| ['.', '..'].include?(entry) }.each do |entry| target_dir = "features/fixtures/#{entry}" if File.directory?(target_dir) - `cp /app/bugsnag.gem #{target_dir}` + `cp #{bugsnag_gem_path} #{target_dir}` `gem unpack #{target_dir}/bugsnag.gem --target #{target_dir}/temp-bugsnag-lib` end end +ensure + File.unlink(bugsnag_gem_path) unless running_in_docker? +end + +def current_ip + return "host.docker.internal" if OS.mac? + + ip_addr = `ifconfig | grep -Eo 'inet (addr:)?([0-9]*\\\.){3}[0-9]*' | grep -v '127.0.0.1'` + ip_list = /((?:[0-9]*\.){3}[0-9]*)/.match(ip_addr) + ip_list.captures.first end -AfterConfiguration do |config| +Maze.hooks.before_all do install_fixture_gems + + # log to console, not a file + Maze.config.file_log = false + Maze.config.log_requests = true + + # don't wait so long for requests/not to receive requests locally + unless ENV["CI"] + Maze.config.receive_requests_wait = 10 + Maze.config.receive_no_requests_wait = 10 + end + + # bugsnag-ruby doesn't need to send the integrity header + Maze.config.enforce_bugsnag_integrity = false end -Before do - Docker.compose_project_name = "#{rand.to_s}:#{Time.new.strftime("%s")}" - Runner.environment.clear - Runner.environment["BUGSNAG_API_KEY"] = $api_key - Runner.environment["BUGSNAG_ENDPOINT"] = "http://maze-runner:#{MOCK_API_PORT}" +Maze.hooks.before do + Maze::Runner.environment["BUGSNAG_API_KEY"] = $api_key + + host = running_in_docker? ? "maze-runner" : current_ip + + Maze::Runner.environment["BUGSNAG_ENDPOINT"] = "http://#{host}:#{Maze.config.port}/notify" + Maze::Runner.environment["BUGSNAG_SESSION_ENDPOINT"] = "http://#{host}:#{Maze.config.port}/sessions" end diff --git a/lib/bugsnag.rb b/lib/bugsnag.rb index 4d94ce8c8..a25ed6b49 100644 --- a/lib/bugsnag.rb +++ b/lib/bugsnag.rb @@ -2,6 +2,7 @@ require "thread" require "bugsnag/version" +require "bugsnag/utility/feature_data_store" require "bugsnag/configuration" require "bugsnag/meta_data" require "bugsnag/report" @@ -14,6 +15,8 @@ require "bugsnag/delivery/synchronous" require "bugsnag/delivery/thread_queue" +require "bugsnag/feature_flag" + # Rack is not bundled with the other integrations # as it doesn't auto-configure when loaded require "bugsnag/integrations/rack" @@ -36,6 +39,7 @@ require "bugsnag/utility/duplicator" require "bugsnag/utility/metadata_delegate" +require "bugsnag/utility/feature_flag_delegate" # rubocop:todo Metrics/ModuleLength module Bugsnag @@ -45,6 +49,8 @@ module Bugsnag NIL_EXCEPTION_DESCRIPTION = "'nil' was notified as an exception" class << self + include Utility::FeatureDataStore + ## # Configure the Bugsnag notifier application-wide settings. # @@ -426,6 +432,16 @@ def clear_metadata(section, *args) configuration.clear_metadata(section, *args) end + # Expose the feature flag delegate internally for use when creating new Events + # + # The Bugsnag module's feature_flag_delegate is request-specific + # + # @return [Bugsnag::Utility::FeatureFlagDelegate] + # @api private + def feature_flag_delegate + configuration.request_data[:feature_flag_delegate] ||= Utility::FeatureFlagDelegate.new + end + private def should_deliver_notification?(exception, auto_notify) diff --git a/lib/bugsnag/feature_flag.rb b/lib/bugsnag/feature_flag.rb new file mode 100644 index 000000000..623a01b8f --- /dev/null +++ b/lib/bugsnag/feature_flag.rb @@ -0,0 +1,74 @@ +module Bugsnag + class FeatureFlag + # Get the name of this feature flag + # + # @return [String] + attr_reader :name + + # Get the variant of this feature flag + # + # @return [String, nil] + attr_reader :variant + + # @param name [String] The name of this feature flags + # @param variant [String, nil] An optional variant for this flag + def initialize(name, variant = nil) + @name = name + @variant = coerce_variant(variant) + end + + def ==(other) + self.class == other.class && @name == other.name && @variant == other.variant + end + + def hash + [@name, @variant].hash + end + + # Convert this flag to a hash + # + # @example With no variant + # { "featureFlag" => "name" } + # + # @example With a variant + # { "featureFlag" => "name", "variant" => "variant" } + # + # @return [Hash{String => String}] + def to_h + if @variant.nil? + { "featureFlag" => @name } + else + { "featureFlag" => @name, "variant" => @variant } + end + end + + # Check if this flag is valid, i.e. has a name that's a String and a variant + # that's either nil or a String + # + # @return [Boolean] + def valid? + @name.is_a?(String) && + !@name.empty? && + (@variant.nil? || @variant.is_a?(String)) + end + + private + + # Coerce this variant into a valid value (String or nil) + # + # If the variant is not already a string or nil, we use #to_s to coerce it. + # If #to_s raises, the variant will be set to nil + # + # @param variant [Object] + # @return [String, nil] + def coerce_variant(variant) + if variant.nil? || variant.is_a?(String) + variant + else + variant.to_s + end + rescue StandardError + nil + end + end +end diff --git a/lib/bugsnag/report.rb b/lib/bugsnag/report.rb index ab0c2da6b..ca6353950 100644 --- a/lib/bugsnag/report.rb +++ b/lib/bugsnag/report.rb @@ -6,6 +6,8 @@ module Bugsnag # rubocop:todo Metrics/ClassLength class Report + include Utility::FeatureDataStore + NOTIFIER_NAME = "Ruby Bugsnag Notifier" NOTIFIER_VERSION = Bugsnag::VERSION NOTIFIER_URL = "https://www.bugsnag.com" @@ -143,6 +145,7 @@ def initialize(exception, passed_configuration, auto_notify=false) self.user = {} @metadata_delegate = Utility::MetadataDelegate.new + @feature_flag_delegate = Bugsnag.feature_flag_delegate.dup end ## @@ -220,6 +223,7 @@ def as_json time: @created_at }, exceptions: exceptions, + featureFlags: @feature_flag_delegate.as_json, groupingHash: grouping_hash, metaData: meta_data, session: session, @@ -239,6 +243,7 @@ def as_json :version => NOTIFIER_VERSION, :url => NOTIFIER_URL }, + :payloadVersion => CURRENT_PAYLOAD_VERSION, :events => [payload_event] } end @@ -357,6 +362,13 @@ def clear_metadata(section, *args) @metadata_delegate.clear_metadata(@meta_data, section, *args) end + # Get the array of stored feature flags + # + # @return [Array] + def feature_flags + @feature_flag_delegate.to_a + end + ## # Set information about the current user # @@ -393,6 +405,8 @@ def unhandled_overridden? private + attr_reader :feature_flag_delegate + def update_handled_counts(is_unhandled, was_unhandled) # do nothing if there is no session to update return if @session.nil? diff --git a/lib/bugsnag/utility/feature_data_store.rb b/lib/bugsnag/utility/feature_data_store.rb new file mode 100644 index 000000000..c72939cf8 --- /dev/null +++ b/lib/bugsnag/utility/feature_data_store.rb @@ -0,0 +1,41 @@ +module Bugsnag::Utility + # @abstract Requires a #feature_flag_delegate method returning a + # {Bugsnag::Utility::FeatureFlagDelegate} + module FeatureDataStore + # Add a feature flag with the given name & variant + # + # @param name [String] + # @param variant [String, nil] + # @return [void] + def add_feature_flag(name, variant = nil) + feature_flag_delegate.add(name, variant) + end + + # Merge the given array of FeatureFlag instances into the stored feature + # flags + # + # New flags will be appended to the array. Flags with the same name will be + # overwritten, but their position in the array will not change + # + # @param feature_flags [Array] + # @return [void] + def add_feature_flags(feature_flags) + feature_flag_delegate.merge(feature_flags) + end + + # Remove the stored flag with the given name + # + # @param name [String] + # @return [void] + def clear_feature_flag(name) + feature_flag_delegate.remove(name) + end + + # Remove all the stored flags + # + # @return [void] + def clear_feature_flags + feature_flag_delegate.clear + end + end +end diff --git a/lib/bugsnag/utility/feature_flag_delegate.rb b/lib/bugsnag/utility/feature_flag_delegate.rb new file mode 100644 index 000000000..c90980f33 --- /dev/null +++ b/lib/bugsnag/utility/feature_flag_delegate.rb @@ -0,0 +1,89 @@ +module Bugsnag::Utility + # @api private + class FeatureFlagDelegate + def initialize + # feature flags are stored internally in a hash of "name" => + # we don't use a Set because new feature flags should overwrite old ones + # that share a name, but FeatureFlag equality also uses the variant + @storage = {} + end + + def initialize_dup(original) + super + + # copy the internal storage when 'dup' is called + @storage = @storage.dup + end + + # Add a feature flag with the given name & variant + # + # @param name [String] + # @param variant [String, nil] + # @return [void] + def add(name, variant) + flag = Bugsnag::FeatureFlag.new(name, variant) + + return unless flag.valid? + + @storage[flag.name] = flag + end + + # Merge the given array of FeatureFlag instances into the stored feature + # flags + # + # New flags will be appended to the array. Flags with the same name will be + # overwritten, but their position in the array will not change + # + # @param feature_flags [Array] + # @return [void] + def merge(feature_flags) + feature_flags.each do |flag| + next unless flag.is_a?(Bugsnag::FeatureFlag) + next unless flag.valid? + + @storage[flag.name] = flag + end + end + + # Remove the stored flag with the given name + # + # @param name [String] + # @return [void] + def remove(name) + @storage.delete(name) + end + + # Remove all the stored flags + # + # @return [void] + def clear + @storage.clear + end + + # Get an array of FeatureFlag instances + # + # @example + # [ + # <#Bugsnag::FeatureFlag>, + # <#Bugsnag::FeatureFlag>, + # ] + # + # @return [Array] + def to_a + @storage.values + end + + # Get the feature flags in their JSON representation + # + # @example + # [ + # { "featureFlag" => "name", "variant" => "variant" }, + # { "featureFlag" => "another name" }, + # ] + # + # @return [Array String}>] + def as_json + to_a.map(&:to_h) + end + end +end diff --git a/scripts/license_finder.sh b/scripts/license_finder.sh deleted file mode 100755 index b33d68ba0..000000000 --- a/scripts/license_finder.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -curl https://raw.githubusercontent.com/bugsnag/license-audit/master/config/decision_files/global.yml -o config/decisions.yml -curl https://raw.githubusercontent.com/bugsnag/license-audit/master/config/decision_files/bugsnag-ruby.yml >> config/decisions.yml - -bundle install -license_finder --decisions-file=config/decisions.yml diff --git a/spec/bugsnag_spec.rb b/spec/bugsnag_spec.rb index 6f5af3b46..9487b9a86 100644 --- a/spec/bugsnag_spec.rb +++ b/spec/bugsnag_spec.rb @@ -1127,4 +1127,119 @@ module Kernel }) end end + + describe "feature flags" do + it "is added to the payload" do + Bugsnag.add_feature_flag('abc') + Bugsnag.add_feature_flag('xyz', '123') + + Bugsnag.notify(BugsnagTestException.new("It crashed")) + + expect(Bugsnag).to have_sent_notification { |payload, headers| + event = get_event_from_payload(payload) + expect(event["featureFlags"]).to eq([ + { "featureFlag" => "abc" }, + { "featureFlag" => "xyz", "variant" => "123" }, + ]) + } + end + + it "does not mutate the Bugsnag module's feature flags if more flags are added" do + Bugsnag.add_feature_flag('abc') + Bugsnag.add_feature_flag('xyz', '123') + + Bugsnag.notify(BugsnagTestException.new("It crashed")) do |event| + event.add_feature_flag('another one') + end + + expect(Bugsnag).to have_sent_notification { |payload, headers| + event = get_event_from_payload(payload) + expect(event["featureFlags"]).to eq([ + { "featureFlag" => "abc" }, + { "featureFlag" => "xyz", "variant" => "123" }, + { "featureFlag" => "another one" }, + ]) + + expect(Bugsnag.feature_flag_delegate.as_json).to eq([ + { "featureFlag" => "abc" }, + { "featureFlag" => "xyz", "variant" => "123" }, + ]) + } + end + + it "does not mutate the Bugsnag module's feature flags if flags are removed" do + Bugsnag.add_feature_flag('abc') + Bugsnag.add_feature_flag('xyz', '123') + + Bugsnag.notify(BugsnagTestException.new("It crashed")) do |event| + event.clear_feature_flags + end + + expect(Bugsnag).to have_sent_notification { |payload, headers| + event = get_event_from_payload(payload) + expect(event["featureFlags"]).to be_empty + + expect(Bugsnag.feature_flag_delegate.as_json).to eq([ + { "featureFlag" => "abc" }, + { "featureFlag" => "xyz", "variant" => "123" }, + ]) + } + end + + it "does not mutate the event's feature flags if the Bugsnag module's flags are removed" do + Bugsnag.add_feature_flags([ + Bugsnag::FeatureFlag.new('abc'), + Bugsnag::FeatureFlag.new('xyz', 123), + ]) + + Bugsnag.notify(BugsnagTestException.new("It crashed")) do |event| + Bugsnag.clear_feature_flags + end + + expect(Bugsnag).to have_sent_notification { |payload, headers| + event = get_event_from_payload(payload) + expect(event["featureFlags"]).to eq([ + { "featureFlag" => "abc" }, + { "featureFlag" => "xyz", "variant" => "123" }, + ]) + + expect(Bugsnag.feature_flag_delegate.as_json).to be_empty + } + end + + it "stores feature flags per-thread" do + threads = 5.times.map do |i| + Thread.new do + expect(Bugsnag.feature_flag_delegate.to_a).to be_empty + + Bugsnag.add_feature_flag("thread_#{i} flag 1", i) + + expect(Bugsnag.feature_flag_delegate.to_a).to eq([ + Bugsnag::FeatureFlag.new("thread_#{i} flag 1", i), + ]) + end + end + + threads += 5.times.map do |i| + Thread.new do + expect(Bugsnag.feature_flag_delegate.to_a).to be_empty + + Bugsnag.add_feature_flags([ + Bugsnag::FeatureFlag.new("thread_#{i} flag 2", i * 100), + Bugsnag::FeatureFlag.new("thread_#{i} flag 3", i * 100 + 1), + ]) + + expect(Bugsnag.feature_flag_delegate.to_a).to eq([ + Bugsnag::FeatureFlag.new("thread_#{i} flag 2", i * 100), + Bugsnag::FeatureFlag.new("thread_#{i} flag 3", i * 100 + 1), + ]) + end + end + + threads.shuffle.each(&:join) + + # we added no flags in this thread, so there should be none + expect(Bugsnag.feature_flag_delegate.to_a).to be_empty + end + end end diff --git a/spec/feature_flag_spec.rb b/spec/feature_flag_spec.rb new file mode 100644 index 000000000..4fdf5c22f --- /dev/null +++ b/spec/feature_flag_spec.rb @@ -0,0 +1,164 @@ +require 'spec_helper' + +describe Bugsnag::FeatureFlag do + it "has a name" do + flag = Bugsnag::FeatureFlag.new("abc") + + expect(flag.name).to eq("abc") + expect(flag.variant).to be_nil + end + + it "has an optional variant" do + flag = Bugsnag::FeatureFlag.new("abc", "xyz") + + expect(flag.name).to eq("abc") + expect(flag.variant).to eq("xyz") + end + + [ + [123, "123"], + [true, "true"], + [false, "false"], + [[1, 2, 3], "[1, 2, 3]"], + [{ a: 1, b: 2 }, "{:a=>1, :b=>2}"], + ].each do |variant, expected| + it "converts the variant to a string if given '#{variant.class}'" do + flag = Bugsnag::FeatureFlag.new("abc", variant) + + expect(flag.name).to eq("abc") + expect(flag.variant).to eq(expected) + end + end + + it "sets variant to 'nil' if variant cannot be converted to a string" do + class StringRaiser + def to_s + raise 'Oh no you do not!' + end + end + + flag = Bugsnag::FeatureFlag.new("abc", StringRaiser.new) + + expect(flag.name).to eq("abc") + expect(flag.variant).to be_nil + end + + + it "is immutable" do + flag = Bugsnag::FeatureFlag.new("abc", "xyz") + + expect(flag).not_to respond_to(:name=) + expect(flag).not_to respond_to(:variant=) + end + + describe "#to_h" do + it "converts the flag to a hash when no variant is given" do + flag = Bugsnag::FeatureFlag.new("xyz") + + expect(flag.to_h).to eq({ "featureFlag" => "xyz" }) + end + + it "converts the flag to a hash when variant is given" do + flag = Bugsnag::FeatureFlag.new("xyz", "1234") + + expect(flag.to_h).to eq({ "featureFlag" => "xyz", "variant" => "1234" }) + end + end + + describe "#==" do + it "is equal to other instances with the same name when neither have a variant" do + flag1 = Bugsnag::FeatureFlag.new("xyz") + flag2 = Bugsnag::FeatureFlag.new("xyz") + + expect(flag1).to eq(flag2) + expect(flag2).to eq(flag1) + + expect(flag1).not_to be(flag2) + end + + it "is equal to other instances with the same name and variant" do + flag1 = Bugsnag::FeatureFlag.new("xyz", "1234") + flag2 = Bugsnag::FeatureFlag.new("xyz", "1234") + + expect(flag1).to eq(flag2) + expect(flag2).to eq(flag1) + + expect(flag1).not_to be(flag2) + end + + it "is not equal to other instances with the same name but a different variant" do + flag1 = Bugsnag::FeatureFlag.new("xyz", "1234") + flag2 = Bugsnag::FeatureFlag.new("xyz", "9876") + + expect(flag1).not_to eq(flag2) + expect(flag2).not_to eq(flag1) + end + + it "is not equal to other instances with the same name when only one has a variant" do + flag1 = Bugsnag::FeatureFlag.new("xyz") + flag2 = Bugsnag::FeatureFlag.new("xyz", "9876") + + expect(flag1).not_to eq(flag2) + expect(flag2).not_to eq(flag1) + end + + it "is not equal to other instances with a different name but the same variant" do + flag1 = Bugsnag::FeatureFlag.new("xyz", "1234") + flag2 = Bugsnag::FeatureFlag.new("abc", "1234") + + expect(flag1).not_to eq(flag2) + expect(flag2).not_to eq(flag1) + end + + it "is not equal to other instances with a different name and variant" do + flag1 = Bugsnag::FeatureFlag.new("xyz", "1234") + flag2 = Bugsnag::FeatureFlag.new("abc", "9876") + + expect(flag1).not_to eq(flag2) + expect(flag2).not_to eq(flag1) + end + end + + describe "#valid?" do + [ + nil, + true, + false, + 1234, + [1, 2, 3], + { a: 1, b: 2 }, + :abc, + "", + ].each do |name| + it "returns false when name is '#{name.inspect}' with no variant" do + flag = Bugsnag::FeatureFlag.new(name) + + expect(flag.valid?).to be(false) + end + + it "returns false when name is '#{name.inspect}' and variant is present" do + flag = Bugsnag::FeatureFlag.new(name, name) + + expect(flag.valid?).to be(false) + end + + it "returns true when name is a string and variant is '#{name.inspect}'" do + flag = Bugsnag::FeatureFlag.new("a name", name) + + expect(flag.valid?).to be(true) + end + end + + it "returns true when name is a string with no variant" do + flag = Bugsnag::FeatureFlag.new("a name") + + expect(flag.valid?).to be(true) + end + + it "returns true when name and variant are strings" do + flag = Bugsnag::FeatureFlag.new("a name", "a variant") + + expect(flag.valid?).to be(true) + end + end +end diff --git a/spec/fixtures/apps/rails-initializer-config/Gemfile b/spec/fixtures/apps/rails-initializer-config/Gemfile index a7a2ca6c3..22958a757 100644 --- a/spec/fixtures/apps/rails-initializer-config/Gemfile +++ b/spec/fixtures/apps/rails-initializer-config/Gemfile @@ -5,5 +5,5 @@ ruby_version = Gem::Version.new(RUBY_VERSION.dup) gem 'railties', ruby_version <= Gem::Version.new('2.6') ? '4.2.10' : '~> 6.0.2', require: %w(action_controller rails) gem 'rake', ruby_version <= Gem::Version.new('1.9.3') ? '~> 11.3.0' : '~> 12.3.0' gem 'minitest', ruby_version <= Gem::Version.new('2.2') ? '5.11.3' : '~> 5.14.0' -gem 'nokogiri', '1.6.8' +gem 'nokogiri', ruby_version < Gem::Version.new('2.6') ? '1.6.8' : '~> 1.13.9' gem 'bugsnag', path: '../../../..' diff --git a/spec/fixtures/apps/rails-invalid-initializer-config/Gemfile b/spec/fixtures/apps/rails-invalid-initializer-config/Gemfile index a7a2ca6c3..22958a757 100644 --- a/spec/fixtures/apps/rails-invalid-initializer-config/Gemfile +++ b/spec/fixtures/apps/rails-invalid-initializer-config/Gemfile @@ -5,5 +5,5 @@ ruby_version = Gem::Version.new(RUBY_VERSION.dup) gem 'railties', ruby_version <= Gem::Version.new('2.6') ? '4.2.10' : '~> 6.0.2', require: %w(action_controller rails) gem 'rake', ruby_version <= Gem::Version.new('1.9.3') ? '~> 11.3.0' : '~> 12.3.0' gem 'minitest', ruby_version <= Gem::Version.new('2.2') ? '5.11.3' : '~> 5.14.0' -gem 'nokogiri', '1.6.8' +gem 'nokogiri', ruby_version < Gem::Version.new('2.6') ? '1.6.8' : '~> 1.13.9' gem 'bugsnag', path: '../../../..' diff --git a/spec/fixtures/apps/rails-no-config/Gemfile b/spec/fixtures/apps/rails-no-config/Gemfile index a7a2ca6c3..22958a757 100644 --- a/spec/fixtures/apps/rails-no-config/Gemfile +++ b/spec/fixtures/apps/rails-no-config/Gemfile @@ -5,5 +5,5 @@ ruby_version = Gem::Version.new(RUBY_VERSION.dup) gem 'railties', ruby_version <= Gem::Version.new('2.6') ? '4.2.10' : '~> 6.0.2', require: %w(action_controller rails) gem 'rake', ruby_version <= Gem::Version.new('1.9.3') ? '~> 11.3.0' : '~> 12.3.0' gem 'minitest', ruby_version <= Gem::Version.new('2.2') ? '5.11.3' : '~> 5.14.0' -gem 'nokogiri', '1.6.8' +gem 'nokogiri', ruby_version < Gem::Version.new('2.6') ? '1.6.8' : '~> 1.13.9' gem 'bugsnag', path: '../../../..' diff --git a/spec/integrations/rake_spec.rb b/spec/integrations/rake_spec.rb index 6139ae86c..553df011f 100644 --- a/spec/integrations/rake_spec.rb +++ b/spec/integrations/rake_spec.rb @@ -44,7 +44,7 @@ res.status = 200 res.body = "OK\n" end - Thread.new{ server.start } + Thread.new { server.start } end after do @@ -52,8 +52,6 @@ queue.clear end - let(:request) { JSON.parse(queue.pop) } - it 'should run the rake middleware when rake tasks crash' do ENV['BUGSNAG_TEST_SERVER_PORT'] = server.config[:Port].to_s task_fixtures_path = File.join(File.dirname(__FILE__), '../fixtures', 'tasks') @@ -61,7 +59,23 @@ system("../../../bin/rake test:crash") end - result = request() + result = nil + attempts = 0 + + while result.nil? && attempts < 20 + begin + sleep 0.1 + attempts += 1 + + result = queue.pop(true) + rescue ThreadError + end + end + + expect(result).not_to be_nil + + result = JSON.parse(result) + expect(result["events"][0]["metaData"]["rake_task"]).not_to be_nil expect(result["events"][0]["metaData"]["rake_task"]["name"]).to eq("test:crash") expect(result["events"][0]["app"]["type"]).to eq("rake") diff --git a/spec/report_spec.rb b/spec/report_spec.rb index 20c3cd3b0..b226dcaea 100644 --- a/spec/report_spec.rb +++ b/spec/report_spec.rb @@ -2089,5 +2089,196 @@ def to_s }) end end + + it "reports the payload version in the header and body" do + Bugsnag.notify(BugsnagTestException.new("It crashed")) + + expect(Bugsnag).to(have_sent_notification { |payload, headers| + expect(headers["Bugsnag-Payload-Version"]).to eq("4.0") + expect(payload["payloadVersion"]).to eq("4.0") + }) + end + + describe "feature flags" do + it "includes no feature flags by default" do + Bugsnag.notify(BugsnagTestException.new("It crashed")) + + expect(Bugsnag).to have_sent_notification { |payload, headers| + event = get_event_from_payload(payload) + expect(event["featureFlags"]).to eq([]) + } + end + + it "includes the Bugsnag module's feature flags if present" do + Bugsnag.add_feature_flag('abc') + Bugsnag.add_feature_flag('xyz', '123') + + Bugsnag.notify(BugsnagTestException.new("It crashed")) + + expect(Bugsnag).to have_sent_notification { |payload, headers| + event = get_event_from_payload(payload) + expect(event["featureFlags"]).to eq([ + { "featureFlag" => "abc" }, + { "featureFlag" => "xyz", "variant" => "123" }, + ]) + } + end + + it "does not mutate the Bugsnag module's feature flags if more flags are added" do + Bugsnag.add_feature_flag('abc') + Bugsnag.add_feature_flag('xyz', '123') + + Bugsnag.notify(BugsnagTestException.new("It crashed")) do |event| + event.add_feature_flag('another one') + end + + expect(Bugsnag).to have_sent_notification { |payload, headers| + event = get_event_from_payload(payload) + expect(event["featureFlags"]).to eq([ + { "featureFlag" => "abc" }, + { "featureFlag" => "xyz", "variant" => "123" }, + { "featureFlag" => "another one" }, + ]) + + expect(Bugsnag.feature_flag_delegate.as_json).to eq([ + { "featureFlag" => "abc" }, + { "featureFlag" => "xyz", "variant" => "123" }, + ]) + } + end + + it "does not mutate the Bugsnag module's feature flags if flags are removed" do + Bugsnag.add_feature_flag('abc') + Bugsnag.add_feature_flag('xyz', '123') + + Bugsnag.notify(BugsnagTestException.new("It crashed")) do |event| + event.clear_feature_flags + end + + expect(Bugsnag).to have_sent_notification { |payload, headers| + event = get_event_from_payload(payload) + expect(event["featureFlags"]).to be_empty + + expect(Bugsnag.feature_flag_delegate.as_json).to eq([ + { "featureFlag" => "abc" }, + { "featureFlag" => "xyz", "variant" => "123" }, + ]) + } + end + + it "can add individual feature flags to the payload" do + Bugsnag.notify(BugsnagTestException.new("It crashed")) do |event| + event.add_feature_flag("flag 1") + event.add_feature_flag("flag 2", "1234") + end + + expect(Bugsnag).to have_sent_notification { |payload, headers| + event = get_event_from_payload(payload) + expect(event["featureFlags"]).to eq([ + { "featureFlag" => "flag 1" }, + { "featureFlag" => "flag 2", "variant" => "1234" }, + ]) + } + end + + it "can add multiple feature flags to the payload in one go" do + Bugsnag.notify(BugsnagTestException.new("It crashed")) do |event| + flags = [ + Bugsnag::FeatureFlag.new("a"), + Bugsnag::FeatureFlag.new("b"), + Bugsnag::FeatureFlag.new("c", "1"), + Bugsnag::FeatureFlag.new("d", "2"), + ] + + event.add_feature_flags(flags) + end + + expect(Bugsnag).to have_sent_notification { |payload, headers| + event = get_event_from_payload(payload) + expect(event["featureFlags"]).to eq([ + { "featureFlag" => "a" }, + { "featureFlag" => "b" }, + { "featureFlag" => "c", "variant" => "1" }, + { "featureFlag" => "d", "variant" => "2" }, + ]) + } + end + + it "can remove a feature flag from the payload" do + Bugsnag.notify(BugsnagTestException.new("It crashed")) do |event| + flags = [ + Bugsnag::FeatureFlag.new("a"), + Bugsnag::FeatureFlag.new("b"), + Bugsnag::FeatureFlag.new("c", "1"), + Bugsnag::FeatureFlag.new("d", "2"), + ] + + event.add_feature_flags(flags) + event.add_feature_flag("e") + + event.clear_feature_flag("b") + event.clear_feature_flag("d") + end + + expect(Bugsnag).to have_sent_notification { |payload, headers| + event = get_event_from_payload(payload) + expect(event["featureFlags"]).to eq([ + { "featureFlag" => "a" }, + { "featureFlag" => "c", "variant" => "1" }, + { "featureFlag" => "e" }, + ]) + } + end + + it "can remove all feature flags from the payload" do + Bugsnag.notify(BugsnagTestException.new("It crashed")) do |event| + flags = [ + Bugsnag::FeatureFlag.new("a"), + Bugsnag::FeatureFlag.new("b"), + Bugsnag::FeatureFlag.new("c", "1"), + Bugsnag::FeatureFlag.new("d", "2"), + ] + + event.add_feature_flags(flags) + event.add_feature_flag("e") + + event.clear_feature_flags + end + + expect(Bugsnag).to have_sent_notification { |payload, headers| + event = get_event_from_payload(payload) + expect(event["featureFlags"]).to eq([]) + } + end + + it "can get feature flags from the event" do + Bugsnag.notify(BugsnagTestException.new("It crashed")) do |event| + flags = [ + Bugsnag::FeatureFlag.new("a"), + Bugsnag::FeatureFlag.new("b"), + Bugsnag::FeatureFlag.new("c", "1"), + Bugsnag::FeatureFlag.new("d", "2"), + ] + + event.add_feature_flags(flags) + event.add_feature_flag("e") + + expect(event.feature_flags).to eq([ + Bugsnag::FeatureFlag.new("a"), + Bugsnag::FeatureFlag.new("b"), + Bugsnag::FeatureFlag.new("c", "1"), + Bugsnag::FeatureFlag.new("d", "2"), + Bugsnag::FeatureFlag.new("e"), + ]) + + event.clear_feature_flags + end + + expect(Bugsnag).to have_sent_notification { |payload, headers| + event = get_event_from_payload(payload) + expect(event["featureFlags"]).to eq([]) + } + end + end end # rubocop:enable Metrics/BlockLength diff --git a/spec/utility/feature_flag_delegate_spec.rb b/spec/utility/feature_flag_delegate_spec.rb new file mode 100644 index 000000000..5e93ddfa4 --- /dev/null +++ b/spec/utility/feature_flag_delegate_spec.rb @@ -0,0 +1,327 @@ +require 'spec_helper' + +describe Bugsnag::Utility::FeatureFlagDelegate do + invalid_names = [ + nil, + true, + false, + 1234, + [1, 2, 3], + { a: 1, b: 2 }, + "", + ] + + it "contains no flags by default" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + expect(delegate.as_json).to eq([]) + end + + it "does not get mutated after being duplicated" do + delegate1 = Bugsnag::Utility::FeatureFlagDelegate.new + delegate1.add('abc', '123') + + delegate2 = delegate1.dup + delegate2.add('xyz', '987') + + expect(delegate1.to_a).to eq([ + Bugsnag::FeatureFlag.new('abc', '123'), + ]) + + expect(delegate2.to_a).to eq([ + Bugsnag::FeatureFlag.new('abc', '123'), + Bugsnag::FeatureFlag.new('xyz', '987'), + ]) + + delegate3 = delegate2.dup + delegate3.clear + + expect(delegate1.to_a).to eq([ + Bugsnag::FeatureFlag.new('abc', '123'), + ]) + + expect(delegate2.to_a).to eq([ + Bugsnag::FeatureFlag.new('abc', '123'), + Bugsnag::FeatureFlag.new('xyz', '987'), + ]) + + expect(delegate3.to_a).to be_empty + end + + describe "#add" do + it "can add flags individually" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.add("abc", "xyz") + delegate.add("another", nil) + delegate.add("a third one", "1234") + + expect(delegate.as_json).to eq([ + { "featureFlag" => "abc", "variant" => "xyz" }, + { "featureFlag" => "another" }, + { "featureFlag" => "a third one", "variant" => "1234" }, + ]) + end + + it "replaces flags by name when the original has no variant" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.add("abc", nil) + delegate.add("another", nil) + delegate.add("abc", "123") + + expect(delegate.as_json).to eq([ + { "featureFlag" => "abc", "variant" => "123" }, + { "featureFlag" => "another" }, + ]) + end + + it "replaces flags by name when the replacement has no variant" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.add("abc", "123") + delegate.add("another", nil) + delegate.add("abc", nil) + + expect(delegate.as_json).to eq([ + { "featureFlag" => "abc" }, + { "featureFlag" => "another" }, + ]) + end + + it "replaces flags by name when both have variants" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.add("abc", "123") + delegate.add("another", nil) + delegate.add("abc", "987") + + expect(delegate.as_json).to eq([ + { "featureFlag" => "abc", "variant" => "987" }, + { "featureFlag" => "another" }, + ]) + end + + invalid_names.each do |name| + it "drops flags when name is '#{name.inspect}'" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.add("abc", "123") + delegate.add(name, nil) + delegate.add("xyz", "987") + + expect(delegate.as_json).to eq([ + { "featureFlag" => "abc", "variant" => "123" }, + { "featureFlag" => "xyz", "variant" => "987" }, + ]) + end + end + end + + describe "#merge" do + it "can add multiple flags at once" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.merge([ + Bugsnag::FeatureFlag.new("a", "xyz"), + Bugsnag::FeatureFlag.new("b"), + Bugsnag::FeatureFlag.new("c", "111"), + Bugsnag::FeatureFlag.new("d"), + ]) + + expect(delegate.as_json).to eq([ + { "featureFlag" => "a", "variant" => "xyz" }, + { "featureFlag" => "b" }, + { "featureFlag" => "c", "variant" => "111" }, + { "featureFlag" => "d" }, + ]) + end + + it "replaces flags by name when the original has no variant" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.add("a", nil) + + delegate.merge([ + Bugsnag::FeatureFlag.new("b"), + Bugsnag::FeatureFlag.new("a", "123"), + ]) + + expect(delegate.as_json).to eq([ + { "featureFlag" => "a", "variant" => "123" }, + { "featureFlag" => "b" }, + ]) + end + + it "replaces flags by name when the replacement has no variant" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.add("a", "123") + + delegate.merge([ + Bugsnag::FeatureFlag.new("b"), + Bugsnag::FeatureFlag.new("a"), + ]) + + expect(delegate.as_json).to eq([ + { "featureFlag" => "a" }, + { "featureFlag" => "b" }, + ]) + end + + it "replaces flags by name when both have variants" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.add("a", "987") + + delegate.merge([ + Bugsnag::FeatureFlag.new("b"), + Bugsnag::FeatureFlag.new("a", "123"), + ]) + + expect(delegate.as_json).to eq([ + { "featureFlag" => "a", "variant" => "123" }, + { "featureFlag" => "b" }, + ]) + end + + it "ignores anything that isn't a FeatureFlag instance" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.merge([ + Bugsnag::FeatureFlag.new("a", "xyz"), + 1234, + Bugsnag::FeatureFlag.new("b"), + "hello", + Bugsnag::FeatureFlag.new("c", "111"), + RuntimeError.new("xyz"), + Bugsnag::FeatureFlag.new("d"), + nil, + ]) + + expect(delegate.as_json).to eq([ + { "featureFlag" => "a", "variant" => "xyz" }, + { "featureFlag" => "b" }, + { "featureFlag" => "c", "variant" => "111" }, + { "featureFlag" => "d" }, + ]) + end + + invalid_names.each do |name| + it "drops flag when name is '#{name.inspect}'" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.merge([ + Bugsnag::FeatureFlag.new("abc", "123"), + Bugsnag::FeatureFlag.new(name, "456"), + Bugsnag::FeatureFlag.new("xyz", "789"), + ]) + + expect(delegate.as_json).to eq([ + { "featureFlag" => "abc", "variant" => "123" }, + { "featureFlag" => "xyz", "variant" => "789" }, + ]) + end + end + end + + describe "#remove" do + it "can remove flags by name" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.add("abc", "xyz") + delegate.add("another", nil) + delegate.add("a third one", "1234") + + delegate.remove("abc") + delegate.remove("a third one") + + expect(delegate.as_json).to eq([ + { "featureFlag" => "another" }, + ]) + end + + it "does nothing when no flag exists with the given name" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.add("abc", "xyz") + delegate.remove("xyz") + + expect(delegate.as_json).to eq([ + { "featureFlag" => "abc", "variant" => "xyz" }, + ]) + end + end + + describe "#clear" do + it "can remove all flags at once" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.add("abc", "xyz") + delegate.add("another", nil) + delegate.add("a third one", "1234") + + delegate.clear + + expect(delegate.as_json).to eq([]) + end + + it "does nothing when there are no flags" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.clear + + expect(delegate.as_json).to eq([]) + end + end + + describe "#to_a" do + it "returns an empty array when there are no feature flags" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + expect(delegate.to_a).to eq([]) + end + + it "returns an array of feature flags" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.add("abc", "xyz") + delegate.add("another", nil) + delegate.add("a third one", "1234") + + expect(delegate.to_a).to eq([ + Bugsnag::FeatureFlag.new("abc", "xyz"), + Bugsnag::FeatureFlag.new("another"), + Bugsnag::FeatureFlag.new("a third one", "1234"), + ]) + end + + it "can be mutated without affecting the internal storage" do + delegate = Bugsnag::Utility::FeatureFlagDelegate.new + + delegate.add("abc", "xyz") + delegate.add("another", nil) + delegate.add("a third one", "1234") + + flags = delegate.to_a + + expected = [ + Bugsnag::FeatureFlag.new("abc", "xyz"), + Bugsnag::FeatureFlag.new("another"), + Bugsnag::FeatureFlag.new("a third one", "1234"), + ] + + expect(flags).to eq(expected) + + flags.pop + flags.pop + flags.push(1234) + + expect(delegate.to_a).to eq(expected) + expect(flags).to eq([ + Bugsnag::FeatureFlag.new("abc", "xyz"), + 1234, + ]) + end + end +end