diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/01-feature.md similarity index 51% rename from .github/ISSUE_TEMPLATE/feature_request.md rename to .github/ISSUE_TEMPLATE/01-feature.md index 8937d5bc0..64a5a5424 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/01-feature.md @@ -1,10 +1,7 @@ --- -name: Feature Request -about: Propose a new feature of the Ruby SDK -title: '' -labels: enhancement -assignees: - +name: 💡 Feature Request +about: Propose new functionality for the SDK +type: Feature --- **Describe the idea** diff --git a/.github/ISSUE_TEMPLATE/02-improvement.md b/.github/ISSUE_TEMPLATE/02-improvement.md new file mode 100644 index 000000000..121c7277c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-improvement.md @@ -0,0 +1,11 @@ +--- +name: 💡 Improvement +about: Propose an improvement for existing functionality of the SDK +type: Improvement +--- + +**Describe the idea** + +**Why do you think it's beneficial to most of the users** + +**Possible implementation** diff --git a/.github/ISSUE_TEMPLATE/03-bug.yml b/.github/ISSUE_TEMPLATE/03-bug.yml new file mode 100644 index 000000000..81a5e224c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03-bug.yml @@ -0,0 +1,45 @@ +name: 🐞 Bug Report +description: "Report an unexpected problem or behavior of this SDK" +type: Bug +body: + - type: textarea + attributes: + label: Issue Description + validations: + required: true + - type: textarea + attributes: + label: Reproduction Steps + validations: + required: true + - type: textarea + attributes: + label: Expected Behavior + validations: + required: true + - type: textarea + attributes: + label: Actual Behavior + validations: + required: true + - type: input + attributes: + label: Ruby Version + validations: + required: true + - type: input + attributes: + label: SDK Version + validations: + required: true + - type: input + attributes: + label: Integration and Its Version + description: e.g. Rails/Sidekiq/Rake/DelayedJob...etc. + validations: + required: false + - type: textarea + attributes: + label: Sentry Config + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index 216f73087..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Bug report -description: "Report an SDK issue" -labels: bug -assignees: sl0thentr0py -body: -- type: textarea - attributes: - label: Issue Description - validations: - required: true -- type: textarea - attributes: - label: Reproduction Steps - validations: - required: true -- type: textarea - attributes: - label: Expected Behavior - validations: - required: true -- type: textarea - attributes: - label: Actual Behavior - validations: - required: true -- type: input - attributes: - label: Ruby Version - validations: - required: true -- type: input - attributes: - label: SDK Version - validations: - required: true -- type: input - attributes: - label: Integration and Its Version - description: e.g. Rails/Sidekiq/Rake/DelayedJob...etc. - validations: - required: false -- type: textarea - attributes: - label: Sentry Config - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 79c87a961..000000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: Question -about: Ask questions about the Ruby SDK -title: '' -labels: question -assignees: sl0thentr0py - ---- - diff --git a/.github/workflows/build_batch_release.yml b/.github/workflows/build_batch_release.yml index cb0639992..9e8ed0776 100644 --- a/.github/workflows/build_batch_release.yml +++ b/.github/workflows/build_batch_release.yml @@ -9,15 +9,15 @@ jobs: name: Build gems runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 + ruby-version: 3.4 - name: Build gem source run: ruby .scripts/batch_build.rb - name: Archive Artifacts - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v4 with: name: ${{ github.sha }} path: sentry*/*.gem diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index f9cad4c6e..ac02f9a3e 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -9,12 +9,12 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 + ruby-version: 3.4 - name: Capture sdk name uses: actions-ecosystem/action-regex-match@v2 id: regex-match @@ -27,7 +27,7 @@ jobs: working-directory: ${{env.sdk-directory}} run: make build - name: Archive Artifacts - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v4 with: name: ${{ github.sha }} path: ${{env.sdk-directory}}/*.gem diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index bd6958c10..a3cd6b1ab 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 43180011d..5a0a66f0b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,11 +6,11 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: "3.3" + ruby-version: "3.4" bundler-cache: true - name: Run rubocop run: bundle exec rubocop diff --git a/.github/workflows/prepare_batch_release.yml b/.github/workflows/prepare_batch_release.yml index 38a785f29..4fbdce88a 100644 --- a/.github/workflows/prepare_batch_release.yml +++ b/.github/workflows/prepare_batch_release.yml @@ -13,14 +13,20 @@ jobs: runs-on: ubuntu-latest name: "Release a new version" steps: - - uses: actions/checkout@v2 + - name: Get auth token + id: token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 with: - token: ${{ secrets.GH_RELEASE_PAT }} + app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} + private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} + - uses: actions/checkout@v4 + with: + token: ${{ steps.token.outputs.token }} fetch-depth: 0 - name: Prepare release uses: getsentry/action-prepare-release@v1 env: - GITHUB_TOKEN: ${{ secrets.GH_RELEASE_PAT }} + GITHUB_TOKEN: ${{ steps.token.outputs.token }} with: version: ${{ github.event.inputs.version }} force: ${{ github.event.inputs.force }} diff --git a/.github/workflows/prepare_raven_release.yml b/.github/workflows/prepare_raven_release.yml index 7c8504610..871401fcf 100644 --- a/.github/workflows/prepare_raven_release.yml +++ b/.github/workflows/prepare_raven_release.yml @@ -13,14 +13,20 @@ jobs: runs-on: ubuntu-latest name: "Release a new version" steps: - - uses: actions/checkout@v2 + - name: Get auth token + id: token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 with: - token: ${{ secrets.GH_RELEASE_PAT }} + app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} + private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} + - uses: actions/checkout@v4 + with: + token: ${{ steps.token.outputs.token }} fetch-depth: 0 - name: Prepare release uses: getsentry/action-prepare-release@v1 env: - GITHUB_TOKEN: ${{ secrets.GH_RELEASE_PAT }} + GITHUB_TOKEN: ${{ steps.token.outputs.token }} with: path: sentry-raven version: ${{ github.event.inputs.version }} diff --git a/.github/workflows/sentry_delayed_job_test.yml b/.github/workflows/sentry_delayed_job_test.yml index 206c6eeb7..d162c5cd4 100644 --- a/.github/workflows/sentry_delayed_job_test.yml +++ b/.github/workflows/sentry_delayed_job_test.yml @@ -25,7 +25,12 @@ jobs: working-directory: sentry-delayed_job name: Ruby ${{ matrix.ruby_version }}, options - ${{ toJson(matrix.options) }} runs-on: ubuntu-latest + env: + RUBYOPT: ${{ matrix.options.rubyopt }} + BUNDLE_GEMFILE: ${{ github.workspace }}/sentry-delayed_job/Gemfile + BUNDLE_WITHOUT: rubocop strategy: + fail-fast: false matrix: ruby_version: ${{ fromJson(needs.ruby-versions.outputs.versions) }} include: @@ -42,8 +47,9 @@ jobs: # LoadError: # cannot load such file -- mutex_m - { ruby_version: "head" } + - { ruby_version: 'jruby-head' } steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Install sqlite run: | # See https://github.community/t5/GitHub-Actions/ubuntu-latest-Apt-repository-list-issues/td-p/41122/page/2 @@ -55,16 +61,13 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby_version }} + bundler-cache: true - name: Run specs - env: - RUBYOPT: ${{ matrix.options.rubyopt }} - run: | - bundle install --jobs 4 --retry 3 - bundle exec rake + run: bundle exec rake - name: Upload Coverage if: ${{ matrix.options.codecov }} - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/sentry_opentelemetry_test.yml b/.github/workflows/sentry_opentelemetry_test.yml index 1aba897ba..fbe31e222 100644 --- a/.github/workflows/sentry_opentelemetry_test.yml +++ b/.github/workflows/sentry_opentelemetry_test.yml @@ -25,7 +25,13 @@ jobs: working-directory: sentry-opentelemetry name: Ruby ${{ matrix.ruby_version }} & OpenTelemetry ${{ matrix.opentelemetry_version }}, options - ${{ toJson(matrix.options) }} runs-on: ubuntu-latest + env: + RUBYOPT: ${{ matrix.options.rubyopt }} + BUNDLE_GEMFILE: ${{ github.workspace }}/sentry-opentelemetry/Gemfile + BUNDLE_WITHOUT: rubocop + OPENTELEMETRY_VERSION: ${{ matrix.opentelemetry_version }} strategy: + fail-fast: false matrix: ruby_version: ${{ fromJson(needs.ruby-versions.outputs.versions) }} # opentelemetry_version: [1.2.0] @@ -37,9 +43,10 @@ jobs: rubyopt: "--enable-frozen-string-literal --debug=frozen-string-literal", }, } - - { ruby_version: 3.2, options: { codecov: 1 } } + exclude: + - { ruby_version: 'jruby-head' } steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby_version }} uses: ruby/setup-ruby@v1 @@ -48,15 +55,9 @@ jobs: bundler-cache: true - name: Run specs - env: - RUBYOPT: ${{ matrix.options.rubyopt }} - OPENTELEMETRY_VERSION: ${{ matrix.opentelemetry_version }} - run: | - bundle install --jobs 4 --retry 3 - bundle exec rake + run: bundle exec rake - name: Upload Coverage - if: ${{ matrix.options.codecov }} - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/sentry_rails_test.yml b/.github/workflows/sentry_rails_test.yml index ceabdc6af..4573aae36 100644 --- a/.github/workflows/sentry_rails_test.yml +++ b/.github/workflows/sentry_rails_test.yml @@ -19,6 +19,11 @@ jobs: working-directory: sentry-rails name: Ruby ${{ matrix.ruby_version }} & Rails ${{ matrix.rails_version }}, options - ${{ toJson(matrix.options) }} runs-on: ubuntu-latest + env: + RUBYOPT: ${{ matrix.options.rubyopt }} + BUNDLE_GEMFILE: ${{ github.workspace }}/sentry-rails/Gemfile + BUNDLE_WITHOUT: rubocop + RAILS_VERSION: ${{ matrix.rails_version }} strategy: fail-fast: false matrix: @@ -43,6 +48,13 @@ jobs: - { ruby_version: "2.7", rails_version: 5.2.0 } - { ruby_version: "2.7", rails_version: 6.0.0 } - { ruby_version: "2.7", rails_version: 6.1.0 } + - { ruby_version: "3.1", rails_version: 7.2.0 } + - { ruby_version: "3.2", rails_version: 7.2.0 } + - { ruby_version: "3.3", rails_version: 7.2.0 } + - { ruby_version: "3.4", rails_version: 7.2.0 } + - { ruby_version: "3.2", rails_version: "8.0.0" } + - { ruby_version: "3.3", rails_version: "8.0.0" } + - { ruby_version: "3.4", rails_version: "8.0.0" } - { ruby_version: "jruby", rails_version: 6.1.0 } - { ruby_version: "3.2", @@ -54,11 +66,10 @@ jobs: } - { ruby_version: "3.2", - rails_version: 7.1.0, - options: { codecov: 1 }, + rails_version: 7.1.0 } steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Install sqlite and ImageMagick run: | # See https://github.community/t5/GitHub-Actions/ubuntu-latest-Apt-repository-list-issues/td-p/41122/page/2 @@ -72,15 +83,9 @@ jobs: bundler-cache: true - name: Build with Rails ${{ matrix.rails_version }} - env: - RAILS_VERSION: ${{ matrix.rails_version }} - RUBYOPT: ${{ matrix.options.rubyopt }} - run: | - bundle install --jobs 4 --retry 3 - bundle exec rake + run: bundle exec rake - name: Upload Coverage - if: ${{ matrix.options.codecov }} - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/sentry_raven_test.yml b/.github/workflows/sentry_raven_test.yml index c1732dd85..4e120cb24 100644 --- a/.github/workflows/sentry_raven_test.yml +++ b/.github/workflows/sentry_raven_test.yml @@ -48,7 +48,7 @@ jobs: rails_version: 6.0.0 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby_version }} uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/sentry_resque_test.yml b/.github/workflows/sentry_resque_test.yml index bdd71e6ce..861187baa 100644 --- a/.github/workflows/sentry_resque_test.yml +++ b/.github/workflows/sentry_resque_test.yml @@ -25,7 +25,12 @@ jobs: working-directory: sentry-resque name: Ruby ${{ matrix.ruby_version }}, options - ${{ toJson(matrix.options) }} runs-on: ubuntu-latest + env: + RUBYOPT: ${{ matrix.options.rubyopt }} + BUNDLE_GEMFILE: ${{ github.workspace }}/sentry-resque/Gemfile + BUNDLE_WITHOUT: rubocop strategy: + fail-fast: false matrix: ruby_version: ${{ fromJson(needs.ruby-versions.outputs.versions) }} include: @@ -36,9 +41,10 @@ jobs: rubyopt: "--enable-frozen-string-literal --debug=frozen-string-literal", }, } - - { ruby_version: "3.2", options: { codecov: 1 } } + exclude: + - { ruby_version: 'jruby-head' } steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby_version }} uses: ruby/setup-ruby@v1 with: @@ -50,23 +56,17 @@ jobs: with: redis-version: 5 - - name: Run specs + - name: Run specs without Rails env: RUBYOPT: ${{ matrix.options.rubyopt }} - run: | - bundle install --jobs 4 --retry 3 - bundle exec rake + run: BUNDLE_WITHOUT="rubocop rails" bundle exec rake - name: Run specs with Rails env: - BUNDLE_GEMFILE: Gemfile_with_rails.rb RUBYOPT: ${{ matrix.options.rubyopt }} - run: | - bundle install --jobs 4 --retry 3 - bundle exec rake + run: bundle exec rake - name: Upload Coverage - if: ${{ matrix.options.codecov }} - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/sentry_ruby_test.yml b/.github/workflows/sentry_ruby_test.yml index 2e6f64126..b2b90e2a6 100644 --- a/.github/workflows/sentry_ruby_test.yml +++ b/.github/workflows/sentry_ruby_test.yml @@ -25,10 +25,18 @@ jobs: working-directory: sentry-ruby name: Ruby ${{ matrix.ruby_version }} & Rack ${{ matrix.rack_version }}, options - ${{ toJson(matrix.options) }} runs-on: ubuntu-latest + timeout-minutes: 10 + env: + RUBYOPT: ${{ matrix.options.rubyopt }} + BUNDLE_GEMFILE: ${{ github.workspace }}/sentry-ruby/Gemfile + BUNDLE_WITHOUT: rubocop + RACK_VERSION: ${{ matrix.rack_version }} + REDIS_RB_VERSION: ${{ matrix.redis_rb_version }} strategy: + fail-fast: false matrix: ruby_version: ${{ fromJson(needs.ruby-versions.outputs.versions) }} - rack_version: [2.0, 3.0] + rack_version: [2.0, 3.0, 3.1] redis_rb_version: [4.0] include: - { ruby_version: 3.2, rack_version: 0, redis_rb_version: 5.0 } @@ -44,12 +52,22 @@ jobs: } - { ruby_version: 3.2, - rack_version: 3.0, - redis_rb_version: 5.0, - options: { codecov: 1 }, + rack_version: 3.0 + } + - { + ruby_version: 3.3, + rack_version: 3.1, + redis_rb_version: 5.3 + } + - { + ruby_version: 3.4, + rack_version: 3.1, + redis_rb_version: 5.3 } + exclude: + - { ruby_version: 'jruby-head' } steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby_version }} uses: ruby/setup-ruby@v1 @@ -63,16 +81,9 @@ jobs: redis-version: 6 - name: Run specs with Rack ${{ matrix.rack_version }} and redis-rb ${{ matrix.redis_rb_version }} - env: - RUBYOPT: ${{ matrix.options.rubyopt }} - RACK_VERSION: ${{ matrix.rack_version }} - REDIS_RB_VERSION: ${{ matrix.redis_rb_version }} - run: | - bundle install --jobs 4 --retry 3 - bundle exec rake + run: bundle exec rake - name: Upload Coverage - if: ${{ matrix.options.codecov }} - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/sentry_sidekiq_test.yml b/.github/workflows/sentry_sidekiq_test.yml index 06bb45a9d..9ef84ff1a 100644 --- a/.github/workflows/sentry_sidekiq_test.yml +++ b/.github/workflows/sentry_sidekiq_test.yml @@ -19,10 +19,16 @@ jobs: working-directory: sentry-sidekiq name: Ruby ${{ matrix.ruby_version }} & Sidekiq ${{ matrix.sidekiq_version }}, options - ${{ toJson(matrix.options) }} runs-on: ubuntu-latest + env: + RUBYOPT: ${{ matrix.options.rubyopt }} + BUNDLE_GEMFILE: ${{ github.workspace }}/sentry-sidekiq/Gemfile + BUNDLE_WITHOUT: rubocop + SIDEKIQ_VERSION: ${{ matrix.sidekiq_version }} strategy: + fail-fast: false matrix: sidekiq_version: ["5.0", "6.5", "7.0"] - ruby_version: ["2.7", "3.0", "3.1", "3.2", "3.3", jruby] + ruby_version: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4", jruby] include: - { ruby_version: 2.4, sidekiq_version: 5.0 } - { ruby_version: 2.5, sidekiq_version: 5.0 } @@ -40,13 +46,11 @@ jobs: rubyopt: "--enable-frozen-string-literal --debug=frozen-string-literal", }, } - - { - ruby_version: "3.2", - sidekiq_version: 7.0, - options: { codecov: 1 }, - } + - { ruby_version: "3.2", sidekiq_version: 7.0 } + - { ruby_version: "3.3", sidekiq_version: 7.0 } + - { ruby_version: "3.4", sidekiq_version: 7.0 } steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby_version }} uses: ruby/setup-ruby@v1 @@ -61,14 +65,10 @@ jobs: - name: Run specs with Sidekiq ${{ matrix.sidekiq_version }} env: - SIDEKIQ_VERSION: ${{ matrix.sidekiq_version }} - RUBYOPT: ${{ matrix.options.rubyopt }} - run: | - bundle install --jobs 4 --retry 3 - make test + WITH_SENTRY_RAILS: 1 + run: bundle exec rake - name: Upload Coverage - if: ${{ matrix.options.codecov }} - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 0b513cc74..7e3c680b2 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,6 @@ Gemfile.lock .ruby-gemset .idea *.rdb +.rgignore examples/**/node_modules diff --git a/.rubocop.yml b/.rubocop.yml index cb409d1a8..9286a27d3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,6 +4,17 @@ inherit_gem: Layout/SpaceInsideArrayLiteralBrackets: Enabled: false +Layout/EmptyLineAfterMagicComment: + Enabled: true + +Style/FrozenStringLiteralComment: + Enabled: true + +Style/RedundantFreeze: + Enabled: true + AllCops: Exclude: - - 'sentry-raven/**/*' + - "sentry-raven/**/*" + - "sentry-*/tmp/**/*" + - "sentry-*/examples/**/*" diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aea96c8a..954b4ac33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,168 @@ +## Unreleased + +### Features + +- Improve the accuracy of duration calculations in cron jobs monitoring ([#2471](https://github.com/getsentry/sentry-ruby/pull/2471)) +- Use attempt_threshold to skip reporting on first N attempts ([#2503](https://github.com/getsentry/sentry-ruby/pull/2503)) +- Support `code.namespace` for Ruby 3.4+ stacktraces ([#2506](https://github.com/getsentry/sentry-ruby/pull/2506)) + +### Bug fixes + +- Default to `internal_error` error type for OpenTelemetry spans [#2473](https://github.com/getsentry/sentry-ruby/pull/2473) + +### Internal + +- Test Ruby 3.4 in CI ([#2506](https://github.com/getsentry/sentry-ruby/pull/2506)) +- Upgrade actions workflows versions ([#2506](https://github.com/getsentry/sentry-ruby/pull/2506)) + +## 5.22.1 + +### Bug Fixes + +- Safe-navigate to session flusher [#2396](https://github.com/getsentry/sentry-ruby/pull/2396) +- Fix latency related nil error for Sidekiq Queues Module span data [#2486](https://github.com/getsentry/sentry-ruby/pull/2486) + - Fixes [#2485](https://github.com/getsentry/sentry-ruby/issues/2485) + +## 5.22.0 + +### Features + +- Add `include_sentry_event` matcher for RSpec [#2424](https://github.com/getsentry/sentry-ruby/pull/2424) +- Add support for Sentry Cache instrumentation, when using Rails.cache [#2380](https://github.com/getsentry/sentry-ruby/pull/2380) + Note: MemoryStore and FileStore require Rails 8.0+ +- Add support for Queue Instrumentation for Sidekiq. [#2403](https://github.com/getsentry/sentry-ruby/pull/2403) +- Add support for string errors in error reporter ([#2464](https://github.com/getsentry/sentry-ruby/pull/2464)) +- Reset `trace_id` and add root transaction for sidekiq-cron [#2446](https://github.com/getsentry/sentry-ruby/pull/2446) +- Add support for Excon HTTP client instrumentation ([#2383](https://github.com/getsentry/sentry-ruby/pull/2383)) + +### Bug Fixes + +- Ignore internal Sidekiq::JobRetry::Handled exception [#2337](https://github.com/getsentry/sentry-ruby/pull/2337) +- Fix Vernier profiler not stopping when already stopped [#2429](https://github.com/getsentry/sentry-ruby/pull/2429) +- Fix `send_default_pii` handling in rails controller spans [#2443](https://github.com/getsentry/sentry-ruby/pull/2443) + - Fixes [#2438](https://github.com/getsentry/sentry-ruby/issues/2438) +- Fix `RescuedExceptionInterceptor` to handle an empty configuration [#2428](https://github.com/getsentry/sentry-ruby/pull/2428) +- Add mutex sync to `SessionFlusher` aggregates [#2469](https://github.com/getsentry/sentry-ruby/pull/2469) + - Fixes [#2468](https://github.com/getsentry/sentry-ruby/issues/2468) +- Fix sentry-rails' backtrace cleaner issues ([#2475](https://github.com/getsentry/sentry-ruby/pull/2475)) + - Fixes [#2472](https://github.com/getsentry/sentry-ruby/issues/2472) + +## 5.21.0 + +### Features + +- Experimental support for multi-threaded profiling using [Vernier](https://github.com/jhawthorn/vernier) ([#2372](https://github.com/getsentry/sentry-ruby/pull/2372)) + + You can have much better profiles if you're using multi-threaded servers like Puma now by leveraging Vernier. + To use it, first add `vernier` to your `Gemfile` and make sure it is loaded before `sentry-ruby`. + + ```ruby + # Gemfile + + gem 'vernier' + gem 'sentry-ruby' + ``` + + Then, set a `profiles_sample_rate` and the new `profiler_class` configuration in your sentry initializer to use the new profiler. + + ```ruby + # config/initializers/sentry.rb + + Sentry.init do |config| + # ... + config.profiles_sample_rate = 1.0 + config.profiler_class = Sentry::Vernier::Profiler + end + ``` + +### Internal + +- Profile items have bigger size limit now ([#2421](https://github.com/getsentry/sentry-ruby/pull/2421)) +- Consistent string freezing ([#2422](https://github.com/getsentry/sentry-ruby/pull/2422)) + +## 5.20.1 + +### Bug Fixes + +- Skip `rubocop.yml` in `spec.files` ([#2420](https://github.com/getsentry/sentry-ruby/pull/2420)) + +## 5.20.0 + +- Add support for `$SENTRY_DEBUG` and `$SENTRY_SPOTLIGHT` ([#2374](https://github.com/getsentry/sentry-ruby/pull/2374)) +- Support human readable intervals in `sidekiq-cron` ([#2387](https://github.com/getsentry/sentry-ruby/pull/2387)) +- Set default app dirs pattern ([#2390](https://github.com/getsentry/sentry-ruby/pull/2390)) +- Add new `strip_backtrace_load_path` boolean config (default true) to enable disabling load path stripping ([#2409](https://github.com/getsentry/sentry-ruby/pull/2409)) + +### Bug Fixes + +- Fix error events missing a DSC when there's an active span ([#2408](https://github.com/getsentry/sentry-ruby/pull/2408)) +- Verifies presence of client before adding a breadcrumb ([#2394](https://github.com/getsentry/sentry-ruby/pull/2394)) +- Fix `Net:HTTP` integration for non-ASCII URI's ([#2417](https://github.com/getsentry/sentry-ruby/pull/2417)) +- Prevent Hub from having nil scope and client ([#2402](https://github.com/getsentry/sentry-ruby/pull/2402)) + +## 5.19.0 + +### Features + +- Use `Concurrent.available_processor_count` instead of `Concurrent.usable_processor_count` ([#2358](https://github.com/getsentry/sentry-ruby/pull/2358)) + +- Support for tracing Faraday requests ([#2345](https://github.com/getsentry/sentry-ruby/pull/2345)) + - Closes [#1795](https://github.com/getsentry/sentry-ruby/issues/1795) + - Please note that the Faraday instrumentation has some limitations in case of async requests: + + Usage: + + ```rb + Sentry.init do |config| + # ... + config.enabled_patches << :faraday + end + ``` + +- Support for attachments ([#2357](https://github.com/getsentry/sentry-ruby/pull/2357)) + + Usage: + + ```ruby + Sentry.add_attachment(path: '/foo/bar.txt') + Sentry.add_attachment(filename: 'payload.json', bytes: '{"value": 42}')) + ``` + +- Transaction data are now included in the context ([#2365](https://github.com/getsentry/sentry-ruby/pull/2365)) + - Closes [#2363](https://github.com/getsentry/sentry-ruby/issues/2363) + +- Inject Sentry meta tags in the Rails application layout automatically in the generator ([#2369](https://github.com/getsentry/sentry-ruby/pull/2369)) + + To turn this behavior off, use + + ```bash + bin/rails generate sentry --inject-meta false + ``` + +### Bug Fixes + +- Fix skipping `connect` spans in open-telemetry [#2364](https://github.com/getsentry/sentry-ruby/pull/2364) + +## 5.18.2 + +### Bug Fixes + +- Don't overwrite `ip_address` if already set on `user` [#2350](https://github.com/getsentry/sentry-ruby/pull/2350) + - Fixes [#2347](https://github.com/getsentry/sentry-ruby/issues/2347) +- `teardown_sentry_test` helper should clear global even processors too ([#2342](https://github.com/getsentry/sentry-ruby/pull/2342)) +- Suppress the unnecessary “unsupported options notice” ([#2349](https://github.com/getsentry/sentry-ruby/pull/2349)) + +### Internal + +- Use `Concurrent.usable_processor_count` when it is available ([#2339](https://github.com/getsentry/sentry-ruby/pull/2339)) +- Report dropped spans in Client Reports ([#2346](https://github.com/getsentry/sentry-ruby/pull/2346)) + +## 5.18.1 + +### Bug Fixes + +- Drop `Gem::Specification`'s usage so it doesn't break bundler standalone ([#2335](https://github.com/getsentry/sentry-ruby/pull/2335)) + ## 5.18.0 ### Features @@ -194,6 +359,7 @@ config.cron.default_timezone = 'America/New_York' end ``` + - Clean up logging [#2216](https://github.com/getsentry/sentry-ruby/pull/2216) - Pick up config.cron.default_timezone from Rails config [#2213](https://github.com/getsentry/sentry-ruby/pull/2213) - Don't add most scope data (tags/extra/breadcrumbs) to `CheckInEvent` [#2217](https://github.com/getsentry/sentry-ruby/pull/2217) @@ -241,6 +407,7 @@ ```rb config.enabled_patches += [:sidekiq_cron] ``` + - Add support for [`sidekiq-scheduler`](https://github.com/sidekiq-scheduler/sidekiq-scheduler) [#2172](https://github.com/getsentry/sentry-ruby/pull/2172) You can opt in to the `sidekiq-scheduler` patch and we will automatically monitor check-ins for all repeating jobs (i.e. `cron`, `every`, and `interval`) specified in the config. @@ -269,6 +436,7 @@ config.rails.active_support_logger_subscription_items.delete("sql.active_record") config.rails.active_support_logger_subscription_items["foo"] = :bar ``` + - Enable opting out of patches [#2151](https://github.com/getsentry/sentry-ruby/pull/2151) ### Bug Fixes @@ -296,6 +464,7 @@ # do job stuff Sentry.capture_check_in('job_name', :ok, check_in_id: check_in_id) ``` + - Add `Sentry::Cron::MonitorCheckIns` module for automatic monitoring of jobs [#2130](https://github.com/getsentry/sentry-ruby/pull/2130) Standard job frameworks such as `ActiveJob` and `Sidekiq` can now use this module to automatically capture check ins. @@ -326,6 +495,7 @@ ``` You can pass in optional attributes to `sentry_monitor_check_ins` as follows. + ```rb # slug defaults to the job class name sentry_monitor_check_ins slug: 'custom_slug' @@ -365,6 +535,7 @@ config.trace_propagation_targets = [/.*/] # default is to all targets config.trace_propagation_targets = [/example.com/, 'foobar.org/api/v2'] ``` + - Tracing without Performance - Implement `PropagationContext` on `Scope` and add `Sentry.get_trace_propagation_headers` API [#2084](https://github.com/getsentry/sentry-ruby/pull/2084) - Implement `Sentry.continue_trace` API [#2089](https://github.com/getsentry/sentry-ruby/pull/2089) @@ -404,7 +575,6 @@ - Use allowlist to filter `ActiveSupport` breadcrumbs' data [#2048](https://github.com/getsentry/sentry-ruby/pull/2048) - ErrorHandler should cleanup the scope ([#2059](https://github.com/getsentry/sentry-ruby/pull/2059)) - ## 5.9.0 ### Features @@ -422,6 +592,7 @@ Sentry.capture_exception(ignored_exception) # won't be sent to Sentry Sentry.capture_exception(ignored_exception, hint: { ignore_exclusions: true }) # will be sent to Sentry ``` + - Support capturing low-level errors propagated to Puma [#2026](https://github.com/getsentry/sentry-ruby/pull/2026) - Add `spec` to `Backtrace::APP_DIRS_PATTERN` [#2029](https://github.com/getsentry/sentry-ruby/pull/2029) @@ -460,7 +631,7 @@ > **Warning** > Profiling is currently in beta. Beta features are still in-progress and may have bugs. We recognize the irony. - > If you have any questions or feedback, please email us at profiling@sentry.io, reach out via Discord (#profiling), or open an issue. + > If you have any questions or feedback, please email us at , reach out via Discord (#profiling), or open an issue. ### Bug Fixes @@ -555,8 +726,8 @@ ``` - Use `Sentry.with_child_span` in redis and net/http instead of `span.start_child` [#1920](https://github.com/getsentry/sentry-ruby/pull/1920) - - This might change the nesting of some spans and make it more accurate - - Followup fix to set the sentry-trace header in the correct place [#1922](https://github.com/getsentry/sentry-ruby/pull/1922) + - This might change the nesting of some spans and make it more accurate + - Followup fix to set the sentry-trace header in the correct place [#1922](https://github.com/getsentry/sentry-ruby/pull/1922) - Use `Exception#detailed_message` when generating exception message if applicable [#1924](https://github.com/getsentry/sentry-ruby/pull/1924) - Make `sentry-sidekiq` compatible with Sidekiq 7 [#1930](https://github.com/getsentry/sentry-ruby/pull/1930) @@ -564,10 +735,10 @@ ### Bug Fixes - `Sentry::BackgroundWorker` will release `ActiveRecord` connection pool only when the `ActiveRecord` connection is established -- Remove bad encoding arguments in redis span descriptions [#1914](https://github.com/getsentry/sentry-ruby/pull/1914) - - Fixes [#1911](https://github.com/getsentry/sentry-ruby/issues/1911) +- Remove bad encoding arguments in redis span descriptions [#1914](https://github.com/getsentry/sentry-ruby/pull/1914) + - Fixes [#1911](https://github.com/getsentry/sentry-ruby/issues/1911) - Add missing `initialized?` checks to `sentry-rails` [#1919](https://github.com/getsentry/sentry-ruby/pull/1919) - - Fixes [#1885](https://github.com/getsentry/sentry-ruby/issues/1885) + - Fixes [#1885](https://github.com/getsentry/sentry-ruby/issues/1885) - Update Tracing Span's op names [#1923](https://github.com/getsentry/sentry-ruby/pull/1923) Currently, Ruby integrations' Span op names aren't aligned with the core specification's convention, so we decided to update them altogether in this PR. @@ -644,6 +815,7 @@ 1/0 #=> ZeroDivisionError will be reported and re-raised end ``` + - Prepare for Rails 7.1's error reporter API change [#1834](https://github.com/getsentry/sentry-ruby/pull/1834) - Set `sentry.error_event_id` in request env if the middleware captures errors [#1849](https://github.com/getsentry/sentry-ruby/pull/1849) @@ -796,7 +968,6 @@ end This will help users report size-related issues in the future. - - Automatic session tracking [#1715](https://github.com/getsentry/sentry-ruby/pull/1715) **Example**: @@ -815,7 +986,6 @@ end To disable this feature, set `config.auto_session_tracking` to `false`. - ### Bug Fixes - Require set library [#1753](https://github.com/getsentry/sentry-ruby/pull/1753) @@ -830,7 +1000,6 @@ end - Avoid duplicated capturing on the same exception object [#1738](https://github.com/getsentry/sentry-ruby/pull/1738) - Fixes [#1731](https://github.com/getsentry/sentry-ruby/issues/1731) - ### Refactoring - Encapsulate extension helpers [#1725](https://github.com/getsentry/sentry-ruby/pull/1725) @@ -895,7 +1064,6 @@ end This version removes the dependency of [faraday](https://github.com/lostisland/faraday) and replaces related implementation with the `Net::HTTP` standard library. - #### Why? Since the old `sentry-raven` SDK, we've been using `faraday` as the HTTP client for years (see [HTTPTransport](https://github.com/getsentry/sentry-ruby/blob/4-9/sentry-ruby/lib/sentry/transport/http_transport.rb)). It's an amazing tool that saved us many work and allowed us to focus on SDK features. @@ -910,10 +1078,8 @@ And with the release of [faraday 2.0](https://github.com/lostisland/faraday/rele So we think it's time to say goodbye to it with this release. - #### What's changed? - By default, the SDK used `faraday`'s `net_http` adapter, which is also built on top of `Net::HTTP`. So this change shouldn't impact most of the users. The only noticeable changes are the removal of 2 faraday-specific transport configurations: @@ -1014,7 +1180,6 @@ end 2. Set `config.transport.transport = FaradayTransport` - **Please keep in mind that this may not work in the future when the SDK changes its `HTTPTransport` implementation.** ## 4.9.2 @@ -1154,7 +1319,6 @@ When `config.send_default_pii` is set as `true`, `:http_logger` will include que - Start Testing Against Rails 7.0 [#1581](https://github.com/getsentry/sentry-ruby/pull/1581) - ## 4.7.3 - Avoid leaking tracing timestamp to breadcrumbs [#1575](https://github.com/getsentry/sentry-ruby/pull/1575) @@ -1173,6 +1337,7 @@ When `config.send_default_pii` is set as `true`, `:http_logger` will include que ## 4.7.1 ### Bug Fixes + - Send events when report_after_job_retries is true and a job is configured with retry: 0 [#1557](https://github.com/getsentry/sentry-ruby/pull/1557) - Fixes [#1556](https://github.com/getsentry/sentry-ruby/issues/1556) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3619eb371..8d2a93368 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,11 @@ You can contribute to this project in the following ways: And if you have any questions, please feel free to reach out on [Discord]. +## Develop This Project With Multi-root Workspaces + +If you use editors that support [VS Code-style multi-root workspaces](https://code.visualstudio.com/docs/editor/multi-root-workspaces), +such as VS Code, Cursor...etc., opening the editor with `sentry-ruby.code-workspace` file will provide a better development experience. + ## Contribute To Individual Gems - Install the dependencies of a specific gem by running `bundle` in it's subdirectory. I.e: diff --git a/Gemfile b/Gemfile index c4344ea66..2dbaf0283 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" git_source(:github) { |name| "https://github.com/#{name}.git" } @@ -5,6 +7,8 @@ gem "rake", "~> 12.0" ruby_version = Gem::Version.new(RUBY_VERSION) +gem "jar-dependencies", "0.4.1" if RUBY_PLATFORM == "java" + # Development tools if ruby_version >= Gem::Version.new("2.7.0") gem "debug", github: "ruby/debug", platform: :ruby @@ -19,4 +23,7 @@ gem "simplecov" gem "simplecov-cobertura", "~> 1.4" gem "rexml" -gem "rubocop-rails-omakase" +group :rubocop do + gem "rubocop-rails-omakase" + gem "rubocop-packaging" +end diff --git a/codecov.yml b/codecov.yml index 3dd8085c8..03096bfb1 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,7 +1,7 @@ codecov: disable_default_path_fixes: true ignore: - - "**/spec" + - "**/spec/**" comment: layout: "header, diff, flags, components, tree" component_management: diff --git a/sentry-delayed_job/.rubocop.yml b/sentry-delayed_job/.rubocop.yml new file mode 120000 index 000000000..7cc18e076 --- /dev/null +++ b/sentry-delayed_job/.rubocop.yml @@ -0,0 +1 @@ +../.rubocop.yml \ No newline at end of file diff --git a/sentry-delayed_job/Gemfile b/sentry-delayed_job/Gemfile index c1b2a698b..ddf765ba0 100644 --- a/sentry-delayed_job/Gemfile +++ b/sentry-delayed_job/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" git_source(:github) { |name| "https://github.com/#{name}.git" } @@ -6,9 +8,6 @@ gemspec gem "sentry-ruby", path: "../sentry-ruby" gem "sentry-rails", path: "../sentry-rails" -# For https://github.com/ruby/psych/issues/655 -gem "psych", "5.1.0" - gem "delayed_job" gem "delayed_job_active_record" @@ -16,11 +15,20 @@ gem "rails", "> 5.0.0" platform :jruby do # See https://github.com/jruby/activerecord-jdbc-adapter/issues/1139 - gem "activerecord-jdbcmysql-adapter", github: "jruby/activerecord-jdbc-adapter" + gem "activerecord-jdbcmysql-adapter", github: "jruby/activerecord-jdbc-adapter", ref: "6b3983bbbfda75ee2a1f5bc4c8d35efd7b71d84b" gem "jdbc-sqlite3" end -# 1.7.0 dropped support for ruby < 3.0, remove later after upgrading craft setup -gem "sqlite3", "1.6.9", platform: :ruby +ruby_version = Gem::Version.new(RUBY_VERSION) + +if ruby_version < Gem::Version.new("2.5.0") + gem "sqlite3", "~> 1.3.0", platform: :ruby +elsif ruby_version < Gem::Version.new("3.0.0") + gem "sqlite3", "~> 1.6.0", platform: :ruby +elsif ruby_version >= Gem::Version.new("3.0.0") && ruby_version < Gem::Version.new("3.1.0") + gem "sqlite3", "~> 1.7.0", platform: :ruby +elsif ruby_version >= Gem::Version.new("3.1.0") + gem "sqlite3", "~> 2.2", platform: :ruby +end eval_gemfile File.expand_path("../Gemfile", __dir__) diff --git a/sentry-delayed_job/Rakefile b/sentry-delayed_job/Rakefile index 7b2756854..13afab191 100644 --- a/sentry-delayed_job/Rakefile +++ b/sentry-delayed_job/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler/gem_tasks" require "rspec/core/rake_task" diff --git a/sentry-delayed_job/bin/console b/sentry-delayed_job/bin/console index 660c7a889..f0f5a7b6a 100755 --- a/sentry-delayed_job/bin/console +++ b/sentry-delayed_job/bin/console @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require "bundler/setup" require "sentry/ruby" diff --git a/sentry-delayed_job/example/Gemfile b/sentry-delayed_job/example/Gemfile index 2ed843ab1..d138c16d5 100644 --- a/sentry-delayed_job/example/Gemfile +++ b/sentry-delayed_job/example/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" gem "rails" diff --git a/sentry-delayed_job/example/app.rb b/sentry-delayed_job/example/app.rb index bcec27c52..04f391468 100644 --- a/sentry-delayed_job/example/app.rb +++ b/sentry-delayed_job/example/app.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_job" require "active_record" require "delayed_job" diff --git a/sentry-delayed_job/lib/sentry-delayed_job.rb b/sentry-delayed_job/lib/sentry-delayed_job.rb index f96b03b6a..bc961259f 100644 --- a/sentry-delayed_job/lib/sentry-delayed_job.rb +++ b/sentry-delayed_job/lib/sentry-delayed_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "delayed_job" require "sentry-ruby" require "sentry/integrable" diff --git a/sentry-delayed_job/lib/sentry/delayed_job/configuration.rb b/sentry-delayed_job/lib/sentry/delayed_job/configuration.rb index 4408b37af..42ee8e63c 100644 --- a/sentry-delayed_job/lib/sentry/delayed_job/configuration.rb +++ b/sentry-delayed_job/lib/sentry/delayed_job/configuration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry class Configuration attr_reader :delayed_job diff --git a/sentry-delayed_job/lib/sentry/delayed_job/version.rb b/sentry-delayed_job/lib/sentry/delayed_job/version.rb index a0e159863..64ea7616b 100644 --- a/sentry-delayed_job/lib/sentry/delayed_job/version.rb +++ b/sentry-delayed_job/lib/sentry/delayed_job/version.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module Sentry module DelayedJob - VERSION = "5.18.0" + VERSION = "5.22.1" end end diff --git a/sentry-delayed_job/sentry-delayed_job.gemspec b/sentry-delayed_job/sentry-delayed_job.gemspec index 1034fa8bf..3f07ba88b 100644 --- a/sentry-delayed_job/sentry-delayed_job.gemspec +++ b/sentry-delayed_job/sentry-delayed_job.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "lib/sentry/delayed_job/version" Gem::Specification.new do |spec| @@ -7,21 +9,27 @@ Gem::Specification.new do |spec| spec.description = spec.summary = "A gem that provides DelayedJob integration for the Sentry error logger" spec.email = "accounts@sentry.io" spec.license = 'MIT' - spec.homepage = "https://github.com/getsentry/sentry-ruby" spec.platform = Gem::Platform::RUBY spec.required_ruby_version = '>= 2.4' spec.extra_rdoc_files = ["README.md", "LICENSE.txt"] - spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples)'`.split("\n") + spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples|\.rubocop\.yml)'`.split("\n") + + github_root_uri = 'https://github.com/getsentry/sentry-ruby' + spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}" - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = spec.homepage - spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" + spec.metadata = { + "homepage_uri" => spec.homepage, + "source_code_uri" => spec.homepage, + "changelog_uri" => "#{github_root_uri}/blob/#{spec.version}/CHANGELOG.md", + "bug_tracker_uri" => "#{github_root_uri}/issues", + "documentation_uri" => "http://www.rubydoc.info/gems/#{spec.name}/#{spec.version}" + } spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_dependency "sentry-ruby", "~> 5.18.0" + spec.add_dependency "sentry-ruby", "~> 5.22.1" spec.add_dependency "delayed_job", ">= 4.0" end diff --git a/sentry-delayed_job/spec/sentry/delayed_job/configuration_spec.rb b/sentry-delayed_job/spec/sentry/delayed_job/configuration_spec.rb index fdde5b4f5..a785f53c3 100644 --- a/sentry-delayed_job/spec/sentry/delayed_job/configuration_spec.rb +++ b/sentry-delayed_job/spec/sentry/delayed_job/configuration_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::DelayedJob::Configuration do diff --git a/sentry-delayed_job/spec/sentry/delayed_job_spec.rb b/sentry-delayed_job/spec/sentry/delayed_job_spec.rb index ac2d31fb1..24a1de77d 100644 --- a/sentry-delayed_job/spec/sentry/delayed_job_spec.rb +++ b/sentry-delayed_job/spec/sentry/delayed_job_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::DelayedJob do diff --git a/sentry-delayed_job/spec/spec_helper.rb b/sentry-delayed_job/spec/spec_helper.rb index 2d8bf6d10..3e07723bb 100644 --- a/sentry-delayed_job/spec/spec_helper.rb +++ b/sentry-delayed_job/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler/setup" begin require "debug/prelude" diff --git a/sentry-opentelemetry/.rubocop.yml b/sentry-opentelemetry/.rubocop.yml new file mode 120000 index 000000000..7cc18e076 --- /dev/null +++ b/sentry-opentelemetry/.rubocop.yml @@ -0,0 +1 @@ +../.rubocop.yml \ No newline at end of file diff --git a/sentry-opentelemetry/Gemfile b/sentry-opentelemetry/Gemfile index e084c17c6..b30d36d42 100644 --- a/sentry-opentelemetry/Gemfile +++ b/sentry-opentelemetry/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" git_source(:github) { |name| "https://github.com/#{name}.git" } diff --git a/sentry-opentelemetry/Rakefile b/sentry-opentelemetry/Rakefile index 7b2756854..13afab191 100644 --- a/sentry-opentelemetry/Rakefile +++ b/sentry-opentelemetry/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler/gem_tasks" require "rspec/core/rake_task" diff --git a/sentry-opentelemetry/lib/sentry/opentelemetry/propagator.rb b/sentry-opentelemetry/lib/sentry/opentelemetry/propagator.rb index 7bfb5424a..51045e209 100644 --- a/sentry-opentelemetry/lib/sentry/opentelemetry/propagator.rb +++ b/sentry-opentelemetry/lib/sentry/opentelemetry/propagator.rb @@ -5,8 +5,8 @@ module OpenTelemetry class Propagator FIELDS = [SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME].freeze - SENTRY_TRACE_KEY = ::OpenTelemetry::Context.create_key('sentry-trace') - SENTRY_BAGGAGE_KEY = ::OpenTelemetry::Context.create_key('sentry-baggage') + SENTRY_TRACE_KEY = ::OpenTelemetry::Context.create_key("sentry-trace") + SENTRY_BAGGAGE_KEY = ::OpenTelemetry::Context.create_key("sentry-baggage") def inject( carrier, @@ -41,8 +41,8 @@ def extract( trace_id, span_id, _parent_sampled = sentry_trace_data span_context = ::OpenTelemetry::Trace::SpanContext.new( - trace_id: [trace_id].pack('H*'), - span_id: [span_id].pack('H*'), + trace_id: [trace_id].pack("H*"), + span_id: [span_id].pack("H*"), # we simulate a sampled trace on the otel side and leave the sampling to sentry trace_flags: ::OpenTelemetry::Trace::TraceFlags::SAMPLED, remote: true diff --git a/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb b/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb index db6d500f0..e898c6b33 100644 --- a/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb +++ b/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'singleton' +require "singleton" module Sentry module OpenTelemetry @@ -83,7 +83,7 @@ def from_sentry_sdk?(otel_span) dsn = Sentry.configuration.dsn return false unless dsn - if otel_span.name.start_with?("HTTP") + if otel_span.name.start_with?("HTTP") || otel_span.name == "connect" # only check client requests, connects are sometimes internal return false unless INTERNAL_SPAN_KINDS.include?(otel_span.kind) @@ -151,14 +151,14 @@ def update_span_status(sentry_span, otel_span) if (http_status_code = otel_span.attributes[SEMANTIC_CONVENTIONS::HTTP_STATUS_CODE]) sentry_span.set_http_status(http_status_code) elsif (status_code = otel_span.status.code) - status = [0, 1].include?(status_code) ? 'ok' : 'unknown_error' + status = [0, 1].include?(status_code) ? "ok" : "internal_error" sentry_span.set_status(status) end end def update_span_with_otel_data(sentry_span, otel_span) update_span_status(sentry_span, otel_span) - sentry_span.set_data('otel.kind', otel_span.kind) + sentry_span.set_data("otel.kind", otel_span.kind) otel_span.attributes&.each { |k, v| sentry_span.set_data(k, v) } op, description = parse_span_description(otel_span) diff --git a/sentry-opentelemetry/lib/sentry/opentelemetry/version.rb b/sentry-opentelemetry/lib/sentry/opentelemetry/version.rb index e8dd18200..994c42977 100644 --- a/sentry-opentelemetry/lib/sentry/opentelemetry/version.rb +++ b/sentry-opentelemetry/lib/sentry/opentelemetry/version.rb @@ -2,6 +2,6 @@ module Sentry module OpenTelemetry - VERSION = "5.18.0" + VERSION = "5.22.1" end end diff --git a/sentry-opentelemetry/sentry-opentelemetry.gemspec b/sentry-opentelemetry/sentry-opentelemetry.gemspec index 1ab98a6b0..2b1385e54 100644 --- a/sentry-opentelemetry/sentry-opentelemetry.gemspec +++ b/sentry-opentelemetry/sentry-opentelemetry.gemspec @@ -9,21 +9,27 @@ Gem::Specification.new do |spec| spec.description = spec.summary = "A gem that provides OpenTelemetry integration for the Sentry error logger" spec.email = "accounts@sentry.io" spec.license = 'MIT' - spec.homepage = "https://github.com/getsentry/sentry-ruby" spec.platform = Gem::Platform::RUBY spec.required_ruby_version = '>= 2.4' spec.extra_rdoc_files = ["README.md", "LICENSE.txt"] - spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples)'`.split("\n") + spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples|\.rubocop\.yml)'`.split("\n") - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = spec.homepage - spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" + github_root_uri = 'https://github.com/getsentry/sentry-ruby' + spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}" + + spec.metadata = { + "homepage_uri" => spec.homepage, + "source_code_uri" => spec.homepage, + "changelog_uri" => "#{github_root_uri}/blob/#{spec.version}/CHANGELOG.md", + "bug_tracker_uri" => "#{github_root_uri}/issues", + "documentation_uri" => "http://www.rubydoc.info/gems/#{spec.name}/#{spec.version}" + } spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_dependency "sentry-ruby", "~> 5.18.0" + spec.add_dependency "sentry-ruby", "~> 5.22.1" spec.add_dependency "opentelemetry-sdk", "~> 1.0" end diff --git a/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb b/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb index 48d27596e..a769e6783 100644 --- a/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb +++ b/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb @@ -8,6 +8,27 @@ let(:tracer) { ::OpenTelemetry.tracer_provider.tracer('sentry', '1.0') } let(:empty_context) { ::OpenTelemetry::Context.empty } let(:invalid_span) { ::OpenTelemetry::SDK::Trace::Span::INVALID } + let(:error_span) do + attributes = { + 'http.method' => 'GET', + 'http.host' => 'sentry.io', + 'http.scheme' => 'https' + } + + tracer.start_root_span('HTTP GET', attributes: attributes, kind: :server).tap do |span| + span.status = OpenTelemetry::Trace::Status.error("not a success") + end + end + let(:http_error_span) do + attributes = { + 'http.method' => 'GET', + 'http.host' => 'sentry.io', + 'http.scheme' => 'https', + 'http.status_code' => '409' + } + + tracer.start_root_span('HTTP GET', attributes: attributes, kind: :server) + end let(:root_span) do attributes = { @@ -64,6 +85,18 @@ tracer.start_span('HTTP POST', with_parent: root_parent_context, attributes: attributes, kind: :client) end + let(:child_internal_span_connect) do + attributes = { + 'http.method' => 'POST', + 'http.scheme' => 'https', + 'http.target' => '/api/5434472/envelope/', + 'net.peer.name' => 'sentry.localdomain', + 'net.peer.port' => 443 + } + + tracer.start_span('connect', with_parent: root_parent_context, attributes: attributes, kind: :internal) + end + before do perform_basic_setup perform_otel_setup @@ -153,6 +186,11 @@ subject.on_start(child_internal_span, root_parent_context) end + it 'noops on `connect` requests' do + expect(transaction).not_to receive(:start_child) + subject.on_start(child_internal_span_connect, root_parent_context) + end + it 'starts a sentry child span on otel child span' do expect(transaction).to receive(:start_child).and_call_original subject.on_start(child_db_span, root_parent_context) @@ -180,12 +218,16 @@ subject.on_start(root_span, empty_context) subject.on_start(child_db_span, root_parent_context) subject.on_start(child_http_span, root_parent_context) + subject.on_start(error_span, empty_context) + subject.on_start(http_error_span, empty_context) end let(:finished_db_span) { child_db_span.finish } let(:finished_http_span) { child_http_span.finish } let(:finished_root_span) { root_span.finish } let(:finished_invalid_span) { invalid_span.finish } + let(:finished_error_span) { error_span.finish } + let(:finished_http_error_span) { http_error_span.finish } it 'noops when not initialized' do expect(Sentry).to receive(:initialized?).and_return(false) @@ -207,6 +249,30 @@ subject.on_finish(finished_invalid_span) end + it 'updates span status on error' do + expect(subject.span_map).to receive(:delete).and_call_original + + span_id = finished_error_span.context.hex_span_id + sentry_span = subject.span_map[span_id] + + expect(sentry_span).to receive(:finish).and_call_original + subject.on_finish(finished_error_span) + + expect(sentry_span.status).to eq('internal_error') + end + + it 'updates span status on HTTP error' do + expect(subject.span_map).to receive(:delete).and_call_original + + span_id = finished_http_error_span.context.hex_span_id + sentry_span = subject.span_map[span_id] + + expect(sentry_span).to receive(:finish).and_call_original + subject.on_finish(finished_http_error_span) + + expect(sentry_span.status).to eq('already_exists') + end + it 'finishes sentry child span on otel child db span finish' do expect(subject.span_map).to receive(:delete).and_call_original @@ -224,7 +290,7 @@ expect(sentry_span.data).to include({ 'otel.kind' => finished_db_span.kind }) expect(sentry_span.timestamp).to eq(finished_db_span.end_timestamp / 1e9) - expect(subject.span_map.size).to eq(2) + expect(subject.span_map.size).to eq(4) expect(subject.span_map.keys).not_to include(span_id) end @@ -246,13 +312,15 @@ expect(sentry_span.timestamp).to eq(finished_http_span.end_timestamp / 1e9) expect(sentry_span.status).to eq('ok') - expect(subject.span_map.size).to eq(2) + expect(subject.span_map.size).to eq(4) expect(subject.span_map.keys).not_to include(span_id) end it 'finishes sentry transaction on otel root span finish' do subject.on_finish(finished_db_span) subject.on_finish(finished_http_span) + subject.on_finish(finished_error_span) + subject.on_finish(finished_http_error_span) expect(subject.span_map).to receive(:delete).and_call_original diff --git a/sentry-rails/.rubocop.yml b/sentry-rails/.rubocop.yml new file mode 120000 index 000000000..7cc18e076 --- /dev/null +++ b/sentry-rails/.rubocop.yml @@ -0,0 +1 @@ +../.rubocop.yml \ No newline at end of file diff --git a/sentry-rails/Gemfile b/sentry-rails/Gemfile index fd2ce76cb..53cfb92d7 100644 --- a/sentry-rails/Gemfile +++ b/sentry-rails/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" git_source(:github) { |name| "https://github.com/#{name}.git" } @@ -6,39 +8,42 @@ gemspec gem "sentry-ruby", path: "../sentry-ruby" platform :jruby do - gem 'activerecord-jdbcmysql-adapter' + gem "activerecord-jdbcmysql-adapter" gem "jdbc-sqlite3" end +ruby_version = Gem::Version.new(RUBY_VERSION) + rails_version = ENV["RAILS_VERSION"] -rails_version = "7.1.0" if rails_version.nil? +rails_version = "8.0.0" if rails_version.nil? rails_version = Gem::Version.new(rails_version) -if rails_version < Gem::Version.new("6.0.0") - gem "sqlite3", "~> 1.3.0", platform: :ruby -else - # 1.7.0 dropped support for ruby < 3.0, remove later after upgrading craft setup - gem "sqlite3", "1.6.9", platform: :ruby -end +gem "rails", "~> #{rails_version}" -if rails_version >= Gem::Version.new("7.2.0.alpha") - gem "rails", github: "rails/rails" +if rails_version >= Gem::Version.new("8.0.0") + gem "rspec-rails" + gem "sqlite3", "~> 2.1.1", platform: :ruby elsif rails_version >= Gem::Version.new("7.1.0") - gem "rails", "~> #{rails_version}" + gem "rspec-rails" + gem "sqlite3", "~> 1.7.3", platform: :ruby +elsif rails_version >= Gem::Version.new("6.1.0") + gem "rspec-rails", "~> 4.0" + + if ruby_version >= Gem::Version.new("2.7.0") + gem "sqlite3", "~> 1.7.3", platform: :ruby + else + gem "sqlite3", "~> 1.6.9", platform: :ruby + end else - gem "rails", "~> #{rails_version}" + gem "rspec-rails", "~> 4.0" gem "psych", "~> 3.0.0" -end - -gem "mini_magick" - -gem "sprockets-rails" - -gem "sidekiq" - -gem "rspec-rails", "~> 4.0" -ruby_version = Gem::Version.new(RUBY_VERSION) + if rails_version >= Gem::Version.new("6.0.0") + gem "sqlite3", "~> 1.4.0", platform: :ruby + else + gem "sqlite3", "~> 1.3.0", platform: :ruby + end +end if ruby_version < Gem::Version.new("2.5.0") # https://github.com/flavorjones/loofah/pull/267 @@ -46,6 +51,10 @@ if ruby_version < Gem::Version.new("2.5.0") gem "loofah", "2.20.0" end +gem "mini_magick" + +gem "sprockets-rails" + gem "benchmark-ips" gem "benchmark_driver" gem "benchmark-ipsa" diff --git a/sentry-rails/Rakefile b/sentry-rails/Rakefile index 2624fb72e..d58f59ad2 100644 --- a/sentry-rails/Rakefile +++ b/sentry-rails/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler/gem_tasks" require "rspec/core/rake_task" diff --git a/sentry-rails/app/jobs/sentry/send_event_job.rb b/sentry-rails/app/jobs/sentry/send_event_job.rb index 75ac80529..06ca59172 100644 --- a/sentry-rails/app/jobs/sentry/send_event_job.rb +++ b/sentry-rails/app/jobs/sentry/send_event_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + if defined?(ActiveJob) module Sentry parent_job = diff --git a/sentry-rails/benchmarks/allocation_comparison.rb b/sentry-rails/benchmarks/allocation_comparison.rb index f2499d937..15b1e3c4a 100644 --- a/sentry-rails/benchmarks/allocation_comparison.rb +++ b/sentry-rails/benchmarks/allocation_comparison.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'benchmark/memory' require "sentry-ruby" require "sentry/benchmarks/benchmark_transport" diff --git a/sentry-rails/benchmarks/allocation_report.rb b/sentry-rails/benchmarks/allocation_report.rb index 13fee1513..9bc691a26 100644 --- a/sentry-rails/benchmarks/allocation_report.rb +++ b/sentry-rails/benchmarks/allocation_report.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'benchmark/ipsa' require "sentry-ruby" require "sentry/benchmarks/benchmark_transport" diff --git a/sentry-rails/benchmarks/application.rb b/sentry-rails/benchmarks/application.rb index fa4d3f5e8..0ef435cee 100644 --- a/sentry-rails/benchmarks/application.rb +++ b/sentry-rails/benchmarks/application.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/all" require "action_controller" require_relative "../spec/support/test_rails_app/app" diff --git a/sentry-rails/bin/console b/sentry-rails/bin/console index 660c7a889..f0f5a7b6a 100755 --- a/sentry-rails/bin/console +++ b/sentry-rails/bin/console @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require "bundler/setup" require "sentry/ruby" diff --git a/sentry-rails/lib/generators/sentry_generator.rb b/sentry-rails/lib/generators/sentry_generator.rb index 76ad3ecf9..e293dae95 100644 --- a/sentry-rails/lib/generators/sentry_generator.rb +++ b/sentry-rails/lib/generators/sentry_generator.rb @@ -1,8 +1,12 @@ +# frozen_string_literal: true + require "rails/generators/base" class SentryGenerator < ::Rails::Generators::Base class_option :dsn, type: :string, desc: "Sentry DSN" + class_option :inject_meta, type: :boolean, default: true, desc: "Inject meta tag into layout" + def copy_initializer_file dsn = options[:dsn] ? "'#{options[:dsn]}'" : "ENV['SENTRY_DSN']" @@ -16,4 +20,12 @@ def copy_initializer_file end RUBY end + + def inject_code_into_layout + return unless options[:inject_meta] + + inject_into_file "app/views/layouts/application.html.erb", before: "\n" do + " <%= Sentry.get_trace_propagation_meta.html_safe %>\n " + end + end end diff --git a/sentry-rails/lib/sentry-rails.rb b/sentry-rails/lib/sentry-rails.rb index c436e1dfa..a8d6499b1 100644 --- a/sentry-rails/lib/sentry-rails.rb +++ b/sentry-rails/lib/sentry-rails.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + require "sentry/rails/version" require "sentry/rails" diff --git a/sentry-rails/lib/sentry/rails.rb b/sentry-rails/lib/sentry/rails.rb index 14c8e1b80..b4b79dd85 100644 --- a/sentry-rails/lib/sentry/rails.rb +++ b/sentry-rails/lib/sentry/rails.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rails" require "sentry-ruby" require "sentry/integrable" diff --git a/sentry-rails/lib/sentry/rails/action_cable.rb b/sentry-rails/lib/sentry/rails/action_cable.rb index 06833a68b..222aab1b8 100644 --- a/sentry-rails/lib/sentry/rails/action_cable.rb +++ b/sentry-rails/lib/sentry/rails/action_cable.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module Sentry module Rails module ActionCableExtensions class ErrorHandler - OP_NAME = "websocket.server".freeze + OP_NAME = "websocket.server" SPAN_ORIGIN = "auto.http.rails.actioncable" class << self diff --git a/sentry-rails/lib/sentry/rails/active_job.rb b/sentry-rails/lib/sentry/rails/active_job.rb index afdf87cd7..953af632b 100644 --- a/sentry-rails/lib/sentry/rails/active_job.rb +++ b/sentry-rails/lib/sentry/rails/active_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry module Rails module ActiveJobExtensions @@ -16,8 +18,8 @@ def already_supported_by_sentry_integration? end class SentryReporter - OP_NAME = "queue.active_job".freeze - SPAN_ORIGIN = "auto.queue.active_job".freeze + OP_NAME = "queue.active_job" + SPAN_ORIGIN = "auto.queue.active_job" class << self def record(job, &block) diff --git a/sentry-rails/lib/sentry/rails/background_worker.rb b/sentry-rails/lib/sentry/rails/background_worker.rb index 56dcd6609..2c0cc7a71 100644 --- a/sentry-rails/lib/sentry/rails/background_worker.rb +++ b/sentry-rails/lib/sentry/rails/background_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry class BackgroundWorker def _perform(&block) diff --git a/sentry-rails/lib/sentry/rails/backtrace_cleaner.rb b/sentry-rails/lib/sentry/rails/backtrace_cleaner.rb index 25812b97a..ce32173a0 100644 --- a/sentry-rails/lib/sentry/rails/backtrace_cleaner.rb +++ b/sentry-rails/lib/sentry/rails/backtrace_cleaner.rb @@ -1,21 +1,21 @@ +# frozen_string_literal: true + require "active_support/backtrace_cleaner" require "active_support/core_ext/string/access" module Sentry module Rails class BacktraceCleaner < ActiveSupport::BacktraceCleaner - APP_DIRS_PATTERN = /\A(?:\.\/)?(?:app|config|lib|test|\(\w*\))/.freeze - RENDER_TEMPLATE_PATTERN = /:in `.*_\w+_{2,3}\d+_\d+'/.freeze + APP_DIRS_PATTERN = /\A(?:\.\/)?(?:app|config|lib|test|\(\w*\))/ + RENDER_TEMPLATE_PATTERN = /:in (?:`|').*_\w+_{2,3}\d+_\d+'/ def initialize super - # we don't want any default silencers because they're too aggressive + # We don't want any default silencers because they're too aggressive remove_silencers! + # We don't want any default filters because Rails 7.2 starts shortening the paths. See #2472 + remove_filters! - @root = "#{Sentry.configuration.project_root}/" - add_filter do |line| - line.start_with?(@root) ? line.from(@root.size) : line - end add_filter do |line| if line =~ RENDER_TEMPLATE_PATTERN line.sub(RENDER_TEMPLATE_PATTERN, "") diff --git a/sentry-rails/lib/sentry/rails/breadcrumb/active_support_logger.rb b/sentry-rails/lib/sentry/rails/breadcrumb/active_support_logger.rb index 9dfdd1d26..a301cb960 100644 --- a/sentry-rails/lib/sentry/rails/breadcrumb/active_support_logger.rb +++ b/sentry-rails/lib/sentry/rails/breadcrumb/active_support_logger.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry module Rails module Breadcrumb diff --git a/sentry-rails/lib/sentry/rails/breadcrumb/monotonic_active_support_logger.rb b/sentry-rails/lib/sentry/rails/breadcrumb/monotonic_active_support_logger.rb index 71785d256..541f9bea3 100644 --- a/sentry-rails/lib/sentry/rails/breadcrumb/monotonic_active_support_logger.rb +++ b/sentry-rails/lib/sentry/rails/breadcrumb/monotonic_active_support_logger.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "sentry/rails/instrument_payload_cleanup_helper" module Sentry diff --git a/sentry-rails/lib/sentry/rails/capture_exceptions.rb b/sentry-rails/lib/sentry/rails/capture_exceptions.rb index d52456081..f571bd689 100644 --- a/sentry-rails/lib/sentry/rails/capture_exceptions.rb +++ b/sentry-rails/lib/sentry/rails/capture_exceptions.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module Sentry module Rails class CaptureExceptions < Sentry::Rack::CaptureExceptions RAILS_7_1 = Gem::Version.new(::Rails.version) >= Gem::Version.new("7.1.0.alpha") - SPAN_ORIGIN = 'auto.http.rails'.freeze + SPAN_ORIGIN = "auto.http.rails" def initialize(_) super @@ -20,7 +22,7 @@ def collect_exception(env) end def transaction_op - "http.server".freeze + "http.server" end def capture_exception(exception, env) diff --git a/sentry-rails/lib/sentry/rails/configuration.rb b/sentry-rails/lib/sentry/rails/configuration.rb index 883700142..a28a07f69 100644 --- a/sentry-rails/lib/sentry/rails/configuration.rb +++ b/sentry-rails/lib/sentry/rails/configuration.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + require "sentry/rails/tracing/action_controller_subscriber" require "sentry/rails/tracing/action_view_subscriber" require "sentry/rails/tracing/active_record_subscriber" require "sentry/rails/tracing/active_storage_subscriber" +require "sentry/rails/tracing/active_support_subscriber" module Sentry class Configuration @@ -31,20 +34,20 @@ class Configuration module Rails IGNORE_DEFAULT = [ - 'AbstractController::ActionNotFound', - 'ActionController::BadRequest', - 'ActionController::InvalidAuthenticityToken', - 'ActionController::InvalidCrossOriginRequest', - 'ActionController::MethodNotAllowed', - 'ActionController::NotImplemented', - 'ActionController::ParameterMissing', - 'ActionController::RoutingError', - 'ActionController::UnknownAction', - 'ActionController::UnknownFormat', - 'ActionDispatch::Http::MimeNegotiation::InvalidType', - 'ActionController::UnknownHttpMethod', - 'ActionDispatch::Http::Parameters::ParseError', - 'ActiveRecord::RecordNotFound' + "AbstractController::ActionNotFound", + "ActionController::BadRequest", + "ActionController::InvalidAuthenticityToken", + "ActionController::InvalidCrossOriginRequest", + "ActionController::MethodNotAllowed", + "ActionController::NotImplemented", + "ActionController::ParameterMissing", + "ActionController::RoutingError", + "ActionController::UnknownAction", + "ActionController::UnknownFormat", + "ActionDispatch::Http::MimeNegotiation::InvalidType", + "ActionController::UnknownHttpMethod", + "ActionDispatch::Http::Parameters::ParseError", + "ActiveRecord::RecordNotFound" ].freeze ACTIVE_SUPPORT_LOGGER_SUBSCRIPTION_ITEMS_DEFAULT = { @@ -162,6 +165,7 @@ def initialize end @tracing_subscribers = Set.new([ Sentry::Rails::Tracing::ActionViewSubscriber, + Sentry::Rails::Tracing::ActiveSupportSubscriber, Sentry::Rails::Tracing::ActiveRecordSubscriber, Sentry::Rails::Tracing::ActiveStorageSubscriber ]) diff --git a/sentry-rails/lib/sentry/rails/controller_methods.rb b/sentry-rails/lib/sentry/rails/controller_methods.rb index 90330e946..aa8b4f3e4 100644 --- a/sentry-rails/lib/sentry/rails/controller_methods.rb +++ b/sentry-rails/lib/sentry/rails/controller_methods.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry module Rails module ControllerMethods diff --git a/sentry-rails/lib/sentry/rails/controller_transaction.rb b/sentry-rails/lib/sentry/rails/controller_transaction.rb index b85506a3d..a753f06d7 100644 --- a/sentry-rails/lib/sentry/rails/controller_transaction.rb +++ b/sentry-rails/lib/sentry/rails/controller_transaction.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module Sentry module Rails module ControllerTransaction - SPAN_ORIGIN = 'auto.view.rails'.freeze + SPAN_ORIGIN = "auto.view.rails" def self.included(base) base.prepend_around_action(:sentry_around_action) @@ -21,8 +23,10 @@ def sentry_around_action child_span.set_http_status(response.status) child_span.set_data(:format, request.format) child_span.set_data(:method, request.method) - child_span.set_data(:path, request.path) - child_span.set_data(:params, request.params) + + pii = Sentry.configuration.send_default_pii + child_span.set_data(:path, pii ? request.fullpath : request.filtered_path) + child_span.set_data(:params, pii ? request.params : request.filtered_parameters) end result diff --git a/sentry-rails/lib/sentry/rails/engine.rb b/sentry-rails/lib/sentry/rails/engine.rb index 12ac173a7..dc50819cd 100644 --- a/sentry-rails/lib/sentry/rails/engine.rb +++ b/sentry-rails/lib/sentry/rails/engine.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry class Engine < ::Rails::Engine isolate_namespace Sentry diff --git a/sentry-rails/lib/sentry/rails/error_subscriber.rb b/sentry-rails/lib/sentry/rails/error_subscriber.rb index fac675dbf..161b8793b 100644 --- a/sentry-rails/lib/sentry/rails/error_subscriber.rb +++ b/sentry-rails/lib/sentry/rails/error_subscriber.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry module Rails # This is not a user-facing class. You should use it with Rails 7.0's error reporter feature and its interfaces. @@ -25,7 +27,20 @@ def report(error, handled:, severity:, context:, source: nil) hint.merge!(context.delete(:hint)) end - Sentry::Rails.capture_exception(error, level: severity, contexts: { "rails.error" => context }, tags: tags, hint: hint) + options = { level: severity, contexts: { "rails.error" => context }, tags: tags, hint: hint } + + case error + when String + Sentry::Rails.capture_message(error, **options) + when Exception + Sentry::Rails.capture_exception(error, **options) + else + log_debug("Expected an Exception or a String, got: #{error.inspect}") + end + end + + def log_debug(message) + Sentry.configuration.logger.debug(message) end end end diff --git a/sentry-rails/lib/sentry/rails/instrument_payload_cleanup_helper.rb b/sentry-rails/lib/sentry/rails/instrument_payload_cleanup_helper.rb index 343d2d41c..07422c5e8 100644 --- a/sentry-rails/lib/sentry/rails/instrument_payload_cleanup_helper.rb +++ b/sentry-rails/lib/sentry/rails/instrument_payload_cleanup_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry module Rails module InstrumentPayloadCleanupHelper diff --git a/sentry-rails/lib/sentry/rails/overrides/streaming_reporter.rb b/sentry-rails/lib/sentry/rails/overrides/streaming_reporter.rb index 5ce0c637b..1fdd4ad60 100644 --- a/sentry-rails/lib/sentry/rails/overrides/streaming_reporter.rb +++ b/sentry-rails/lib/sentry/rails/overrides/streaming_reporter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry module Rails module Overrides diff --git a/sentry-rails/lib/sentry/rails/railtie.rb b/sentry-rails/lib/sentry/rails/railtie.rb index ef09851c5..0a0cdf213 100644 --- a/sentry-rails/lib/sentry/rails/railtie.rb +++ b/sentry-rails/lib/sentry/rails/railtie.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "sentry/rails/capture_exceptions" require "sentry/rails/rescued_exception_interceptor" require "sentry/rails/backtrace_cleaner" @@ -12,7 +14,7 @@ class Railtie < ::Rails::Railtie app.config.middleware.insert_after ActionDispatch::DebugExceptions, Sentry::Rails::RescuedExceptionInterceptor end - # because the extension works by registering the around_perform callcack, it should always be ran + # because the extension works by registering the around_perform callback, it should always be run # before the application is eager-loaded (before user's jobs register their own callbacks) # See https://github.com/getsentry/sentry-ruby/issues/1249#issuecomment-853871871 for the detail explanation initializer "sentry.extend_active_job", before: :eager_load! do |app| @@ -94,14 +96,14 @@ def patch_background_worker def inject_breadcrumbs_logger if Sentry.configuration.breadcrumbs_logger.include?(:active_support_logger) - require 'sentry/rails/breadcrumb/active_support_logger' + require "sentry/rails/breadcrumb/active_support_logger" Sentry::Rails::Breadcrumb::ActiveSupportLogger.inject(Sentry.configuration.rails.active_support_logger_subscription_items) end if Sentry.configuration.breadcrumbs_logger.include?(:monotonic_active_support_logger) return warn "Usage of `monotonic_active_support_logger` require a version of Rails >= 6.1, please upgrade your Rails version or use another logger" if ::Rails.version.to_f < 6.1 - require 'sentry/rails/breadcrumb/monotonic_active_support_logger' + require "sentry/rails/breadcrumb/monotonic_active_support_logger" Sentry::Rails::Breadcrumb::MonotonicActiveSupportLogger.inject end end diff --git a/sentry-rails/lib/sentry/rails/rescued_exception_interceptor.rb b/sentry-rails/lib/sentry/rails/rescued_exception_interceptor.rb index 2c977dba4..757377298 100644 --- a/sentry-rails/lib/sentry/rails/rescued_exception_interceptor.rb +++ b/sentry-rails/lib/sentry/rails/rescued_exception_interceptor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry module Rails class RescuedExceptionInterceptor @@ -17,7 +19,16 @@ def call(env) end def report_rescued_exceptions? - Sentry.configuration.rails.report_rescued_exceptions + # In rare edge cases, `Sentry.configuration` might be `nil` here. + # Hence, we use a safe navigation and fallback to a reasonable default + # of `true` in case the configuration couldn't be loaded. + # See https://github.com/getsentry/sentry-ruby/issues/2386 + report_rescued_exceptions = Sentry.configuration&.rails&.report_rescued_exceptions + return report_rescued_exceptions unless report_rescued_exceptions.nil? + + # `true` is the default for `report_rescued_exceptions`, as specified in + # `sentry-rails/lib/sentry/rails/configuration.rb`. + true end end end diff --git a/sentry-rails/lib/sentry/rails/tracing.rb b/sentry-rails/lib/sentry/rails/tracing.rb index 3c5ae2222..82509ab8b 100644 --- a/sentry-rails/lib/sentry/rails/tracing.rb +++ b/sentry-rails/lib/sentry/rails/tracing.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry module Rails module Tracing diff --git a/sentry-rails/lib/sentry/rails/tracing/abstract_subscriber.rb b/sentry-rails/lib/sentry/rails/tracing/abstract_subscriber.rb index fec3f7bde..7ba338c6b 100644 --- a/sentry-rails/lib/sentry/rails/tracing/abstract_subscriber.rb +++ b/sentry-rails/lib/sentry/rails/tracing/abstract_subscriber.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry module Rails module Tracing diff --git a/sentry-rails/lib/sentry/rails/tracing/action_controller_subscriber.rb b/sentry-rails/lib/sentry/rails/tracing/action_controller_subscriber.rb index 11e9eadf2..42b89ce32 100644 --- a/sentry-rails/lib/sentry/rails/tracing/action_controller_subscriber.rb +++ b/sentry-rails/lib/sentry/rails/tracing/action_controller_subscriber.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "sentry/rails/tracing/abstract_subscriber" require "sentry/rails/instrument_payload_cleanup_helper" @@ -8,8 +10,8 @@ class ActionControllerSubscriber < AbstractSubscriber extend InstrumentPayloadCleanupHelper EVENT_NAMES = ["process_action.action_controller"].freeze - OP_NAME = "view.process_action.action_controller".freeze - SPAN_ORIGIN = "auto.view.rails".freeze + OP_NAME = "view.process_action.action_controller" + SPAN_ORIGIN = "auto.view.rails" def self.subscribe! Sentry.logger.warn <<~MSG diff --git a/sentry-rails/lib/sentry/rails/tracing/action_view_subscriber.rb b/sentry-rails/lib/sentry/rails/tracing/action_view_subscriber.rb index baed2c7e5..3f56d72ac 100644 --- a/sentry-rails/lib/sentry/rails/tracing/action_view_subscriber.rb +++ b/sentry-rails/lib/sentry/rails/tracing/action_view_subscriber.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "sentry/rails/tracing/abstract_subscriber" module Sentry @@ -5,8 +7,8 @@ module Rails module Tracing class ActionViewSubscriber < AbstractSubscriber EVENT_NAMES = ["render_template.action_view"].freeze - SPAN_PREFIX = "template.".freeze - SPAN_ORIGIN = "auto.template.rails".freeze + SPAN_PREFIX = "template." + SPAN_ORIGIN = "auto.template.rails" def self.subscribe! subscribe_to_event(EVENT_NAMES) do |event_name, duration, payload| diff --git a/sentry-rails/lib/sentry/rails/tracing/active_record_subscriber.rb b/sentry-rails/lib/sentry/rails/tracing/active_record_subscriber.rb index 813aa9589..09468b6cc 100644 --- a/sentry-rails/lib/sentry/rails/tracing/active_record_subscriber.rb +++ b/sentry-rails/lib/sentry/rails/tracing/active_record_subscriber.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "sentry/rails/tracing/abstract_subscriber" module Sentry @@ -5,19 +7,15 @@ module Rails module Tracing class ActiveRecordSubscriber < AbstractSubscriber EVENT_NAMES = ["sql.active_record"].freeze - SPAN_PREFIX = "db.".freeze - SPAN_ORIGIN = "auto.db.rails".freeze + SPAN_PREFIX = "db." + SPAN_ORIGIN = "auto.db.rails" EXCLUDED_EVENTS = ["SCHEMA", "TRANSACTION"].freeze SUPPORT_SOURCE_LOCATION = ActiveSupport::BacktraceCleaner.method_defined?(:clean_frame) if SUPPORT_SOURCE_LOCATION - # Need to be specific down to the lib path so queries generated in specs don't get ignored - SENTRY_RUBY_PATH = File.join(Gem::Specification.find_by_name("sentry-ruby").full_gem_path, "lib") - SENTRY_RAILS_PATH = File.join(Gem::Specification.find_by_name("sentry-rails").full_gem_path, "lib") - class_attribute :backtrace_cleaner, default: (ActiveSupport::BacktraceCleaner.new.tap do |cleaner| - cleaner.add_silencer { |line| line.include?(SENTRY_RUBY_PATH) || line.include?(SENTRY_RAILS_PATH) } + cleaner.add_silencer { |line| line.include?("sentry-ruby/lib") || line.include?("sentry-rails/lib") } end) end @@ -73,6 +71,7 @@ def subscribe! if source_location backtrace_line = Sentry::Backtrace::Line.parse(source_location) + span.set_data(Span::DataConventions::FILEPATH, backtrace_line.file) if backtrace_line.file span.set_data(Span::DataConventions::LINENO, backtrace_line.number) if backtrace_line.number span.set_data(Span::DataConventions::FUNCTION, backtrace_line.method) if backtrace_line.method diff --git a/sentry-rails/lib/sentry/rails/tracing/active_storage_subscriber.rb b/sentry-rails/lib/sentry/rails/tracing/active_storage_subscriber.rb index 5d62bad22..50ee0d703 100644 --- a/sentry-rails/lib/sentry/rails/tracing/active_storage_subscriber.rb +++ b/sentry-rails/lib/sentry/rails/tracing/active_storage_subscriber.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "sentry/rails/tracing/abstract_subscriber" module Sentry @@ -19,12 +21,12 @@ class ActiveStorageSubscriber < AbstractSubscriber analyze.active_storage ].freeze - SPAN_ORIGIN = "auto.file.rails".freeze + SPAN_ORIGIN = "auto.file.rails" def self.subscribe! subscribe_to_event(EVENT_NAMES) do |event_name, duration, payload| record_on_current_span( - op: "file.#{event_name}".freeze, + op: "file.#{event_name}", origin: SPAN_ORIGIN, start_timestamp: payload[START_TIMESTAMP_NAME], description: payload[:service], diff --git a/sentry-rails/lib/sentry/rails/tracing/active_support_subscriber.rb b/sentry-rails/lib/sentry/rails/tracing/active_support_subscriber.rb new file mode 100644 index 000000000..e19c22245 --- /dev/null +++ b/sentry-rails/lib/sentry/rails/tracing/active_support_subscriber.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require "sentry/rails/tracing/abstract_subscriber" + +module Sentry + module Rails + module Tracing + class ActiveSupportSubscriber < AbstractSubscriber + READ_EVENT_NAMES = %w[ + cache_read.active_support + ].freeze + + WRITE_EVENT_NAMES = %w[ + cache_write.active_support + cache_increment.active_support + cache_decrement.active_support + ].freeze + + REMOVE_EVENT_NAMES = %w[ + cache_delete.active_support + ].freeze + + FLUSH_EVENT_NAMES = %w[ + cache_prune.active_support + ].freeze + + EVENT_NAMES = READ_EVENT_NAMES + WRITE_EVENT_NAMES + REMOVE_EVENT_NAMES + FLUSH_EVENT_NAMES + + SPAN_ORIGIN = "auto.cache.rails" + + def self.subscribe! + subscribe_to_event(EVENT_NAMES) do |event_name, duration, payload| + record_on_current_span( + op: operation_name(event_name), + origin: SPAN_ORIGIN, + start_timestamp: payload[START_TIMESTAMP_NAME], + description: payload[:store], + duration: duration + ) do |span| + span.set_data("cache.key", [*payload[:key]].select { |key| Utils::EncodingHelper.valid_utf_8?(key) }) + span.set_data("cache.hit", payload[:hit] == true) # Handle nil case + end + end + end + + def self.operation_name(event_name) + case + when READ_EVENT_NAMES.include?(event_name) + "cache.get" + when WRITE_EVENT_NAMES.include?(event_name) + "cache.put" + when REMOVE_EVENT_NAMES.include?(event_name) + "cache.remove" + when FLUSH_EVENT_NAMES.include?(event_name) + "cache.flush" + else + "other" + end + end + end + end + end +end diff --git a/sentry-rails/lib/sentry/rails/version.rb b/sentry-rails/lib/sentry/rails/version.rb index 2e8928c9f..99cb90c13 100644 --- a/sentry-rails/lib/sentry/rails/version.rb +++ b/sentry-rails/lib/sentry/rails/version.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module Sentry module Rails - VERSION = "5.18.0" + VERSION = "5.22.1" end end diff --git a/sentry-rails/sentry-rails.gemspec b/sentry-rails/sentry-rails.gemspec index 996a722e4..69e8e45f3 100644 --- a/sentry-rails/sentry-rails.gemspec +++ b/sentry-rails/sentry-rails.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "lib/sentry/rails/version" Gem::Specification.new do |spec| @@ -7,21 +9,27 @@ Gem::Specification.new do |spec| spec.description = spec.summary = "A gem that provides Rails integration for the Sentry error logger" spec.email = "accounts@sentry.io" spec.license = 'MIT' - spec.homepage = "https://github.com/getsentry/sentry-ruby" spec.platform = Gem::Platform::RUBY spec.required_ruby_version = '>= 2.4' spec.extra_rdoc_files = ["README.md", "LICENSE.txt"] - spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples)'`.split("\n") + spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples|\.rubocop\.yml)'`.split("\n") + + github_root_uri = 'https://github.com/getsentry/sentry-ruby' + spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}" - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = spec.homepage - spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" + spec.metadata = { + "homepage_uri" => spec.homepage, + "source_code_uri" => spec.homepage, + "changelog_uri" => "#{github_root_uri}/blob/#{spec.version}/CHANGELOG.md", + "bug_tracker_uri" => "#{github_root_uri}/issues", + "documentation_uri" => "http://www.rubydoc.info/gems/#{spec.name}/#{spec.version}" + } spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.add_dependency "railties", ">= 5.0" - spec.add_dependency "sentry-ruby", "~> 5.18.0" + spec.add_dependency "sentry-ruby", "~> 5.22.1" end diff --git a/sentry-rails/spec/dummy/test_rails_app/app.rb b/sentry-rails/spec/dummy/test_rails_app/app.rb index af5a54400..5b1bd82a9 100644 --- a/sentry-rails/spec/dummy/test_rails_app/app.rb +++ b/sentry-rails/spec/dummy/test_rails_app/app.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ENV["RAILS_ENV"] = "test" require "rails" @@ -9,7 +11,6 @@ require 'sentry/rails' -ActiveSupport::Deprecation.silenced = true ActiveRecord::Base.logger = Logger.new(nil) ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: "db") @@ -50,12 +51,23 @@ def self.name end end + app.config.active_support.deprecation = :silence app.config.action_controller.view_paths = "spec/dummy/test_rails_app" app.config.hosts = nil app.config.secret_key_base = "test" app.config.logger = ActiveSupport::Logger.new(nil) - app.config.eager_load = true + app.config.eager_load = false app.config.active_job.queue_adapter = :test + app.config.cache_store = :memory_store + app.config.action_controller.perform_caching = true + app.config.filter_parameters += [:password, :secret] + + # Eager load namespaces can be accumulated after repeated initializations and make initialization + # slower after each run + # This is especially obvious in Rails 7.2, because of https://github.com/rails/rails/pull/49987, but other constants's + # accumulation can also cause slowdown + # Because this is not necessary for the test, we can simply clear it here + app.config.eager_load_namespaces.clear configure_app(app) diff --git a/sentry-rails/spec/dummy/test_rails_app/apps/5-0.rb b/sentry-rails/spec/dummy/test_rails_app/apps/5-0.rb index f1b3a2758..a68a68358 100644 --- a/sentry-rails/spec/dummy/test_rails_app/apps/5-0.rb +++ b/sentry-rails/spec/dummy/test_rails_app/apps/5-0.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ActiveRecord::Schema.define do create_table :posts, force: true do |t| end diff --git a/sentry-rails/spec/dummy/test_rails_app/apps/5-2.rb b/sentry-rails/spec/dummy/test_rails_app/apps/5-2.rb index e7b1f2180..9dfcf0d62 100644 --- a/sentry-rails/spec/dummy/test_rails_app/apps/5-2.rb +++ b/sentry-rails/spec/dummy/test_rails_app/apps/5-2.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ActiveRecord::Schema.define do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false diff --git a/sentry-rails/spec/dummy/test_rails_app/apps/6-0.rb b/sentry-rails/spec/dummy/test_rails_app/apps/6-0.rb index e7b1f2180..9dfcf0d62 100644 --- a/sentry-rails/spec/dummy/test_rails_app/apps/6-0.rb +++ b/sentry-rails/spec/dummy/test_rails_app/apps/6-0.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ActiveRecord::Schema.define do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false diff --git a/sentry-rails/spec/dummy/test_rails_app/apps/6-1.rb b/sentry-rails/spec/dummy/test_rails_app/apps/6-1.rb index ac504704e..a9148a3ee 100644 --- a/sentry-rails/spec/dummy/test_rails_app/apps/6-1.rb +++ b/sentry-rails/spec/dummy/test_rails_app/apps/6-1.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ActiveRecord::Schema.define do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false diff --git a/sentry-rails/spec/dummy/test_rails_app/apps/7-0.rb b/sentry-rails/spec/dummy/test_rails_app/apps/7-0.rb index ac504704e..a9148a3ee 100644 --- a/sentry-rails/spec/dummy/test_rails_app/apps/7-0.rb +++ b/sentry-rails/spec/dummy/test_rails_app/apps/7-0.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ActiveRecord::Schema.define do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false diff --git a/sentry-rails/spec/dummy/test_rails_app/apps/7-1.rb b/sentry-rails/spec/dummy/test_rails_app/apps/7-1.rb index ac504704e..a9148a3ee 100644 --- a/sentry-rails/spec/dummy/test_rails_app/apps/7-1.rb +++ b/sentry-rails/spec/dummy/test_rails_app/apps/7-1.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ActiveRecord::Schema.define do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false diff --git a/sentry-rails/spec/dummy/test_rails_app/configs/5-0.rb b/sentry-rails/spec/dummy/test_rails_app/configs/5-0.rb index 70cb662b3..d291ac8f8 100644 --- a/sentry-rails/spec/dummy/test_rails_app/configs/5-0.rb +++ b/sentry-rails/spec/dummy/test_rails_app/configs/5-0.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + def run_pre_initialize_cleanup; end def configure_app(app); end diff --git a/sentry-rails/spec/dummy/test_rails_app/configs/5-2.rb b/sentry-rails/spec/dummy/test_rails_app/configs/5-2.rb index f6b4f39e5..d5d88d8b6 100644 --- a/sentry-rails/spec/dummy/test_rails_app/configs/5-2.rb +++ b/sentry-rails/spec/dummy/test_rails_app/configs/5-2.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_storage/engine" def run_pre_initialize_cleanup; end diff --git a/sentry-rails/spec/dummy/test_rails_app/configs/6-0.rb b/sentry-rails/spec/dummy/test_rails_app/configs/6-0.rb index 7b0673b3c..bab5746a8 100644 --- a/sentry-rails/spec/dummy/test_rails_app/configs/6-0.rb +++ b/sentry-rails/spec/dummy/test_rails_app/configs/6-0.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_storage/engine" require "action_cable/engine" diff --git a/sentry-rails/spec/dummy/test_rails_app/configs/6-1.rb b/sentry-rails/spec/dummy/test_rails_app/configs/6-1.rb index 4532be7ca..e36c83344 100644 --- a/sentry-rails/spec/dummy/test_rails_app/configs/6-1.rb +++ b/sentry-rails/spec/dummy/test_rails_app/configs/6-1.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_storage/engine" require "action_cable/engine" diff --git a/sentry-rails/spec/dummy/test_rails_app/configs/7-0.rb b/sentry-rails/spec/dummy/test_rails_app/configs/7-0.rb index dd5d0716c..84652ae04 100644 --- a/sentry-rails/spec/dummy/test_rails_app/configs/7-0.rb +++ b/sentry-rails/spec/dummy/test_rails_app/configs/7-0.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_storage/engine" require "action_cable/engine" diff --git a/sentry-rails/spec/dummy/test_rails_app/configs/7-1.rb b/sentry-rails/spec/dummy/test_rails_app/configs/7-1.rb index d17545fe0..d0126d985 100644 --- a/sentry-rails/spec/dummy/test_rails_app/configs/7-1.rb +++ b/sentry-rails/spec/dummy/test_rails_app/configs/7-1.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_storage/engine" require "action_cable/engine" require "sentry/rails/error_subscriber" diff --git a/sentry-rails/spec/dummy/test_rails_app/configs/7-2.rb b/sentry-rails/spec/dummy/test_rails_app/configs/7-2.rb index d17545fe0..d0126d985 100644 --- a/sentry-rails/spec/dummy/test_rails_app/configs/7-2.rb +++ b/sentry-rails/spec/dummy/test_rails_app/configs/7-2.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_storage/engine" require "action_cable/engine" require "sentry/rails/error_subscriber" diff --git a/sentry-rails/spec/isolated/active_job_activation.rb b/sentry-rails/spec/isolated/active_job_activation.rb index e4ab3f358..e8c8cb571 100644 --- a/sentry-rails/spec/isolated/active_job_activation.rb +++ b/sentry-rails/spec/isolated/active_job_activation.rb @@ -2,6 +2,8 @@ # for https://github.com/getsentry/sentry-ruby/issues/1249 require "active_job/railtie" +# Rails 7.2 added HealthCheckController, which requires ActionController +require "action_controller/railtie" require "active_support/all" require "sentry/rails" require "minitest/autorun" diff --git a/sentry-rails/spec/sentry/generator_spec.rb b/sentry-rails/spec/sentry/generator_spec.rb index ac4aff730..0e1f6856f 100644 --- a/sentry-rails/spec/sentry/generator_spec.rb +++ b/sentry-rails/spec/sentry/generator_spec.rb @@ -4,14 +4,45 @@ require "rails/generators/test_case" require "generators/sentry_generator" +behavior_module = if defined?(Rails::Generators::Testing::Behaviour) + Rails::Generators::Testing::Behaviour +else + Rails::Generators::Testing::Behavior +end + RSpec.describe SentryGenerator do - include ::Rails::Generators::Testing::Behaviour + include behavior_module include FileUtils self.destination File.expand_path('../../tmp', __dir__) self.generator_class = described_class + let(:layout_file) do + File.join(destination_root, "app/views/layouts/application.html.erb") + end + before do prepare_destination + + FileUtils.mkdir_p(File.dirname(layout_file)) + + File.write(layout_file, <<~STR) + + + + SentryTesting + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + + + + <%= yield %> + + + STR end it "creates a initializer file" do @@ -29,6 +60,22 @@ RUBY end + it "injects meta tag into the layout" do + run_generator + + content = File.read(layout_file) + + expect(content).to include("Sentry.get_trace_propagation_meta.html_safe") + end + + it "doesn't inject meta tag when it's disabled" do + run_generator %w[--inject-meta false] + + content = File.read(layout_file) + + expect(content).not_to include("Sentry.get_trace_propagation_meta.html_safe") + end + context "with a DSN option" do it "creates a initializer file with the DSN" do run_generator %w[--dsn foobarbaz] diff --git a/sentry-rails/spec/sentry/rails/action_cable_spec.rb b/sentry-rails/spec/sentry/rails/action_cable_spec.rb index 4b353adff..e47ad2efa 100644 --- a/sentry-rails/spec/sentry/rails/action_cable_spec.rb +++ b/sentry-rails/spec/sentry/rails/action_cable_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + if defined?(ActionCable) && ActionCable.version >= Gem::Version.new('6.0.0') require "spec_helper" require "action_cable/engine" diff --git a/sentry-rails/spec/sentry/rails/activejob_spec.rb b/sentry-rails/spec/sentry/rails/activejob_spec.rb index f5892d944..ffcb4d44c 100644 --- a/sentry-rails/spec/sentry/rails/activejob_spec.rb +++ b/sentry-rails/spec/sentry/rails/activejob_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "active_job/railtie" @@ -66,7 +68,7 @@ class FailedJobWithCron < FailedJob end -RSpec.describe "without Sentry initialized" do +RSpec.describe "without Sentry initialized", type: :job do it "runs job" do expect { FailedJob.perform_now }.to raise_error(FailedJob::TestError) end @@ -76,7 +78,7 @@ class FailedJobWithCron < FailedJob end end -RSpec.describe "ActiveJob integration" do +RSpec.describe "ActiveJob integration", type: :job do before do make_basic_app end @@ -307,21 +309,17 @@ def perform(event, hint) context "when we are using an adapter which has a specific integration" do before do - Sentry.configuration.rails.skippable_job_adapters = ["ActiveJob::QueueAdapters::SidekiqAdapter"] + Sentry.configuration.rails.skippable_job_adapters = ["ActiveJob::QueueAdapters::TestAdapter"] end - it "does not trigger sentry and re-raises" do - begin - original_queue_adapter = FailedJob.queue_adapter - FailedJob.queue_adapter = :sidekiq + after do + Sentry.configuration.rails.skippable_job_adapters = [] + end - expect { FailedJob.perform_now }.to raise_error(FailedJob::TestError) + it "does not trigger sentry and re-raises" do + expect { FailedJob.perform_now }.to raise_error(FailedJob::TestError) - expect(transport.events.size).to eq(0) - ensure - # this doesn't affect test result, but we shouldn't change it anyway - FailedJob.queue_adapter = original_queue_adapter - end + expect(transport.events.size).to eq(0) end end diff --git a/sentry-rails/spec/sentry/rails/breadcrumbs/active_support_logger_spec.rb b/sentry-rails/spec/sentry/rails/breadcrumbs/active_support_logger_spec.rb index b156eaf59..e6a0ae6e3 100644 --- a/sentry-rails/spec/sentry/rails/breadcrumbs/active_support_logger_spec.rb +++ b/sentry-rails/spec/sentry/rails/breadcrumbs/active_support_logger_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe "Sentry::Breadcrumbs::ActiveSupportLogger", type: :request do diff --git a/sentry-rails/spec/sentry/rails/breadcrumbs/monotonic_active_support_logger_spec.rb b/sentry-rails/spec/sentry/rails/breadcrumbs/monotonic_active_support_logger_spec.rb index 0334531f6..031443c25 100644 --- a/sentry-rails/spec/sentry/rails/breadcrumbs/monotonic_active_support_logger_spec.rb +++ b/sentry-rails/spec/sentry/rails/breadcrumbs/monotonic_active_support_logger_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" diff --git a/sentry-rails/spec/sentry/rails/client_spec.rb b/sentry-rails/spec/sentry/rails/client_spec.rb index 6c6403f16..86ef1e110 100644 --- a/sentry-rails/spec/sentry/rails/client_spec.rb +++ b/sentry-rails/spec/sentry/rails/client_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Client, type: :request, retry: 3, skip: Gem::Version.new(Rails.version) < Gem::Version.new('5.1.0') do @@ -5,8 +7,16 @@ Sentry.get_current_client.transport end + let(:expected_initial_active_record_connections_count) do + if Gem::Version.new(Rails.version) < Gem::Version.new('7.2.0') + 1 + else + 0 + end + end + before do - expect(ActiveRecord::Base.connection_pool.stat[:busy]).to eq(1) + expect(ActiveRecord::Base.connection_pool.stat[:busy]).to eq(expected_initial_active_record_connections_count) end def send_events @@ -35,7 +45,7 @@ def send_events expect(transport.events.count).to eq(5) - expect(ActiveRecord::Base.connection_pool.stat[:busy]).to eq(1) + expect(ActiveRecord::Base.connection_pool.stat[:busy]).to eq(expected_initial_active_record_connections_count) end end @@ -53,7 +63,7 @@ def send_events expect(transport.events.count).to eq(5) - expect(ActiveRecord::Base.connection_pool.stat[:busy]).to eq(1) + expect(ActiveRecord::Base.connection_pool.stat[:busy]).to eq(expected_initial_active_record_connections_count) end end end diff --git a/sentry-rails/spec/sentry/rails/configuration_spec.rb b/sentry-rails/spec/sentry/rails/configuration_spec.rb index f27ee99a9..37c5a41d6 100644 --- a/sentry-rails/spec/sentry/rails/configuration_spec.rb +++ b/sentry-rails/spec/sentry/rails/configuration_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Rails::Configuration do @@ -23,12 +25,12 @@ class MySubscriber; end it "returns the default subscribers" do - expect(subject.tracing_subscribers.size).to eq(3) + expect(subject.tracing_subscribers.size).to eq(4) end it "is customizable" do subject.tracing_subscribers << MySubscriber - expect(subject.tracing_subscribers.size).to eq(4) + expect(subject.tracing_subscribers.size).to eq(5) end it "is replaceable" do diff --git a/sentry-rails/spec/sentry/rails/controller_methods_spec.rb b/sentry-rails/spec/sentry/rails/controller_methods_spec.rb index a751e1366..6254dc290 100644 --- a/sentry-rails/spec/sentry/rails/controller_methods_spec.rb +++ b/sentry-rails/spec/sentry/rails/controller_methods_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require "sentry/rails/controller_methods" diff --git a/sentry-rails/spec/sentry/rails/error_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/error_subscriber_spec.rb index 1bce4baca..1dcc1a0f4 100644 --- a/sentry-rails/spec/sentry/rails/error_subscriber_spec.rb +++ b/sentry-rails/spec/sentry/rails/error_subscriber_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'sentry/rails/error_subscriber' diff --git a/sentry-rails/spec/sentry/rails/event_spec.rb b/sentry-rails/spec/sentry/rails/event_spec.rb index 674615f3a..8dc969d08 100644 --- a/sentry-rails/spec/sentry/rails/event_spec.rb +++ b/sentry-rails/spec/sentry/rails/event_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Event do @@ -30,6 +32,7 @@ it 'marks in_app correctly' do frames = hash[:exception][:values][0][:stacktrace][:frames] expect(frames[0][:filename]).to eq("test/some/other/path") + expect(frames[0][:abs_path]).to eq("test/some/other/path") expect(frames[0][:in_app]).to eq(true) expect(frames[1][:filename]).to eq("/app/some/other/path") expect(frames[1][:in_app]).to eq(false) @@ -39,7 +42,7 @@ expect(frames[3][:in_app]).to eq(true) expect(frames[4][:filename]).to eq("vendor/bundle/some_gem.rb") expect(frames[4][:in_app]).to eq(false) - expect(frames[5][:filename]).to eq("vendor/bundle/cache/other_gem.rb") + expect(frames[5][:filename]).to eq("dummy/test_rails_app/vendor/bundle/cache/other_gem.rb") expect(frames[5][:in_app]).to eq(false) end @@ -48,6 +51,7 @@ $LOAD_PATH << "#{Rails.root}/app/models" frames = hash[:exception][:values][0][:stacktrace][:frames] expect(frames[3][:filename]).to eq("app/models/user.rb") + expect(frames[3][:abs_path]).to eq("#{Rails.root}/app/models/user.rb") $LOAD_PATH.delete("#{Rails.root}/app/models") end end @@ -56,7 +60,8 @@ it 'normalizes the filename using the load path' do $LOAD_PATH.push "vendor/bundle" frames = hash[:exception][:values][0][:stacktrace][:frames] - expect(frames[5][:filename]).to eq("cache/other_gem.rb") + expect(frames[5][:filename]).to eq("dummy/test_rails_app/vendor/bundle/cache/other_gem.rb") + expect(frames[5][:abs_path]).to eq("#{Rails.root}/vendor/bundle/cache/other_gem.rb") $LOAD_PATH.pop end end @@ -64,7 +69,8 @@ context "when a non-in_app path under project_root isn't on the load path" do it 'normalizes the filename using project_root' do frames = hash[:exception][:values][0][:stacktrace][:frames] - expect(frames[5][:filename]).to eq("vendor/bundle/cache/other_gem.rb") + expect(frames[5][:filename]).to eq("dummy/test_rails_app/vendor/bundle/cache/other_gem.rb") + expect(frames[5][:abs_path]).to eq("#{Rails.root}/vendor/bundle/cache/other_gem.rb") end end end diff --git a/sentry-rails/spec/sentry/rails/tracing/action_controller_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/tracing/action_controller_subscriber_spec.rb index f147e1b8e..9d0a4df4e 100644 --- a/sentry-rails/spec/sentry/rails/tracing/action_controller_subscriber_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing/action_controller_subscriber_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Rails::Tracing::ActionControllerSubscriber, :subscriber, type: :request do diff --git a/sentry-rails/spec/sentry/rails/tracing/action_view_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/tracing/action_view_subscriber_spec.rb index aa2473312..555584024 100644 --- a/sentry-rails/spec/sentry/rails/tracing/action_view_subscriber_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing/action_view_subscriber_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Rails::Tracing::ActionViewSubscriber, :subscriber, type: :request do diff --git a/sentry-rails/spec/sentry/rails/tracing/active_record_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/tracing/active_record_subscriber_spec.rb index aa29b82d2..be674869f 100644 --- a/sentry-rails/spec/sentry/rails/tracing/active_record_subscriber_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing/active_record_subscriber_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Rails::Tracing::ActiveRecordSubscriber, :subscriber do @@ -49,6 +51,7 @@ def foo Post.all.to_a end query_line = __LINE__ - 2 + rspec_class = self.name # RSpec::ExampleGroups::[....] before do transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub) @@ -92,6 +95,7 @@ def foo data = span[:data] expect(data["code.filepath"]).to eq(__FILE__) expect(data["code.lineno"]).to eq(query_line) + expect(data["code.namespace"]).to eq(rspec_class) if RUBY_VERSION.to_f >= 3.4 expect(data["code.function"]).to eq("foo") end end diff --git a/sentry-rails/spec/sentry/rails/tracing/active_storage_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/tracing/active_storage_subscriber_spec.rb index 5d34db1e3..a5e186d7e 100644 --- a/sentry-rails/spec/sentry/rails/tracing/active_storage_subscriber_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing/active_storage_subscriber_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Rails::Tracing::ActiveStorageSubscriber, :subscriber, type: :request, skip: Rails.version.to_f <= 5.2 do diff --git a/sentry-rails/spec/sentry/rails/tracing/active_support_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/tracing/active_support_subscriber_spec.rb new file mode 100644 index 000000000..c276063da --- /dev/null +++ b/sentry-rails/spec/sentry/rails/tracing/active_support_subscriber_spec.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Sentry::Rails::Tracing::ActiveSupportSubscriber, :subscriber, type: :request do + let(:transport) do + Sentry.get_current_client.transport + end + + context "when transaction is sampled" do + before do + make_basic_app do |config, app| + config.traces_sample_rate = 1.0 + config.rails.tracing_subscribers = [described_class] + end + end + + it "tracks cache write" do + transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub) + Sentry.get_current_scope.set_span(transaction) + + Rails.cache.write("my_cache_key", "my_cache_value") + transaction.finish + + expect(transport.events.count).to eq(1) + cache_transaction = transport.events.first.to_hash + expect(cache_transaction[:type]).to eq("transaction") + + expect(cache_transaction[:spans].count).to eq(1) + expect(cache_transaction[:spans][0][:op]).to eq("cache.put") + expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails") + end + + # + it "tracks cache increment" do + skip("Tracks on Rails 8.0 for all Cache Stores; Until then only MemCached and Redis Stores.") if Rails.version.to_f < 8.0 + + Rails.cache.write("my_cache_key", 0) + + transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub) + Sentry.get_current_scope.set_span(transaction) + Rails.cache.increment("my_cache_key") + + transaction.finish + + expect(Rails.cache.read("my_cache_key")).to eq(1) + expect(transport.events.count).to eq(1) + cache_transaction = transport.events.first.to_hash + expect(cache_transaction[:type]).to eq("transaction") + expect(cache_transaction[:spans].count).to eq(1) + expect(cache_transaction[:spans][0][:op]).to eq("cache.put") + expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails") + end + + it "tracks cache decrement" do + skip("Tracks on Rails 8.0 for all Cache Stores; Until then only MemCached and Redis Stores.") if Rails.version.to_f < 8.0 + + Rails.cache.write("my_cache_key", 0) + + transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub) + Sentry.get_current_scope.set_span(transaction) + Rails.cache.decrement("my_cache_key") + + transaction.finish + + expect(transport.events.count).to eq(1) + cache_transaction = transport.events.first.to_hash + expect(cache_transaction[:type]).to eq("transaction") + expect(cache_transaction[:spans].count).to eq(1) + expect(cache_transaction[:spans][0][:op]).to eq("cache.put") + expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails") + end + + it "tracks cache read" do + transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub) + Sentry.get_current_scope.set_span(transaction) + Rails.cache.read("my_cache_key") + + transaction.finish + + expect(transport.events.count).to eq(1) + cache_transaction = transport.events.first.to_hash + expect(cache_transaction[:type]).to eq("transaction") + expect(cache_transaction[:spans].count).to eq(1) + expect(cache_transaction[:spans][0][:op]).to eq("cache.get") + expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails") + end + + it "tracks cache delete" do + transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub) + Sentry.get_current_scope.set_span(transaction) + + Rails.cache.read("my_cache_key") + + transaction.finish + + expect(transport.events.count).to eq(1) + cache_transaction = transport.events.first.to_hash + expect(cache_transaction[:type]).to eq("transaction") + expect(cache_transaction[:spans].count).to eq(1) + expect(cache_transaction[:spans][0][:op]).to eq("cache.get") + expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails") + end + it "tracks cache prune" do + transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub) + Sentry.get_current_scope.set_span(transaction) + + Rails.cache.write("my_cache_key", 123, expires_in: 0.seconds) + + Rails.cache.prune(0) + + transaction.finish + + expect(transport.events.count).to eq(1) + cache_transaction = transport.events.first.to_hash + expect(cache_transaction[:type]).to eq("transaction") + expect(cache_transaction[:spans].count).to eq(2) + expect(cache_transaction[:spans][1][:op]).to eq("cache.flush") + expect(cache_transaction[:spans][1][:origin]).to eq("auto.cache.rails") + end + + it "tracks sets cache hit" do + skip("cache.hit is unset on Rails 6.0.x.") if Rails.version.to_i == 6 + + Rails.cache.write("my_cache_key", "my_cache_value") + transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub) + Sentry.get_current_scope.set_span(transaction) + Rails.cache.read("my_cache_key") + Rails.cache.read("my_cache_key_non_existing") + + transaction.finish + expect(transport.events.count).to eq(1) + cache_transaction = transport.events.first.to_hash + expect(cache_transaction[:type]).to eq("transaction") + expect(cache_transaction[:spans].count).to eq(2) + expect(cache_transaction[:spans][0][:op]).to eq("cache.get") + expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails") + expect(cache_transaction[:spans][0][:data]['cache.key']).to eq(["my_cache_key"]) + expect(cache_transaction[:spans][0][:data]['cache.hit']).to eq(true) + + expect(cache_transaction[:spans][1][:op]).to eq("cache.get") + expect(cache_transaction[:spans][1][:origin]).to eq("auto.cache.rails") + expect(cache_transaction[:spans][1][:data]['cache.key']).to eq(["my_cache_key_non_existing"]) + expect(cache_transaction[:spans][1][:data]['cache.hit']).to eq(false) + end + + it "tracks cache delete" do + Rails.cache.write("my_cache_key", "my_cache_value") + transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub) + Sentry.get_current_scope.set_span(transaction) + Rails.cache.delete("my_cache_key") + + transaction.finish + expect(transport.events.count).to eq(1) + cache_transaction = transport.events.first.to_hash + expect(cache_transaction[:type]).to eq("transaction") + expect(cache_transaction[:spans].count).to eq(1) + expect(cache_transaction[:spans][0][:op]).to eq("cache.remove") + expect(cache_transaction[:spans][0][:origin]).to eq("auto.cache.rails") + expect(cache_transaction[:spans][0][:data]['cache.key']).to eq(["my_cache_key"]) + end + end + + context "when transaction is not sampled" do + before do + make_basic_app + end + + it "doesn't record spans" do + Rails.cache.write("my_cache_key", "my_cache_value") + + expect(transport.events.count).to eq(0) + end + end +end diff --git a/sentry-rails/spec/sentry/rails/tracing_spec.rb b/sentry-rails/spec/sentry/rails/tracing_spec.rb index 287bd8acc..36063f39d 100644 --- a/sentry-rails/spec/sentry/rails/tracing_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Rails::Tracing, type: :request do @@ -104,6 +106,52 @@ end end + describe "filtering pii data" do + context "with send_default_pii = false" do + before do + make_basic_app do |config| + config.traces_sample_rate = 1.0 + config.send_default_pii = false + end + end + + it "does not record sensitive params" do + get "/posts?foo=bar&password=42&secret=baz" + transaction = transport.events.last.to_hash + + params = transaction[:spans][0][:data][:params] + expect(params["foo"]).to eq("bar") + expect(params["password"]).to eq("[FILTERED]") + expect(params["secret"]).to eq("[FILTERED]") + + path = transaction[:spans][0][:data][:path] + expect(path).to eq("/posts?foo=bar&password=[FILTERED]&secret=[FILTERED]") + end + end + + context "with send_default_pii = true" do + before do + make_basic_app do |config| + config.traces_sample_rate = 1.0 + config.send_default_pii = true + end + end + + it "records all params" do + get "/posts?foo=bar&password=42&secret=baz" + transaction = transport.events.last.to_hash + + params = transaction[:spans][0][:data][:params] + expect(params["foo"]).to eq("bar") + expect(params["password"]).to eq("42") + expect(params["secret"]).to eq("baz") + + path = transaction[:spans][0][:data][:path] + expect(path).to eq("/posts?foo=bar&password=42&secret=baz") + end + end + end + context "with instrumenter :otel" do before do make_basic_app do |config| diff --git a/sentry-rails/spec/sentry/rails_spec.rb b/sentry-rails/spec/sentry/rails_spec.rb index 88c1dae1f..8d4e5367f 100644 --- a/sentry-rails/spec/sentry/rails_spec.rb +++ b/sentry-rails/spec/sentry/rails_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Rails, type: :request do @@ -240,6 +242,16 @@ def capture_in_separate_process(exit_code:) expect(traces.dig(-1, "function")).to be_nil end + it "makes sure BacktraceCleaner gem cleanup doesn't affect context lines population" do + get "/view_exception" + + traces = event.dig("exception", "values", 0, "stacktrace", "frames") + gem_frame = traces.find { |t| t["abs_path"].match(/actionview/) } + expect(gem_frame["pre_context"]).not_to be_empty + expect(gem_frame["post_context"]).not_to be_empty + expect(gem_frame["context_line"]).not_to be_empty + end + it "doesn't filters exception backtrace if backtrace_cleanup_callback is overridden" do make_basic_app do |config| config.backtrace_cleanup_callback = lambda { |backtrace| backtrace } @@ -354,6 +366,32 @@ def capture_in_separate_process(exit_code:) expect(transport.events.count).to eq(0) end + + it "captures string messages through error reporter" do + Rails.error.report("Test message", severity: :info, handled: true, context: { foo: "bar" }) + + expect(transport.events.count).to eq(1) + event = transport.events.first + + expect(event.message).to eq("Test message") + expect(event.level).to eq(:info) + expect(event.contexts).to include({ "rails.error" => { foo: "bar" } }) + expect(event.tags).to include({ handled: true }) + end + + it "skips non-string and non-exception errors" do + expect { + Sentry.init do |config| + config.logger = Logger.new($stdout) + end + + Sentry.logger.debug("Expected an Exception or a String, got: #{312.inspect}") + + Rails.error.report(312, severity: :info, handled: true, context: { foo: "bar" }) + }.to output(/Expected an Exception or a String, got: 312/).to_stdout + + expect(transport.events.count).to eq(0) + end end end end diff --git a/sentry-rails/spec/sentry/send_event_job_spec.rb b/sentry-rails/spec/sentry/send_event_job_spec.rb index c50130f65..00f89b2ed 100644 --- a/sentry-rails/spec/sentry/send_event_job_spec.rb +++ b/sentry-rails/spec/sentry/send_event_job_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_job" require "spec_helper" diff --git a/sentry-rails/spec/spec_helper.rb b/sentry-rails/spec/spec_helper.rb index 55d3334d3..5928e7134 100644 --- a/sentry-rails/spec/spec_helper.rb +++ b/sentry-rails/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler/setup" begin require "debug/prelude" @@ -54,6 +56,8 @@ ENV.delete('RAILS_ENV') ENV.delete('RACK_ENV') end + + config.include ActiveJob::TestHelper, type: :job end def reload_send_event_job diff --git a/sentry-raven/spec/raven/json_spec.rb b/sentry-raven/spec/raven/json_spec.rb index 482b79fb3..9c511c48b 100644 --- a/sentry-raven/spec/raven/json_spec.rb +++ b/sentry-raven/spec/raven/json_spec.rb @@ -2,6 +2,7 @@ # JSON impl we use handles it in the way that we expect. require 'spec_helper' +require 'ostruct' RSpec.describe JSON do data = [ diff --git a/sentry-resque/.rubocop.yml b/sentry-resque/.rubocop.yml new file mode 120000 index 000000000..7cc18e076 --- /dev/null +++ b/sentry-resque/.rubocop.yml @@ -0,0 +1 @@ +../.rubocop.yml \ No newline at end of file diff --git a/sentry-resque/Gemfile b/sentry-resque/Gemfile index 58c13cada..5819261e7 100644 --- a/sentry-resque/Gemfile +++ b/sentry-resque/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" git_source(:github) { |name| "https://github.com/#{name}.git" } @@ -8,3 +10,8 @@ gem "sentry-ruby", path: "../sentry-ruby" gem "resque-retry", "~> 1.8" eval_gemfile File.expand_path("../Gemfile", __dir__) + +group :rails do + gem "sentry-rails", path: "../sentry-rails" + gem "rails" +end diff --git a/sentry-resque/Gemfile_with_rails.rb b/sentry-resque/Gemfile_with_rails.rb deleted file mode 100644 index bceb75350..000000000 --- a/sentry-resque/Gemfile_with_rails.rb +++ /dev/null @@ -1,4 +0,0 @@ -eval_gemfile File.expand_path("Gemfile", __dir__) - -gem "sentry-rails", path: "../sentry-rails" -gem "rails" diff --git a/sentry-resque/Rakefile b/sentry-resque/Rakefile index 7b2756854..13afab191 100644 --- a/sentry-resque/Rakefile +++ b/sentry-resque/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler/gem_tasks" require "rspec/core/rake_task" diff --git a/sentry-resque/bin/console b/sentry-resque/bin/console index 660c7a889..f0f5a7b6a 100755 --- a/sentry-resque/bin/console +++ b/sentry-resque/bin/console @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require "bundler/setup" require "sentry/ruby" diff --git a/sentry-resque/example/Gemfile b/sentry-resque/example/Gemfile index b9432d218..20d98ac4a 100644 --- a/sentry-resque/example/Gemfile +++ b/sentry-resque/example/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" gem "rails" diff --git a/sentry-resque/example/app.rb b/sentry-resque/example/app.rb index 843e8c028..2e85eca19 100644 --- a/sentry-resque/example/app.rb +++ b/sentry-resque/example/app.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_job" require "resque" require "sentry-resque" diff --git a/sentry-resque/lib/sentry-resque.rb b/sentry-resque/lib/sentry-resque.rb index 10af757c0..1d499af8a 100644 --- a/sentry-resque/lib/sentry-resque.rb +++ b/sentry-resque/lib/sentry-resque.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "resque" require "sentry-ruby" require "sentry/integrable" diff --git a/sentry-resque/lib/sentry/resque.rb b/sentry-resque/lib/sentry/resque.rb index 84907d84f..4ef85e0dc 100644 --- a/sentry-resque/lib/sentry/resque.rb +++ b/sentry-resque/lib/sentry/resque.rb @@ -40,14 +40,14 @@ def record(queue, worker, payload, &block) finish_transaction(transaction, 200) rescue Exception => exception - klass = if payload['class'].respond_to?(:constantize) - payload['class'].constantize + klass = if payload["class"].respond_to?(:constantize) + payload["class"].constantize else - Object.const_get(payload['class']) + Object.const_get(payload["class"]) end raise if Sentry.configuration.resque.report_after_job_retries && - defined?(::Resque::Plugins::Retry) == 'constant' && + defined?(::Resque::Plugins::Retry) == "constant" && klass.is_a?(::Resque::Plugins::Retry) && !klass.retry_limit_reached? diff --git a/sentry-resque/lib/sentry/resque/configuration.rb b/sentry-resque/lib/sentry/resque/configuration.rb index f0d7ffae8..126d6f7eb 100644 --- a/sentry-resque/lib/sentry/resque/configuration.rb +++ b/sentry-resque/lib/sentry/resque/configuration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry class Configuration attr_reader :resque diff --git a/sentry-resque/lib/sentry/resque/version.rb b/sentry-resque/lib/sentry/resque/version.rb index 0d02e6b0c..b89656b63 100644 --- a/sentry-resque/lib/sentry/resque/version.rb +++ b/sentry-resque/lib/sentry/resque/version.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module Sentry module Resque - VERSION = "5.18.0" + VERSION = "5.22.1" end end diff --git a/sentry-resque/sentry-resque.gemspec b/sentry-resque/sentry-resque.gemspec index e8e9e2eb4..9683fe3a5 100644 --- a/sentry-resque/sentry-resque.gemspec +++ b/sentry-resque/sentry-resque.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "lib/sentry/resque/version" Gem::Specification.new do |spec| @@ -7,21 +9,27 @@ Gem::Specification.new do |spec| spec.description = spec.summary = "A gem that provides Resque integration for the Sentry error logger" spec.email = "accounts@sentry.io" spec.license = 'MIT' - spec.homepage = "https://github.com/getsentry/sentry-ruby" spec.platform = Gem::Platform::RUBY spec.required_ruby_version = '>= 2.4' spec.extra_rdoc_files = ["README.md", "LICENSE.txt"] - spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples)'`.split("\n") + spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples|\.rubocop\.yml)'`.split("\n") + + github_root_uri = 'https://github.com/getsentry/sentry-ruby' + spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}" - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = spec.homepage - spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" + spec.metadata = { + "homepage_uri" => spec.homepage, + "source_code_uri" => spec.homepage, + "changelog_uri" => "#{github_root_uri}/blob/#{spec.version}/CHANGELOG.md", + "bug_tracker_uri" => "#{github_root_uri}/issues", + "documentation_uri" => "http://www.rubydoc.info/gems/#{spec.name}/#{spec.version}" + } spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_dependency "sentry-ruby", "~> 5.18.0" + spec.add_dependency "sentry-ruby", "~> 5.22.1" spec.add_dependency "resque", ">= 1.24" end diff --git a/sentry-resque/spec/sentry/resque/configuration_spec.rb b/sentry-resque/spec/sentry/resque/configuration_spec.rb index abde7c4d4..af2f464d9 100644 --- a/sentry-resque/spec/sentry/resque/configuration_spec.rb +++ b/sentry-resque/spec/sentry/resque/configuration_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Resque::Configuration do diff --git a/sentry-resque/spec/sentry/resque_spec.rb b/sentry-resque/spec/sentry/resque_spec.rb index 2a4739b6f..7239284e3 100644 --- a/sentry-resque/spec/sentry/resque_spec.rb +++ b/sentry-resque/spec/sentry/resque_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" def process_job(worker) diff --git a/sentry-resque/spec/sentry/tracing_spec.rb b/sentry-resque/spec/sentry/tracing_spec.rb index e77b84f4e..bf81f9006 100644 --- a/sentry-resque/spec/sentry/tracing_spec.rb +++ b/sentry-resque/spec/sentry/tracing_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Resque do diff --git a/sentry-resque/spec/spec_helper.rb b/sentry-resque/spec/spec_helper.rb index dd212aab3..73b3baefe 100644 --- a/sentry-resque/spec/spec_helper.rb +++ b/sentry-resque/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler/setup" begin require "debug/prelude" diff --git a/sentry-ruby.code-workspace b/sentry-ruby.code-workspace new file mode 100644 index 000000000..5c90507e0 --- /dev/null +++ b/sentry-ruby.code-workspace @@ -0,0 +1,30 @@ +{ + "folders": [ + { + "path": "sentry-ruby" + }, + { + "path": "sentry-rails" + }, + { + "path": "sentry-sidekiq" + }, + { + "path": "sentry-delayed_job" + }, + { + "path": "sentry-resque" + }, + { + "path": "sentry-opentelemetry" + }, + { + "path": ".github" + } + ], + "extensions": { + "recommendations": [ + "Shopify.ruby-lsp" + ] + } +} diff --git a/sentry-ruby/.rubocop.yml b/sentry-ruby/.rubocop.yml new file mode 120000 index 000000000..7cc18e076 --- /dev/null +++ b/sentry-ruby/.rubocop.yml @@ -0,0 +1 @@ +../.rubocop.yml \ No newline at end of file diff --git a/sentry-ruby/Gemfile b/sentry-ruby/Gemfile index 9c7524dd1..5371ddbf2 100644 --- a/sentry-ruby/Gemfile +++ b/sentry-ruby/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" git_source(:github) { |name| "https://github.com/#{name}.git" } @@ -14,6 +16,7 @@ gem "puma" gem "timecop" gem "stackprof" unless RUBY_PLATFORM == "java" +gem "vernier", platforms: :ruby if RUBY_VERSION >= "3.2.1" gem "graphql", ">= 2.2.6" if RUBY_VERSION.to_f >= 2.7 @@ -24,5 +27,7 @@ gem "benchmark-memory" gem "yard", github: "lsegal/yard" gem "webrick" +gem "faraday" +gem "excon" eval_gemfile File.expand_path("../Gemfile", __dir__) diff --git a/sentry-ruby/README.md b/sentry-ruby/README.md index 878056150..c98757a32 100644 --- a/sentry-ruby/README.md +++ b/sentry-ruby/README.md @@ -106,3 +106,13 @@ To learn more about sampling transactions, please visit the [official documentat * [![Discord Chat](https://img.shields.io/discord/621778831602221064?logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/PXa5Apfe7K) * [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](https://stackoverflow.com/questions/tagged/sentry) * [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry) + +## Contributing to the SDK + +Please make sure to read the [CONTRIBUTING.md](https://github.com/getsentry/sentry-ruby/blob/master/CONTRIBUTING.md) before making a pull request. + +Thanks to everyone who has contributed to this project so far. + + + + diff --git a/sentry-ruby/Rakefile b/sentry-ruby/Rakefile index c199ef8f8..fb7a8c183 100644 --- a/sentry-ruby/Rakefile +++ b/sentry-ruby/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rake/clean" CLOBBER.include "pkg" diff --git a/sentry-ruby/benchmarks/allocation_comparison.rb b/sentry-ruby/benchmarks/allocation_comparison.rb index 90c7b2b19..c765b2ff6 100644 --- a/sentry-ruby/benchmarks/allocation_comparison.rb +++ b/sentry-ruby/benchmarks/allocation_comparison.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'benchmark/memory' require "sentry-ruby" require "sentry/benchmarks/benchmark_transport" diff --git a/sentry-ruby/benchmarks/allocation_report.rb b/sentry-ruby/benchmarks/allocation_report.rb index 3e0359d72..54fbd4139 100644 --- a/sentry-ruby/benchmarks/allocation_report.rb +++ b/sentry-ruby/benchmarks/allocation_report.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'benchmark/ipsa' require "sentry-ruby" require "sentry/benchmarks/benchmark_transport" diff --git a/sentry-ruby/benchmarks/exception_locals_capturing.rb b/sentry-ruby/benchmarks/exception_locals_capturing.rb index 00c05f9e9..6ebc46ee9 100644 --- a/sentry-ruby/benchmarks/exception_locals_capturing.rb +++ b/sentry-ruby/benchmarks/exception_locals_capturing.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'benchmark/ips' def raise_n_exceptions(n, with_sleep: false) diff --git a/sentry-ruby/bin/console b/sentry-ruby/bin/console index 57a532604..6ab9e0413 100755 --- a/sentry-ruby/bin/console +++ b/sentry-ruby/bin/console @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require "bundler/setup" require "debug" diff --git a/sentry-ruby/lib/sentry-ruby.rb b/sentry-ruby/lib/sentry-ruby.rb index a48906925..b7ecdea53 100644 --- a/sentry-ruby/lib/sentry-ruby.rb +++ b/sentry-ruby/lib/sentry-ruby.rb @@ -25,6 +25,7 @@ require "sentry/backpressure_monitor" require "sentry/cron/monitor_check_ins" require "sentry/metrics" +require "sentry/vernier/profiler" [ "sentry/rake", @@ -41,14 +42,16 @@ module Sentry CAPTURED_SIGNATURE = :@__sentry_captured - LOGGER_PROGNAME = "sentry".freeze + LOGGER_PROGNAME = "sentry" - SENTRY_TRACE_HEADER_NAME = "sentry-trace".freeze + SENTRY_TRACE_HEADER_NAME = "sentry-trace" - BAGGAGE_HEADER_NAME = "baggage".freeze + BAGGAGE_HEADER_NAME = "baggage" THREAD_LOCAL = :sentry_hub + MUTEX = Mutex.new + class << self # @!visibility private def exception_locals_tp @@ -211,6 +214,13 @@ def set_context(*args) get_current_scope.set_context(*args) end + # @!method add_attachment + # @!macro add_attachment + def add_attachment(**opts) + return unless initialized? + get_current_scope.add_attachment(**opts) + end + ##### Main APIs ##### # Initializes the SDK with given configuration. @@ -267,8 +277,10 @@ def close @background_worker.shutdown - @main_hub = nil - Thread.current.thread_variable_set(THREAD_LOCAL, nil) + MUTEX.synchronize do + @main_hub = nil + Thread.current.thread_variable_set(THREAD_LOCAL, nil) + end end # Returns true if the SDK is initialized. @@ -295,7 +307,7 @@ def csp_report_uri # # @return [Hub] def get_main_hub - @main_hub + MUTEX.synchronize { @main_hub } end # Takes an instance of Sentry::Breadcrumb and stores it to the current active scope. @@ -556,7 +568,7 @@ def get_trace_propagation_headers # # @return [String] def get_trace_propagation_meta - return '' unless initialized? + return "" unless initialized? get_current_hub.get_trace_propagation_meta end @@ -601,3 +613,5 @@ def utc_now require "sentry/redis" require "sentry/puma" require "sentry/graphql" +require "sentry/faraday" +require "sentry/excon" diff --git a/sentry-ruby/lib/sentry/attachment.rb b/sentry-ruby/lib/sentry/attachment.rb new file mode 100644 index 000000000..847d58c47 --- /dev/null +++ b/sentry-ruby/lib/sentry/attachment.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Sentry + class Attachment + PathNotFoundError = Class.new(StandardError) + + attr_reader :bytes, :filename, :path, :content_type + + def initialize(bytes: nil, filename: nil, content_type: nil, path: nil) + @bytes = bytes + @filename = filename || infer_filename(path) + @path = path + @content_type = content_type + end + + def to_envelope_headers + { type: "attachment", filename: filename, content_type: content_type, length: payload.bytesize } + end + + def payload + @payload ||= if bytes + bytes + else + File.binread(path) + end + rescue Errno::ENOENT + raise PathNotFoundError, "Failed to read attachment file, file not found: #{path}" + end + + private + + def infer_filename(path) + if path + File.basename(path) + else + raise ArgumentError, "filename or path is required" + end + end + end +end diff --git a/sentry-ruby/lib/sentry/backtrace.rb b/sentry-ruby/lib/sentry/backtrace.rb index b7ebb2159..7c17adfec 100644 --- a/sentry-ruby/lib/sentry/backtrace.rb +++ b/sentry-ruby/lib/sentry/backtrace.rb @@ -12,11 +12,11 @@ class Line RUBY_INPUT_FORMAT = / ^ \s* (?: [a-zA-Z]: | uri:classloader: )? ([^:]+ | <.*>): (\d+) - (?: :in\s('|`)([^']+)')?$ - /x.freeze + (?: :in\s('|`)(?:([\w:]+)\#)?([^']+)')?$ + /x # org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:170) - JAVA_INPUT_FORMAT = /^(.+)\.([^\.]+)\(([^\:]+)\:(\d+)\)$/.freeze + JAVA_INPUT_FORMAT = /^(.+)\.([^\.]+)\(([^\:]+)\:(\d+)\)$/ # The file portion of the line (such as app/models/user.rb) attr_reader :file @@ -37,10 +37,11 @@ class Line # @return [Line] The parsed backtrace line def self.parse(unparsed_line, in_app_pattern = nil) ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT) + if ruby_match - _, file, number, _, method = ruby_match.to_a + _, file, number, _, module_name, method = ruby_match.to_a file.sub!(/\.class$/, RB_EXTENSION) - module_name = nil + module_name = module_name else java_match = unparsed_line.match(JAVA_INPUT_FORMAT) _, module_name, method, file, number = java_match.to_a @@ -80,8 +81,6 @@ def inspect end end - APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test|spec)/.freeze - # holder for an Array of Backtrace::Line instances attr_reader :lines @@ -91,7 +90,7 @@ def self.parse(backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_cal ruby_lines = backtrace_cleanup_callback.call(ruby_lines) if backtrace_cleanup_callback in_app_pattern ||= begin - Regexp.new("^(#{project_root}/)?#{app_dirs_pattern || APP_DIRS_PATTERN}") + Regexp.new("^(#{project_root}/)?#{app_dirs_pattern}") end lines = ruby_lines.to_a.map do |unparsed_line| diff --git a/sentry-ruby/lib/sentry/baggage.rb b/sentry-ruby/lib/sentry/baggage.rb index d766e999d..dc02dd8e6 100644 --- a/sentry-ruby/lib/sentry/baggage.rb +++ b/sentry-ruby/lib/sentry/baggage.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'cgi' +require "cgi" module Sentry # A {https://www.w3.org/TR/baggage W3C Baggage Header} implementation. class Baggage - SENTRY_PREFIX = 'sentry-' - SENTRY_PREFIX_REGEX = /^sentry-/.freeze + SENTRY_PREFIX = "sentry-" + SENTRY_PREFIX_REGEX = /^sentry-/ # @return [Hash] attr_reader :items @@ -30,14 +30,14 @@ def self.from_incoming_header(header) items = {} mutable = true - header.split(',').each do |item| + header.split(",").each do |item| item = item.strip - key, val = item.split('=') + key, val = item.split("=") next unless key && val next unless key =~ SENTRY_PREFIX_REGEX - baggage_key = key.split('-')[1] + baggage_key = key.split("-")[1] next unless baggage_key items[CGI.unescape(baggage_key)] = CGI.unescape(val) @@ -64,7 +64,7 @@ def dynamic_sampling_context # @return [String] def serialize items = @items.map { |k, v| "#{SENTRY_PREFIX}#{CGI.escape(k)}=#{CGI.escape(v)}" } - items.join(',') + items.join(",") end end end diff --git a/sentry-ruby/lib/sentry/breadcrumb/sentry_logger.rb b/sentry-ruby/lib/sentry/breadcrumb/sentry_logger.rb index 455535f01..4d94e513a 100644 --- a/sentry-ruby/lib/sentry/breadcrumb/sentry_logger.rb +++ b/sentry-ruby/lib/sentry/breadcrumb/sentry_logger.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true -require 'logger' +require "logger" module Sentry class Breadcrumb module SentryLogger LEVELS = { - ::Logger::DEBUG => 'debug', - ::Logger::INFO => 'info', - ::Logger::WARN => 'warn', - ::Logger::ERROR => 'error', - ::Logger::FATAL => 'fatal' + ::Logger::DEBUG => "debug", + ::Logger::INFO => "info", + ::Logger::WARN => "warn", + ::Logger::ERROR => "error", + ::Logger::FATAL => "fatal" }.freeze def add(*args, &block) diff --git a/sentry-ruby/lib/sentry/check_in_event.rb b/sentry-ruby/lib/sentry/check_in_event.rb index 7a10872b5..91ae17dc3 100644 --- a/sentry-ruby/lib/sentry/check_in_event.rb +++ b/sentry-ruby/lib/sentry/check_in_event.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require 'securerandom' -require 'sentry/cron/monitor_config' +require "securerandom" +require "sentry/cron/monitor_config" module Sentry class CheckInEvent < Event - TYPE = 'check_in' + TYPE = "check_in" # uuid to identify this check-in. # @return [String] @@ -43,7 +43,7 @@ def initialize( self.status = status self.duration = duration self.monitor_config = monitor_config - self.check_in_id = check_in_id || SecureRandom.uuid.delete('-') + self.check_in_id = check_in_id || SecureRandom.uuid.delete("-") end # @return [Hash] diff --git a/sentry-ruby/lib/sentry/client.rb b/sentry-ruby/lib/sentry/client.rb index 608c4e1d5..9d48ee301 100644 --- a/sentry-ruby/lib/sentry/client.rb +++ b/sentry-ruby/lib/sentry/client.rb @@ -30,7 +30,7 @@ def initialize(configuration) else @transport = case configuration.dsn&.scheme - when 'http', 'https' + when "http", "https" HTTPTransport.new(configuration) else DummyTransport.new(configuration) @@ -49,25 +49,35 @@ def capture_event(event, scope, hint = {}) return unless configuration.sending_allowed? if event.is_a?(ErrorEvent) && !configuration.sample_allowed? - transport.record_lost_event(:sample_rate, 'error') + transport.record_lost_event(:sample_rate, "error") return end event_type = event.is_a?(Event) ? event.type : event["type"] data_category = Envelope::Item.data_category(event_type) + + is_transaction = event.is_a?(TransactionEvent) + spans_before = is_transaction ? event.spans.size : 0 + event = scope.apply_to_event(event, hint) if event.nil? log_debug("Discarded event because one of the event processors returned nil") transport.record_lost_event(:event_processor, data_category) + transport.record_lost_event(:event_processor, "span", num: spans_before + 1) if is_transaction return + elsif is_transaction + spans_delta = spans_before - event.spans.size + transport.record_lost_event(:event_processor, "span", num: spans_delta) if spans_delta > 0 end if async_block = configuration.async dispatch_async_event(async_block, event, hint) elsif configuration.background_worker_threads != 0 && hint.fetch(:background, true) - queued = dispatch_background_event(event, hint) - transport.record_lost_event(:queue_overflow, data_category) unless queued + unless dispatch_background_event(event, hint) + transport.record_lost_event(:queue_overflow, data_category) + transport.record_lost_event(:queue_overflow, "span", num: spans_before + 1) if is_transaction + end else send_event(event, hint) end @@ -168,6 +178,7 @@ def event_from_transaction(transaction) def send_event(event, hint = nil) event_type = event.is_a?(Event) ? event.type : event["type"] data_category = Envelope::Item.data_category(event_type) + spans_before = event.is_a?(TransactionEvent) ? event.spans.size : 0 if event_type != TransactionEvent::TYPE && configuration.before_send event = configuration.before_send.call(event, hint) @@ -184,8 +195,13 @@ def send_event(event, hint = nil) if event.nil? log_debug("Discarded event because before_send_transaction returned nil") - transport.record_lost_event(:before_send, data_category) + transport.record_lost_event(:before_send, "transaction") + transport.record_lost_event(:before_send, "span", num: spans_before + 1) return + else + spans_after = event.is_a?(TransactionEvent) ? event.spans.size : 0 + spans_delta = spans_before - spans_after + transport.record_lost_event(:before_send, "span", num: spans_delta) if spans_delta > 0 end end @@ -196,6 +212,7 @@ def send_event(event, hint = nil) rescue => e log_error("Event sending failed", e, debug: configuration.debug) transport.record_lost_event(:network_error, data_category) + transport.record_lost_event(:network_error, "span", num: spans_before + 1) if event.is_a?(TransactionEvent) raise end diff --git a/sentry-ruby/lib/sentry/configuration.rb b/sentry-ruby/lib/sentry/configuration.rb index 069094bda..b13c81c49 100644 --- a/sentry-ruby/lib/sentry/configuration.rb +++ b/sentry-ruby/lib/sentry/configuration.rb @@ -3,7 +3,8 @@ require "concurrent/utility/processor_counter" require "sentry/utils/exception_cause_chain" -require 'sentry/utils/custom_inspection' +require "sentry/utils/custom_inspection" +require "sentry/utils/env_helper" require "sentry/dsn" require "sentry/release_detector" require "sentry/transport/configuration" @@ -22,6 +23,8 @@ class Configuration # have an `engines` dir at the root of your project, you may want # to set this to something like /(app|config|engines|lib)/ # + # The default is value is /(bin|exe|app|config|lib|test|spec)/ + # # @return [Regexp, nil] attr_accessor :app_dirs_pattern @@ -187,6 +190,11 @@ def capture_exception_frame_locals=(value) # @return [String] attr_accessor :project_root + # Whether to strip the load path while constructing the backtrace frame filename. + # Defaults to true. + # @return [Boolean] + attr_accessor :strip_backtrace_load_path + # Insert sentry-trace to outgoing requests' headers # @return [Boolean] attr_accessor :propagate_traces @@ -283,6 +291,10 @@ def capture_exception_frame_locals=(value) # @return [Symbol] attr_reader :instrumenter + # The profiler class + # @return [Class] + attr_reader :profiler_class + # Take a float between 0.0 and 1.0 as the sample rate for capturing profiles. # Note that this rate is relative to traces_sample_rate / traces_sampler, # i.e. the profile is sampled by this rate after the transaction is sampled. @@ -302,18 +314,18 @@ def capture_exception_frame_locals=(value) # But they are mostly considered as noise and should be ignored by default # Please see https://github.com/getsentry/sentry-ruby/pull/2026 for more information PUMA_IGNORE_DEFAULT = [ - 'Puma::MiniSSL::SSLError', - 'Puma::HttpParserError', - 'Puma::HttpParserError501' + "Puma::MiniSSL::SSLError", + "Puma::HttpParserError", + "Puma::HttpParserError501" ].freeze # Most of these errors generate 4XX responses. In general, Sentry clients # only automatically report 5xx responses. IGNORE_DEFAULT = [ - 'Mongoid::Errors::DocumentNotFound', - 'Rack::QueryParser::InvalidParameterError', - 'Rack::QueryParser::ParameterTypeError', - 'Sinatra::NotFound' + "Mongoid::Errors::DocumentNotFound", + "Rack::QueryParser::InvalidParameterError", + "Rack::QueryParser::ParameterTypeError", + "Sinatra::NotFound" ].freeze RACK_ENV_WHITELIST_DEFAULT = %w[ @@ -323,18 +335,20 @@ def capture_exception_frame_locals=(value) ].freeze HEROKU_DYNO_METADATA_MESSAGE = "You are running on Heroku but haven't enabled Dyno Metadata. For Sentry's "\ - "release detection to work correctly, please run `heroku labs:enable runtime-dyno-metadata`".freeze + "release detection to work correctly, please run `heroku labs:enable runtime-dyno-metadata`" - LOG_PREFIX = "** [Sentry] ".freeze - MODULE_SEPARATOR = "::".freeze + LOG_PREFIX = "** [Sentry] " + MODULE_SEPARATOR = "::" SKIP_INSPECTION_ATTRIBUTES = [:@linecache, :@stacktrace_builder] INSTRUMENTERS = [:sentry, :otel] - PROPAGATION_TARGETS_MATCH_ALL = /.*/.freeze + PROPAGATION_TARGETS_MATCH_ALL = /.*/ DEFAULT_PATCHES = %i[redis puma http].freeze + APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test|spec)/ + class << self # Post initialization callbacks are called at the end of initialization process # allowing extending the configuration of sentry-ruby by multiple extensions @@ -349,11 +363,12 @@ def add_post_initialization_callback(&block) end def initialize - self.app_dirs_pattern = nil - self.debug = false - self.background_worker_threads = (Concurrent.processor_count / 2.0).ceil + self.app_dirs_pattern = APP_DIRS_PATTERN + self.debug = Sentry::Utils::EnvHelper.env_to_bool(ENV["SENTRY_DEBUG"]) + self.background_worker_threads = (processor_count / 2.0).ceil self.background_worker_max_queue = BackgroundWorker::DEFAULT_MAX_QUEUE self.backtrace_cleanup_callback = nil + self.strip_backtrace_load_path = true self.max_breadcrumbs = BreadcrumbBuffer::DEFAULT_SIZE self.breadcrumbs_logger = [] self.context_lines = 3 @@ -376,8 +391,11 @@ def initialize self.auto_session_tracking = true self.enable_backpressure_handling = false self.trusted_proxies = [] - self.dsn = ENV['SENTRY_DSN'] - self.spotlight = false + self.dsn = ENV["SENTRY_DSN"] + + spotlight_env = ENV["SENTRY_SPOTLIGHT"] + spotlight_bool = Sentry::Utils::EnvHelper.env_to_bool(spotlight_env, strict: true) + self.spotlight = spotlight_bool.nil? ? (spotlight_env || false) : spotlight_bool self.server_name = server_name_from_env self.instrumenter = :sentry self.trace_propagation_targets = [PROPAGATION_TARGETS_MATCH_ALL] @@ -389,6 +407,8 @@ def initialize self.traces_sampler = nil self.enable_tracing = nil + self.profiler_class = Sentry::Profiler + @transport = Transport::Configuration.new @cron = Cron::Configuration.new @metrics = Metrics::Configuration.new @@ -484,6 +504,18 @@ def profiles_sample_rate=(profiles_sample_rate) @profiles_sample_rate = profiles_sample_rate end + def profiler_class=(profiler_class) + if profiler_class == Sentry::Vernier::Profiler + begin + require "vernier" + rescue LoadError + raise ArgumentError, "Please add the 'vernier' gem to your Gemfile to use the Vernier profiler with Sentry." + end + end + + @profiler_class = profiler_class + end + def sending_allowed? spotlight || sending_to_dsn_allowed? end @@ -555,7 +587,8 @@ def stacktrace_builder app_dirs_pattern: @app_dirs_pattern, linecache: @linecache, context_lines: @context_lines, - backtrace_cleanup_callback: @backtrace_cleanup_callback + backtrace_cleanup_callback: @backtrace_cleanup_callback, + strip_backtrace_load_path: @strip_backtrace_load_path ) end @@ -632,12 +665,12 @@ def valid? end def environment_from_env - ENV['SENTRY_CURRENT_ENV'] || ENV['SENTRY_ENVIRONMENT'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development' + ENV["SENTRY_CURRENT_ENV"] || ENV["SENTRY_ENVIRONMENT"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" end def server_name_from_env if running_on_heroku? - ENV['DYNO'] + ENV["DYNO"] else # Try to resolve the hostname to an FQDN, but fall back to whatever # the load name is. @@ -654,5 +687,10 @@ def run_post_initialization_callbacks instance_eval(&hook) end end + + def processor_count + available_processor_count = Concurrent.available_processor_count if Concurrent.respond_to?(:available_processor_count) + available_processor_count || Concurrent.processor_count + end end end diff --git a/sentry-ruby/lib/sentry/core_ext/object/deep_dup.rb b/sentry-ruby/lib/sentry/core_ext/object/deep_dup.rb index d1141f128..2cc0d47f6 100644 --- a/sentry-ruby/lib/sentry/core_ext/object/deep_dup.rb +++ b/sentry-ruby/lib/sentry/core_ext/object/deep_dup.rb @@ -2,7 +2,7 @@ return if Object.method_defined?(:deep_dup) -require 'sentry/core_ext/object/duplicable' +require "sentry/core_ext/object/duplicable" ######################################### # This file was copied from Rails 5.2 # diff --git a/sentry-ruby/lib/sentry/cron/monitor_check_ins.rb b/sentry-ruby/lib/sentry/cron/monitor_check_ins.rb index b7eee3f5f..df7445286 100644 --- a/sentry-ruby/lib/sentry/cron/monitor_check_ins.rb +++ b/sentry-ruby/lib/sentry/cron/monitor_check_ins.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry module Cron module MonitorCheckIns @@ -12,12 +14,12 @@ def perform(*args, **opts) :in_progress, monitor_config: monitor_config) - start = Sentry.utc_now.to_i + start = Metrics::Timing.duration_start begin # need to do this on ruby <= 2.6 sadly ret = method(:perform).super_method.arity == 0 ? super() : super - duration = Sentry.utc_now.to_i - start + duration = Metrics::Timing.duration_end(start) Sentry.capture_check_in(slug, :ok, @@ -27,7 +29,7 @@ def perform(*args, **opts) ret rescue Exception - duration = Sentry.utc_now.to_i - start + duration = Metrics::Timing.duration_end(start) Sentry.capture_check_in(slug, :error, @@ -57,7 +59,7 @@ def sentry_monitor_check_ins(slug: nil, monitor_config: nil) def sentry_monitor_slug(name: self.name) @sentry_monitor_slug ||= begin - slug = name.gsub('::', '-').downcase + slug = name.gsub("::", "-").downcase slug[-MAX_SLUG_LENGTH..-1] || slug end end diff --git a/sentry-ruby/lib/sentry/cron/monitor_config.rb b/sentry-ruby/lib/sentry/cron/monitor_config.rb index 68b8c709c..dfd28a4bd 100644 --- a/sentry-ruby/lib/sentry/cron/monitor_config.rb +++ b/sentry-ruby/lib/sentry/cron/monitor_config.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'sentry/cron/monitor_schedule' +require "sentry/cron/monitor_schedule" module Sentry module Cron diff --git a/sentry-ruby/lib/sentry/dsn.rb b/sentry-ruby/lib/sentry/dsn.rb index b05ace531..3877b57fb 100644 --- a/sentry-ruby/lib/sentry/dsn.rb +++ b/sentry-ruby/lib/sentry/dsn.rb @@ -4,7 +4,7 @@ module Sentry class DSN - PORT_MAP = { 'http' => 80, 'https' => 443 }.freeze + PORT_MAP = { "http" => 80, "https" => 443 }.freeze REQUIRED_ATTRIBUTES = %w[host path public_key project_id].freeze attr_reader :scheme, :secret_key, :port, *REQUIRED_ATTRIBUTES @@ -13,7 +13,7 @@ def initialize(dsn_string) @raw_value = dsn_string uri = URI.parse(dsn_string) - uri_path = uri.path.split('/') + uri_path = uri.path.split("/") if uri.user # DSN-style string @@ -25,7 +25,7 @@ def initialize(dsn_string) @scheme = uri.scheme @host = uri.host @port = uri.port if uri.port - @path = uri_path.join('/') + @path = uri_path.join("/") end def valid? diff --git a/sentry-ruby/lib/sentry/envelope.rb b/sentry-ruby/lib/sentry/envelope.rb index 96bb83beb..d60e79ba7 100644 --- a/sentry-ruby/lib/sentry/envelope.rb +++ b/sentry-ruby/lib/sentry/envelope.rb @@ -3,91 +3,6 @@ module Sentry # @api private class Envelope - class Item - STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 500 - MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 1000 - - attr_accessor :headers, :payload - - def initialize(headers, payload) - @headers = headers - @payload = payload - end - - def type - @headers[:type] || 'event' - end - - # rate limits and client reports use the data_category rather than envelope item type - def self.data_category(type) - case type - when 'session', 'attachment', 'transaction', 'profile' then type - when 'sessions' then 'session' - when 'check_in' then 'monitor' - when 'statsd', 'metric_meta' then 'metric_bucket' - when 'event' then 'error' - when 'client_report' then 'internal' - else 'default' - end - end - - def data_category - self.class.data_category(type) - end - - def to_s - [JSON.generate(@headers), @payload.is_a?(String) ? @payload : JSON.generate(@payload)].join("\n") - end - - def serialize - result = to_s - - if result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE - remove_breadcrumbs! - result = to_s - end - - if result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE - reduce_stacktrace! - result = to_s - end - - [result, result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE] - end - - def size_breakdown - payload.map do |key, value| - "#{key}: #{JSON.generate(value).bytesize}" - end.join(", ") - end - - private - - def remove_breadcrumbs! - if payload.key?(:breadcrumbs) - payload.delete(:breadcrumbs) - elsif payload.key?("breadcrumbs") - payload.delete("breadcrumbs") - end - end - - def reduce_stacktrace! - if exceptions = payload.dig(:exception, :values) || payload.dig("exception", "values") - exceptions.each do |exception| - # in most cases there is only one exception (2 or 3 when have multiple causes), so we won't loop through this double condition much - traces = exception.dig(:stacktrace, :frames) || exception.dig("stacktrace", "frames") - - if traces && traces.size > STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD - size_on_both_ends = STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD / 2 - traces.replace( - traces[0..(size_on_both_ends - 1)] + traces[-size_on_both_ends..-1], - ) - end - end - end - end - end - attr_accessor :headers, :items def initialize(headers = {}) @@ -108,3 +23,5 @@ def event_id end end end + +require_relative "envelope/item" diff --git a/sentry-ruby/lib/sentry/envelope/item.rb b/sentry-ruby/lib/sentry/envelope/item.rb new file mode 100644 index 000000000..e1539bf6c --- /dev/null +++ b/sentry-ruby/lib/sentry/envelope/item.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module Sentry + # @api private + class Envelope::Item + STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 500 + MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 1000 + + SIZE_LIMITS = Hash.new(MAX_SERIALIZED_PAYLOAD_SIZE).update( + "profile" => 1024 * 1000 * 50 + ) + + attr_reader :size_limit, :headers, :payload, :type, :data_category + + # rate limits and client reports use the data_category rather than envelope item type + def self.data_category(type) + case type + when "session", "attachment", "transaction", "profile", "span" then type + when "sessions" then "session" + when "check_in" then "monitor" + when "statsd", "metric_meta" then "metric_bucket" + when "event" then "error" + when "client_report" then "internal" + else "default" + end + end + + def initialize(headers, payload) + @headers = headers + @payload = payload + @type = headers[:type] || "event" + @data_category = self.class.data_category(type) + @size_limit = SIZE_LIMITS[type] + end + + def to_s + [JSON.generate(@headers), @payload.is_a?(String) ? @payload : JSON.generate(@payload)].join("\n") + end + + def serialize + result = to_s + + if result.bytesize > size_limit + remove_breadcrumbs! + result = to_s + end + + if result.bytesize > size_limit + reduce_stacktrace! + result = to_s + end + + [result, result.bytesize > size_limit] + end + + def size_breakdown + payload.map do |key, value| + "#{key}: #{JSON.generate(value).bytesize}" + end.join(", ") + end + + private + + def remove_breadcrumbs! + if payload.key?(:breadcrumbs) + payload.delete(:breadcrumbs) + elsif payload.key?("breadcrumbs") + payload.delete("breadcrumbs") + end + end + + def reduce_stacktrace! + if exceptions = payload.dig(:exception, :values) || payload.dig("exception", "values") + exceptions.each do |exception| + # in most cases there is only one exception (2 or 3 when have multiple causes), so we won't loop through this double condition much + traces = exception.dig(:stacktrace, :frames) || exception.dig("stacktrace", "frames") + + if traces && traces.size > STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD + size_on_both_ends = STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD / 2 + traces.replace( + traces[0..(size_on_both_ends - 1)] + traces[-size_on_both_ends..-1], + ) + end + end + end + end + end +end diff --git a/sentry-ruby/lib/sentry/event.rb b/sentry-ruby/lib/sentry/event.rb index 97777a1c9..154452599 100644 --- a/sentry-ruby/lib/sentry/event.rb +++ b/sentry-ruby/lib/sentry/event.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'socket' -require 'securerandom' -require 'sentry/interface' -require 'sentry/backtrace' -require 'sentry/utils/real_ip' -require 'sentry/utils/request_id' -require 'sentry/utils/custom_inspection' +require "socket" +require "securerandom" +require "sentry/interface" +require "sentry/backtrace" +require "sentry/utils/real_ip" +require "sentry/utils/request_id" +require "sentry/utils/custom_inspection" module Sentry # This is an abstract class that defines the shared attributes of an event. @@ -42,6 +42,9 @@ class Event # @return [Hash, nil] attr_accessor :dynamic_sampling_context + # @return [Array] + attr_accessor :attachments + # @param configuration [Configuration] # @param integration_meta [Hash, nil] # @param message [String, nil] @@ -57,6 +60,7 @@ def initialize(configuration:, integration_meta: nil, message: nil) @extra = {} @contexts = {} @tags = {} + @attachments = [] @fingerprint = [] @dynamic_sampling_context = nil @@ -104,9 +108,7 @@ def rack_env=(env) unless request || env.empty? add_request_interface(env) - if @send_default_pii - user[:ip_address] = calculate_real_ip_from_rack(env) - end + user[:ip_address] ||= calculate_real_ip_from_rack(env) if @send_default_pii if request_id = Utils::RequestId.read_from(env) tags[:request_id] = request_id diff --git a/sentry-ruby/lib/sentry/excon.rb b/sentry-ruby/lib/sentry/excon.rb new file mode 100644 index 000000000..39559a992 --- /dev/null +++ b/sentry-ruby/lib/sentry/excon.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +Sentry.register_patch(:excon) do + if defined?(::Excon) + require "sentry/excon/middleware" + if Excon.defaults[:middlewares] + Excon.defaults[:middlewares] << Sentry::Excon::Middleware unless Excon.defaults[:middlewares].include?(Sentry::Excon::Middleware) + end + end +end diff --git a/sentry-ruby/lib/sentry/excon/middleware.rb b/sentry-ruby/lib/sentry/excon/middleware.rb new file mode 100644 index 000000000..9c6e59467 --- /dev/null +++ b/sentry-ruby/lib/sentry/excon/middleware.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Sentry + module Excon + OP_NAME = "http.client" + + class Middleware < ::Excon::Middleware::Base + def initialize(stack) + super + @instrumenter = Instrumenter.new + end + + def request_call(datum) + @instrumenter.start_transaction(datum) + @stack.request_call(datum) + end + + def response_call(datum) + @instrumenter.finish_transaction(datum) + @stack.response_call(datum) + end + end + + class Instrumenter + SPAN_ORIGIN = "auto.http.excon" + BREADCRUMB_CATEGORY = "http" + + include Utils::HttpTracing + + def start_transaction(env) + return unless Sentry.initialized? + + current_span = Sentry.get_current_scope&.span + @span = current_span&.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) + + request_info = extract_request_info(env) + + if propagate_trace?(request_info[:url]) + set_propagation_headers(env[:headers]) + end + end + + def finish_transaction(response) + return unless @span + + response_status = response[:response][:status] + request_info = extract_request_info(response) + + if record_sentry_breadcrumb? + record_sentry_breadcrumb(request_info, response_status) + end + + set_span_info(@span, request_info, response_status) + ensure + @span&.finish + end + + private + + def extract_request_info(env) + url = env[:scheme] + "://" + env[:hostname] + env[:path] + result = { method: env[:method].to_s.upcase, url: url } + + if Sentry.configuration.send_default_pii + result[:query] = env[:query] + + # Handle excon 1.0.0+ + result[:query] = build_nested_query(result[:query]) unless result[:query].is_a?(String) + + result[:body] = env[:body] + end + + result + end + end + end +end diff --git a/sentry-ruby/lib/sentry/faraday.rb b/sentry-ruby/lib/sentry/faraday.rb new file mode 100644 index 000000000..05d5f45bd --- /dev/null +++ b/sentry-ruby/lib/sentry/faraday.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Sentry + module Faraday + OP_NAME = "http.client" + + module Connection + # Since there's no way to preconfigure Faraday connections and add our instrumentation + # by default, we need to extend the connection constructor and do it there + # + # @see https://lostisland.github.io/faraday/#/customization/index?id=configuration + def initialize(url = nil, options = nil) + super + + # Ensure that we attach instrumentation only if the adapter is not net/http + # because if is is, then the net/http instrumentation will take care of it + if builder.adapter.name != "Faraday::Adapter::NetHttp" + # Make sure that it's going to be the first middleware so that it can capture + # the entire request processing involving other middlewares + builder.insert(0, ::Faraday::Request::Instrumentation, name: OP_NAME, instrumenter: Instrumenter.new) + end + end + end + + class Instrumenter + SPAN_ORIGIN = "auto.http.faraday" + BREADCRUMB_CATEGORY = "http" + + include Utils::HttpTracing + + def instrument(op_name, env, &block) + return block.call unless Sentry.initialized? + + Sentry.with_child_span(op: op_name, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |sentry_span| + request_info = extract_request_info(env) + + if propagate_trace?(request_info[:url]) + set_propagation_headers(env[:request_headers]) + end + + res = block.call + response_status = res.status + + if record_sentry_breadcrumb? + record_sentry_breadcrumb(request_info, response_status) + end + + if sentry_span + set_span_info(sentry_span, request_info, response_status) + end + + res + end + end + + private + + def extract_request_info(env) + url = env[:url].scheme + "://" + env[:url].host + env[:url].path + result = { method: env[:method].to_s.upcase, url: url } + + if Sentry.configuration.send_default_pii + result[:query] = env[:url].query + result[:body] = env[:body] + end + + result + end + end + end +end + +Sentry.register_patch(:faraday) do + if defined?(::Faraday) + ::Faraday::Connection.prepend(Sentry::Faraday::Connection) + end +end diff --git a/sentry-ruby/lib/sentry/graphql.rb b/sentry-ruby/lib/sentry/graphql.rb index 17bb792c6..645481a4d 100644 --- a/sentry-ruby/lib/sentry/graphql.rb +++ b/sentry-ruby/lib/sentry/graphql.rb @@ -4,6 +4,6 @@ if defined?(::GraphQL::Schema) && defined?(::GraphQL::Tracing::SentryTrace) && ::GraphQL::Schema.respond_to?(:trace_with) ::GraphQL::Schema.trace_with(::GraphQL::Tracing::SentryTrace, set_transaction_name: true) else - config.logger.warn(Sentry::LOGGER_PROGNAME) { 'You tried to enable the GraphQL integration but no GraphQL gem was detected. Make sure you have the `graphql` gem (>= 2.2.6) in your Gemfile.' } + config.logger.warn(Sentry::LOGGER_PROGNAME) { "You tried to enable the GraphQL integration but no GraphQL gem was detected. Make sure you have the `graphql` gem (>= 2.2.6) in your Gemfile." } end end diff --git a/sentry-ruby/lib/sentry/hub.rb b/sentry-ruby/lib/sentry/hub.rb index d0615ee28..d6e3ad2d0 100644 --- a/sentry-ruby/lib/sentry/hub.rb +++ b/sentry-ruby/lib/sentry/hub.rb @@ -73,7 +73,13 @@ def push_scope end def pop_scope - @stack.pop + if @stack.size > 1 + @stack.pop + else + # We never want to enter a situation where we have no scope and no client + client = current_client + @stack = [Layer.new(client, Scope.new)] + end end def start_transaction(transaction: nil, custom_sampling_context: {}, instrumenter: :sentry, **options) @@ -195,10 +201,12 @@ def capture_event(event, **options, &block) elsif !options.empty? unsupported_option_keys = scope.update_from_options(**options) - configuration.log_debug <<~MSG - Options #{unsupported_option_keys} are not supported and will not be applied to the event. - You may want to set them under the `extra` option. - MSG + unless unsupported_option_keys.empty? + configuration.log_debug <<~MSG + Options #{unsupported_option_keys} are not supported and will not be applied to the event. + You may want to set them under the `extra` option. + MSG + end end event = current_client.capture_event(event, scope, hint) @@ -212,6 +220,7 @@ def capture_event(event, **options, &block) end def add_breadcrumb(breadcrumb, hint: {}) + return unless current_client return unless configuration.enabled_in_current_env? if before_breadcrumb = current_client.configuration.before_breadcrumb @@ -246,7 +255,11 @@ def end_session return unless session session.close - Sentry.session_flusher.add_session(session) + + # NOTE: Under some circumstances, session_flusher nilified out of sync + # See: https://github.com/getsentry/sentry-ruby/issues/2378 + # See: https://github.com/getsentry/sentry-ruby/pull/2396 + Sentry.session_flusher&.add_session(session) end def with_session_tracking(&block) diff --git a/sentry-ruby/lib/sentry/interfaces/mechanism.rb b/sentry-ruby/lib/sentry/interfaces/mechanism.rb index 95179a77a..df672e7d9 100644 --- a/sentry-ruby/lib/sentry/interfaces/mechanism.rb +++ b/sentry-ruby/lib/sentry/interfaces/mechanism.rb @@ -12,7 +12,7 @@ class Mechanism < Interface # @return [Boolean] attr_accessor :handled - def initialize(type: 'generic', handled: true) + def initialize(type: "generic", handled: true) @type = type @handled = handled end diff --git a/sentry-ruby/lib/sentry/interfaces/request.rb b/sentry-ruby/lib/sentry/interfaces/request.rb index 519f7859a..4647e6562 100644 --- a/sentry-ruby/lib/sentry/interfaces/request.rb +++ b/sentry-ruby/lib/sentry/interfaces/request.rb @@ -59,7 +59,7 @@ def initialize(env:, send_default_pii:, rack_env_whitelist:) self.query_string = request.query_string end - self.url = request.scheme && request.url.split('?').first + self.url = request.scheme && request.url.split("?").first self.method = request.request_method self.headers = filter_and_format_headers(env, send_default_pii) @@ -85,14 +85,14 @@ def filter_and_format_headers(env, send_default_pii) env.each_with_object({}) do |(key, value), memo| begin key = key.to_s # rack env can contain symbols - next memo['X-Request-Id'] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key) + next memo["X-Request-Id"] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key) next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"]) next if is_skippable_header?(key) next if key == "HTTP_AUTHORIZATION" && !send_default_pii # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever key = key.sub(/^HTTP_/, "") - key = key.split('_').map(&:capitalize).join('-') + key = key.split("_").map(&:capitalize).join("-") memo[key] = Utils::EncodingHelper.encode_to_utf_8(value.to_s) rescue StandardError => e @@ -108,7 +108,7 @@ def filter_and_format_headers(env, send_default_pii) def is_skippable_header?(key) key.upcase != key || # lower-case envs aren't real http headers key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else - !(key.start_with?('HTTP_') || CONTENT_HEADERS.include?(key)) + !(key.start_with?("HTTP_") || CONTENT_HEADERS.include?(key)) end # In versions < 3, Rack adds in an incorrect HTTP_VERSION key, which causes downstream @@ -120,7 +120,7 @@ def is_server_protocol?(key, value, protocol_version) rack_version = Gem::Version.new(::Rack.release) return false if rack_version >= Gem::Version.new("3.0") - key == 'HTTP_VERSION' && value == protocol_version + key == "HTTP_VERSION" && value == protocol_version end def filter_and_format_env(env, rack_env_whitelist) diff --git a/sentry-ruby/lib/sentry/interfaces/single_exception.rb b/sentry-ruby/lib/sentry/interfaces/single_exception.rb index d35491206..db2d54e7b 100644 --- a/sentry-ruby/lib/sentry/interfaces/single_exception.rb +++ b/sentry-ruby/lib/sentry/interfaces/single_exception.rb @@ -7,8 +7,8 @@ class SingleExceptionInterface < Interface include CustomInspection SKIP_INSPECTION_ATTRIBUTES = [:@stacktrace] - PROBLEMATIC_LOCAL_VALUE_REPLACEMENT = "[ignored due to error]".freeze - OMISSION_MARK = "...".freeze + PROBLEMATIC_LOCAL_VALUE_REPLACEMENT = "[ignored due to error]" + OMISSION_MARK = "..." MAX_LOCAL_BYTES = 1024 attr_reader :type, :module, :thread_id, :stacktrace, :mechanism @@ -26,7 +26,7 @@ def initialize(exception:, mechanism:, stacktrace: nil) @value = Utils::EncodingHelper.encode_to_utf_8(exception_message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES)) - @module = exception.class.to_s.split('::')[0...-1].join('::') + @module = exception.class.to_s.split("::")[0...-1].join("::") @thread_id = Thread.current.object_id @stacktrace = stacktrace @mechanism = mechanism diff --git a/sentry-ruby/lib/sentry/interfaces/stacktrace.rb b/sentry-ruby/lib/sentry/interfaces/stacktrace.rb index eadf01188..5f4be3719 100644 --- a/sentry-ruby/lib/sentry/interfaces/stacktrace.rb +++ b/sentry-ruby/lib/sentry/interfaces/stacktrace.rb @@ -27,8 +27,9 @@ class Frame < Interface attr_accessor :abs_path, :context_line, :function, :in_app, :filename, :lineno, :module, :pre_context, :post_context, :vars - def initialize(project_root, line) + def initialize(project_root, line, strip_backtrace_load_path = true) @project_root = project_root + @strip_backtrace_load_path = strip_backtrace_load_path @abs_path = line.file @function = line.method if line.method @@ -44,6 +45,7 @@ def to_s def compute_filename return if abs_path.nil? + return abs_path unless @strip_backtrace_load_path prefix = if under_project_root? && in_app diff --git a/sentry-ruby/lib/sentry/interfaces/stacktrace_builder.rb b/sentry-ruby/lib/sentry/interfaces/stacktrace_builder.rb index 5edac90c5..d2d0758ec 100644 --- a/sentry-ruby/lib/sentry/interfaces/stacktrace_builder.rb +++ b/sentry-ruby/lib/sentry/interfaces/stacktrace_builder.rb @@ -17,22 +17,35 @@ class StacktraceBuilder # @return [Proc, nil] attr_reader :backtrace_cleanup_callback + # @return [Boolean] + attr_reader :strip_backtrace_load_path + # @param project_root [String] # @param app_dirs_pattern [Regexp, nil] # @param linecache [LineCache] # @param context_lines [Integer, nil] # @param backtrace_cleanup_callback [Proc, nil] + # @param strip_backtrace_load_path [Boolean] # @see Configuration#project_root # @see Configuration#app_dirs_pattern # @see Configuration#linecache # @see Configuration#context_lines # @see Configuration#backtrace_cleanup_callback - def initialize(project_root:, app_dirs_pattern:, linecache:, context_lines:, backtrace_cleanup_callback: nil) + # @see Configuration#strip_backtrace_load_path + def initialize( + project_root:, + app_dirs_pattern:, + linecache:, + context_lines:, + backtrace_cleanup_callback: nil, + strip_backtrace_load_path: true + ) @project_root = project_root @app_dirs_pattern = app_dirs_pattern @linecache = linecache @context_lines = context_lines @backtrace_cleanup_callback = backtrace_cleanup_callback + @strip_backtrace_load_path = strip_backtrace_load_path end # Generates a StacktraceInterface with the given backtrace. @@ -73,7 +86,7 @@ def metrics_code_location(unparsed_line) private def convert_parsed_line_into_frame(line) - frame = StacktraceInterface::Frame.new(project_root, line) + frame = StacktraceInterface::Frame.new(project_root, line, strip_backtrace_load_path) frame.set_context(linecache, context_lines) if context_lines frame end diff --git a/sentry-ruby/lib/sentry/logger.rb b/sentry-ruby/lib/sentry/logger.rb index 7f910d86e..b0e65f442 100644 --- a/sentry-ruby/lib/sentry/logger.rb +++ b/sentry-ruby/lib/sentry/logger.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'logger' +require "logger" module Sentry class Logger < ::Logger diff --git a/sentry-ruby/lib/sentry/metrics.rb b/sentry-ruby/lib/sentry/metrics.rb index 99bd9f7f1..bcab324f4 100644 --- a/sentry-ruby/lib/sentry/metrics.rb +++ b/sentry-ruby/lib/sentry/metrics.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'sentry/metrics/metric' -require 'sentry/metrics/counter_metric' -require 'sentry/metrics/distribution_metric' -require 'sentry/metrics/gauge_metric' -require 'sentry/metrics/set_metric' -require 'sentry/metrics/timing' -require 'sentry/metrics/aggregator' +require "sentry/metrics/metric" +require "sentry/metrics/counter_metric" +require "sentry/metrics/distribution_metric" +require "sentry/metrics/gauge_metric" +require "sentry/metrics/set_metric" +require "sentry/metrics/timing" +require "sentry/metrics/aggregator" module Sentry module Metrics @@ -14,32 +14,32 @@ module Metrics INFORMATION_UNITS = %w[bit byte kilobyte kibibyte megabyte mebibyte gigabyte gibibyte terabyte tebibyte petabyte pebibyte exabyte exbibyte] FRACTIONAL_UNITS = %w[ratio percent] - OP_NAME = 'metric.timing' - SPAN_ORIGIN = 'auto.metric.timing' + OP_NAME = "metric.timing" + SPAN_ORIGIN = "auto.metric.timing" class << self - def increment(key, value = 1.0, unit: 'none', tags: {}, timestamp: nil) + def increment(key, value = 1.0, unit: "none", tags: {}, timestamp: nil) Sentry.metrics_aggregator&.add(:c, key, value, unit: unit, tags: tags, timestamp: timestamp) end - def distribution(key, value, unit: 'none', tags: {}, timestamp: nil) + def distribution(key, value, unit: "none", tags: {}, timestamp: nil) Sentry.metrics_aggregator&.add(:d, key, value, unit: unit, tags: tags, timestamp: timestamp) end - def set(key, value, unit: 'none', tags: {}, timestamp: nil) + def set(key, value, unit: "none", tags: {}, timestamp: nil) Sentry.metrics_aggregator&.add(:s, key, value, unit: unit, tags: tags, timestamp: timestamp) end - def gauge(key, value, unit: 'none', tags: {}, timestamp: nil) + def gauge(key, value, unit: "none", tags: {}, timestamp: nil) Sentry.metrics_aggregator&.add(:g, key, value, unit: unit, tags: tags, timestamp: timestamp) end - def timing(key, unit: 'second', tags: {}, timestamp: nil, &block) + def timing(key, unit: "second", tags: {}, timestamp: nil, &block) return unless block_given? return yield unless DURATION_UNITS.include?(unit) result, value = Sentry.with_child_span(op: OP_NAME, description: key, origin: SPAN_ORIGIN) do |span| - tags.each { |k, v| span.set_tag(k, v.is_a?(Array) ? v.join(', ') : v.to_s) } if span + tags.each { |k, v| span.set_tag(k, v.is_a?(Array) ? v.join(", ") : v.to_s) } if span start = Timing.send(unit.to_sym) result = yield diff --git a/sentry-ruby/lib/sentry/metrics/aggregator.rb b/sentry-ruby/lib/sentry/metrics/aggregator.rb index 45ffbd687..12ad54f69 100644 --- a/sentry-ruby/lib/sentry/metrics/aggregator.rb +++ b/sentry-ruby/lib/sentry/metrics/aggregator.rb @@ -41,8 +41,8 @@ def initialize(configuration, client) @stacktrace_builder = configuration.stacktrace_builder @default_tags = {} - @default_tags['release'] = configuration.release if configuration.release - @default_tags['environment'] = configuration.environment if configuration.environment + @default_tags["release"] = configuration.release if configuration.release + @default_tags["environment"] = configuration.environment if configuration.environment @mutex = Mutex.new @@ -59,7 +59,7 @@ def initialize(configuration, client) def add(type, key, value, - unit: 'none', + unit: "none", tags: {}, timestamp: nil, stacklevel: nil) @@ -98,7 +98,7 @@ def flush(force: false) unless flushable_buckets.empty? payload = serialize_buckets(flushable_buckets) envelope.add_item( - { type: 'statsd', length: payload.bytesize }, + { type: "statsd", length: payload.bytesize }, payload ) end @@ -107,7 +107,7 @@ def flush(force: false) code_locations.each do |timestamp, locations| payload = serialize_locations(timestamp, locations) envelope.add_item( - { type: 'metric_meta', content_type: 'application/json' }, + { type: "metric_meta", content_type: "application/json" }, payload ) end @@ -161,8 +161,8 @@ def serialize_buckets(buckets) buckets.map do |timestamp, timestamp_buckets| timestamp_buckets.map do |metric_key, metric| type, key, unit, tags = metric_key - values = metric.serialize.join(':') - sanitized_tags = tags.map { |k, v| "#{sanitize_tag_key(k)}:#{sanitize_tag_value(v)}" }.join(',') + values = metric.serialize.join(":") + sanitized_tags = tags.map { |k, v| "#{sanitize_tag_key(k)}:#{sanitize_tag_value(v)}" }.join(",") "#{sanitize_key(key)}@#{sanitize_unit(unit)}:#{values}|#{type}|\##{sanitized_tags}|T#{timestamp}" end @@ -175,22 +175,22 @@ def serialize_locations(timestamp, locations) mri = "#{type}:#{sanitize_key(key)}@#{sanitize_unit(unit)}" # note this needs to be an array but it really doesn't serve a purpose right now - [mri, [location.merge(type: 'location')]] + [mri, [location.merge(type: "location")]] end.to_h { timestamp: timestamp, mapping: mapping } end def sanitize_key(key) - key.gsub(KEY_SANITIZATION_REGEX, '_') + key.gsub(KEY_SANITIZATION_REGEX, "_") end def sanitize_unit(unit) - unit.gsub(UNIT_SANITIZATION_REGEX, '') + unit.gsub(UNIT_SANITIZATION_REGEX, "") end def sanitize_tag_key(key) - key.gsub(TAG_KEY_SANITIZATION_REGEX, '') + key.gsub(TAG_KEY_SANITIZATION_REGEX, "") end def sanitize_tag_value(value) @@ -209,7 +209,7 @@ def get_updated_tags(tags) updated_tags = @default_tags.merge(tags) transaction_name = get_transaction_name - updated_tags['transaction'] = transaction_name if transaction_name + updated_tags["transaction"] = transaction_name if transaction_name updated_tags end diff --git a/sentry-ruby/lib/sentry/metrics/set_metric.rb b/sentry-ruby/lib/sentry/metrics/set_metric.rb index 02c5c26a6..b38af2743 100644 --- a/sentry-ruby/lib/sentry/metrics/set_metric.rb +++ b/sentry-ruby/lib/sentry/metrics/set_metric.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'set' -require 'zlib' +require "set" +require "zlib" module Sentry module Metrics diff --git a/sentry-ruby/lib/sentry/metrics/timing.rb b/sentry-ruby/lib/sentry/metrics/timing.rb index 510434583..6d4d9b66d 100644 --- a/sentry-ruby/lib/sentry/metrics/timing.rb +++ b/sentry-ruby/lib/sentry/metrics/timing.rb @@ -37,6 +37,14 @@ def day def week Sentry.utc_now.to_i / (3600.0 * 24.0 * 7.0) end + + def duration_start + Process.clock_gettime(Process::CLOCK_MONOTONIC) + end + + def duration_end(start) + Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + end end end end diff --git a/sentry-ruby/lib/sentry/net/http.rb b/sentry-ruby/lib/sentry/net/http.rb index 205c79c55..04820f3d5 100644 --- a/sentry-ruby/lib/sentry/net/http.rb +++ b/sentry-ruby/lib/sentry/net/http.rb @@ -2,11 +2,14 @@ require "net/http" require "resolv" +require "sentry/utils/http_tracing" module Sentry # @api private module Net module HTTP + include Utils::HttpTracing + OP_NAME = "http.client" SPAN_ORIGIN = "auto.http.net_http" BREADCRUMB_CATEGORY = "net.http" @@ -21,8 +24,7 @@ module HTTP # req['connection'] ||= 'close' # return request(req, body, &block) # <- request will be called for the second time from the first call # } - # end - # # ..... + # end # ..... # end # ``` # @@ -34,44 +36,26 @@ def request(req, body = nil, &block) Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |sentry_span| request_info = extract_request_info(req) - if propagate_trace?(request_info[:url], Sentry.configuration) + if propagate_trace?(request_info[:url]) set_propagation_headers(req) end - super.tap do |res| - record_sentry_breadcrumb(request_info, res) + res = super + response_status = res.code.to_i - if sentry_span - sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}") - sentry_span.set_data(Span::DataConventions::URL, request_info[:url]) - sentry_span.set_data(Span::DataConventions::HTTP_METHOD, request_info[:method]) - sentry_span.set_data(Span::DataConventions::HTTP_QUERY, request_info[:query]) if request_info[:query] - sentry_span.set_data(Span::DataConventions::HTTP_STATUS_CODE, res.code.to_i) - end + if record_sentry_breadcrumb? + record_sentry_breadcrumb(request_info, response_status) end - end - end - private + if sentry_span + set_span_info(sentry_span, request_info, response_status) + end - def set_propagation_headers(req) - Sentry.get_trace_propagation_headers&.each { |k, v| req[k] = v } + res + end end - def record_sentry_breadcrumb(request_info, res) - return unless Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger) - - crumb = Sentry::Breadcrumb.new( - level: :info, - category: BREADCRUMB_CATEGORY, - type: :info, - data: { - status: res.code.to_i, - **request_info - } - ) - Sentry.add_breadcrumb(crumb) - end + private def from_sentry_sdk? dsn = Sentry.configuration.dsn @@ -82,7 +66,7 @@ def extract_request_info(req) # IPv6 url could look like '::1/path', and that won't parse without # wrapping it in square brackets. hostname = address =~ Resolv::IPv6::Regex ? "[#{address}]" : address - uri = req.uri || URI.parse("#{use_ssl? ? 'https' : 'http'}://#{hostname}#{req.path}") + uri = req.uri || URI.parse(URI::DEFAULT_PARSER.escape("#{use_ssl? ? 'https' : 'http'}://#{hostname}#{req.path}")) url = "#{uri.scheme}://#{uri.host}#{uri.path}" rescue uri.to_s result = { method: req.method, url: url } @@ -94,12 +78,6 @@ def extract_request_info(req) result end - - def propagate_trace?(url, configuration) - url && - configuration.propagate_traces && - configuration.trace_propagation_targets.any? { |target| url.match?(target) } - end end end end diff --git a/sentry-ruby/lib/sentry/profiler.rb b/sentry-ruby/lib/sentry/profiler.rb index 949a9aecb..d81e46024 100644 --- a/sentry-ruby/lib/sentry/profiler.rb +++ b/sentry-ruby/lib/sentry/profiler.rb @@ -1,11 +1,14 @@ # frozen_string_literal: true -require 'securerandom' +require "securerandom" +require_relative "profiler/helpers" module Sentry class Profiler - VERSION = '1' - PLATFORM = 'ruby' + include Profiler::Helpers + + VERSION = "1" + PLATFORM = "ruby" # 101 Hz in microseconds DEFAULT_INTERVAL = 1e6 / 101 MICRO_TO_NANO_SECONDS = 1e3 @@ -14,14 +17,14 @@ class Profiler attr_reader :sampled, :started, :event_id def initialize(configuration) - @event_id = SecureRandom.uuid.delete('-') + @event_id = SecureRandom.uuid.delete("-") @started = false @sampled = nil @profiling_enabled = defined?(StackProf) && configuration.profiling_enabled? @profiles_sample_rate = configuration.profiles_sample_rate @project_root = configuration.project_root - @app_dirs_pattern = configuration.app_dirs_pattern || Backtrace::APP_DIRS_PATTERN + @app_dirs_pattern = configuration.app_dirs_pattern @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}") end @@ -33,7 +36,7 @@ def start raw: true, aggregate: false) - @started ? log('Started') : log('Not started since running elsewhere') + @started ? log("Started") : log("Not started since running elsewhere") end def stop @@ -41,7 +44,11 @@ def stop return unless @started StackProf.stop - log('Stopped') + log("Stopped") + end + + def active_thread_id + "0" end # Sets initial sampling decision of the profile. @@ -54,14 +61,14 @@ def set_initial_sample_decision(transaction_sampled) unless transaction_sampled @sampled = false - log('Discarding profile because transaction not sampled') + log("Discarding profile because transaction not sampled") return end case @profiles_sample_rate when 0.0 @sampled = false - log('Discarding profile because sample_rate is 0') + log("Discarding profile because sample_rate is 0") return when 1.0 @sampled = true @@ -70,7 +77,7 @@ def set_initial_sample_decision(transaction_sampled) @sampled = Random.rand < @profiles_sample_rate end - log('Discarding profile due to sampling decision') unless @sampled + log("Discarding profile due to sampling decision") unless @sampled end def to_hash @@ -90,13 +97,12 @@ def to_hash frame_map = {} - frames = results[:frames].to_enum.with_index.map do |frame, idx| - frame_id, frame_data = frame - + frames = results[:frames].map.with_index do |(frame_id, frame_data), idx| # need to map over stackprof frame ids to ours frame_map[frame_id] = idx file_path = frame_data[:file] + lineno = frame_data[:line] in_app = in_app?(file_path) filename = compute_filename(file_path, in_app) function, mod = split_module(frame_data[:name]) @@ -109,7 +115,7 @@ def to_hash } frame_hash[:module] = mod if mod - frame_hash[:lineno] = frame_data[:line] if frame_data[:line] && frame_data[:line] >= 0 + frame_hash[:lineno] = lineno if lineno && lineno >= 0 frame_hash end @@ -130,7 +136,7 @@ def to_hash num_seen << results[:raw][idx + len] idx += len + 1 - log('Unknown frame in stack') if stack.size != len + log("Unknown frame in stack") if stack.size != len end idx = 0 @@ -155,16 +161,16 @@ def to_hash # Till then, on multi-threaded servers like puma, we will get frames from other active threads when the one # we're profiling is idle/sleeping/waiting for IO etc. # https://bugs.ruby-lang.org/issues/10602 - thread_id: '0', + thread_id: "0", elapsed_since_start_ns: elapsed_since_start_ns.to_s } end end - log('Some samples thrown away') if samples.size != results[:samples] + log("Some samples thrown away") if samples.size != results[:samples] if samples.size <= MIN_SAMPLES_REQUIRED - log('Not enough samples, discarding profiler') + log("Not enough samples, discarding profiler") record_lost_event(:insufficient_data) return {} end @@ -189,45 +195,8 @@ def log(message) Sentry.logger.debug(LOGGER_PROGNAME) { "[Profiler] #{message}" } end - def in_app?(abs_path) - abs_path.match?(@in_app_pattern) - end - - # copied from stacktrace.rb since I don't want to touch existing code - # TODO-neel-profiler try to fetch this from stackprof once we patch - # the native extension - def compute_filename(abs_path, in_app) - return nil if abs_path.nil? - - under_project_root = @project_root && abs_path.start_with?(@project_root) - - prefix = - if under_project_root && in_app - @project_root - else - longest_load_path = $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size) - - if under_project_root - longest_load_path || @project_root - else - longest_load_path - end - end - - prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path - end - - def split_module(name) - # last module plus class/instance method - i = name.rindex('::') - function = i ? name[(i + 2)..-1] : name - mod = i ? name[0...i] : nil - - [function, mod] - end - def record_lost_event(reason) - Sentry.get_current_client&.transport&.record_lost_event(reason, 'profile') + Sentry.get_current_client&.transport&.record_lost_event(reason, "profile") end end end diff --git a/sentry-ruby/lib/sentry/profiler/helpers.rb b/sentry-ruby/lib/sentry/profiler/helpers.rb new file mode 100644 index 000000000..3c446fba0 --- /dev/null +++ b/sentry-ruby/lib/sentry/profiler/helpers.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "securerandom" + +module Sentry + class Profiler + module Helpers + def in_app?(abs_path) + abs_path.match?(@in_app_pattern) + end + + # copied from stacktrace.rb since I don't want to touch existing code + # TODO-neel-profiler try to fetch this from stackprof once we patch + # the native extension + def compute_filename(abs_path, in_app) + return nil if abs_path.nil? + + under_project_root = @project_root && abs_path.start_with?(@project_root) + + prefix = + if under_project_root && in_app + @project_root + else + longest_load_path = $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size) + + if under_project_root + longest_load_path || @project_root + else + longest_load_path + end + end + + prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path + end + + def split_module(name) + # last module plus class/instance method + i = name.rindex("::") + function = i ? name[(i + 2)..-1] : name + mod = i ? name[0...i] : nil + + [function, mod] + end + end + end +end diff --git a/sentry-ruby/lib/sentry/propagation_context.rb b/sentry-ruby/lib/sentry/propagation_context.rb index 12ce55540..db2f58b8f 100644 --- a/sentry-ruby/lib/sentry/propagation_context.rb +++ b/sentry-ruby/lib/sentry/propagation_context.rb @@ -108,7 +108,7 @@ def get_baggage end # Returns the Dynamic Sampling Context from the baggage. - # @return [String, nil] + # @return [Hash, nil] def get_dynamic_sampling_context get_baggage&.dynamic_sampling_context end diff --git a/sentry-ruby/lib/sentry/rack.rb b/sentry-ruby/lib/sentry/rack.rb index 03bff096c..ab565c6d1 100644 --- a/sentry-ruby/lib/sentry/rack.rb +++ b/sentry-ruby/lib/sentry/rack.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'rack' +require "rack" -require 'sentry/rack/capture_exceptions' +require "sentry/rack/capture_exceptions" diff --git a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb index 653188e10..40cdfb4f8 100644 --- a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb +++ b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb @@ -50,11 +50,11 @@ def call(env) private def collect_exception(env) - env['rack.exception'] || env['sinatra.error'] + env["rack.exception"] || env["sinatra.error"] end def transaction_op - "http.server".freeze + "http.server" end def capture_exception(exception, env) diff --git a/sentry-ruby/lib/sentry/rake.rb b/sentry-ruby/lib/sentry/rake.rb index 0e3e6e8c7..243113023 100644 --- a/sentry-ruby/lib/sentry/rake.rb +++ b/sentry-ruby/lib/sentry/rake.rb @@ -8,10 +8,10 @@ module Rake module Application # @api private def display_error_message(ex) - mechanism = Sentry::Mechanism.new(type: 'rake', handled: false) + mechanism = Sentry::Mechanism.new(type: "rake", handled: false) Sentry.capture_exception(ex, hint: { mechanism: mechanism }) do |scope| - task_name = top_level_tasks.join(' ') + task_name = top_level_tasks.join(" ") scope.set_transaction_name(task_name, source: :task) scope.set_tag("rake_task", task_name) end if Sentry.initialized? && !Sentry.configuration.skip_rake_integration diff --git a/sentry-ruby/lib/sentry/release_detector.rb b/sentry-ruby/lib/sentry/release_detector.rb index a975520eb..c71b216a5 100644 --- a/sentry-ruby/lib/sentry/release_detector.rb +++ b/sentry-ruby/lib/sentry/release_detector.rb @@ -13,12 +13,12 @@ def detect_release(project_root:, running_on_heroku:) def detect_release_from_heroku(running_on_heroku) return unless running_on_heroku - ENV['HEROKU_SLUG_COMMIT'] + ENV["HEROKU_SLUG_COMMIT"] end def detect_release_from_capistrano(project_root) - revision_file = File.join(project_root, 'REVISION') - revision_log = File.join(project_root, '..', 'revisions.log') + revision_file = File.join(project_root, "REVISION") + revision_log = File.join(project_root, "..", "revisions.log") if File.exist?(revision_file) File.read(revision_file).strip @@ -32,7 +32,7 @@ def detect_release_from_git end def detect_release_from_env - ENV['SENTRY_RELEASE'] + ENV["SENTRY_RELEASE"] end end end diff --git a/sentry-ruby/lib/sentry/rspec.rb b/sentry-ruby/lib/sentry/rspec.rb new file mode 100644 index 000000000..9c7c49730 --- /dev/null +++ b/sentry-ruby/lib/sentry/rspec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +RSpec::Matchers.define :include_sentry_event do |event_message = "", **opts| + match do |sentry_events| + @expected_exception = expected_exception(**opts) + @context = context(**opts) + @tags = tags(**opts) + + @expected_event = expected_event(event_message) + @matched_event = find_matched_event(event_message, sentry_events) + + return false unless @matched_event + + [verify_context(), verify_tags()].all? + end + + chain :with_context do |context| + @context = context + end + + chain :with_tags do |tags| + @tags = tags + end + + failure_message do |sentry_events| + info = ["Failed to find event matching:\n"] + info << " message: #{@expected_event.message.inspect}" + info << " exception: #{@expected_exception.inspect}" + info << " context: #{@context.inspect}" + info << " tags: #{@tags.inspect}" + info << "\n" + info << "Captured events:\n" + info << dump_events(sentry_events) + info.join("\n") + end + + def expected_event(event_message) + if @expected_exception + Sentry.get_current_client.event_from_exception(@expected_exception) + else + Sentry.get_current_client.event_from_message(event_message) + end + end + + def expected_exception(**opts) + opts[:exception].new(opts[:message]) if opts[:exception] + end + + def context(**opts) + opts.fetch(:context, @context || {}) + end + + def tags(**opts) + opts.fetch(:tags, @tags || {}) + end + + def find_matched_event(event_message, sentry_events) + @matched_event ||= sentry_events + .find { |event| + if @expected_exception + # Is it OK that we only compare the first exception? + event_exception = event.exception.values.first + expected_event_exception = @expected_event.exception.values.first + + event_exception.type == expected_event_exception.type && event_exception.value == expected_event_exception.value + else + event.message == @expected_event.message + end + } + end + + def dump_events(sentry_events) + sentry_events.map(&Kernel.method(:Hash)).map do |hash| + hash.select { |k, _| [:message, :contexts, :tags, :exception].include?(k) } + end.map do |hash| + JSON.pretty_generate(hash) + end.join("\n\n") + end + + def verify_context + return true if @context.empty? + + @matched_event.contexts.any? { |key, value| value == @context[key] } + end + + def verify_tags + return true if @tags.empty? + + @tags.all? { |key, value| @matched_event.tags.include?(key) && @matched_event.tags[key] == value } + end +end diff --git a/sentry-ruby/lib/sentry/scope.rb b/sentry-ruby/lib/sentry/scope.rb index 591a00682..4feb6ecdd 100644 --- a/sentry-ruby/lib/sentry/scope.rb +++ b/sentry-ruby/lib/sentry/scope.rb @@ -2,6 +2,7 @@ require "sentry/breadcrumb_buffer" require "sentry/propagation_context" +require "sentry/attachment" require "etc" module Sentry @@ -22,6 +23,7 @@ class Scope :rack_env, :span, :session, + :attachments, :propagation_context ] @@ -55,10 +57,12 @@ def apply_to_event(event, hint = nil) event.level = level event.breadcrumbs = breadcrumbs event.rack_env = rack_env if rack_env + event.attachments = attachments end if span event.contexts[:trace] ||= span.get_trace_context + event.dynamic_sampling_context ||= span.get_dynamic_sampling_context else event.contexts[:trace] ||= propagation_context.get_trace_context event.dynamic_sampling_context ||= propagation_context.get_dynamic_sampling_context @@ -102,6 +106,7 @@ def dup copy.span = span.deep_dup copy.session = session.deep_dup copy.propagation_context = propagation_context.deep_dup + copy.attachments = attachments.dup copy end @@ -119,6 +124,7 @@ def update_from_scope(scope) self.fingerprint = scope.fingerprint self.span = scope.span self.propagation_context = scope.propagation_context + self.attachments = scope.attachments end # Updates the scope's data from the given options. @@ -128,6 +134,7 @@ def update_from_scope(scope) # @param user [Hash] # @param level [String, Symbol] # @param fingerprint [Array] + # @param attachments [Array] # @return [Array] def update_from_options( contexts: nil, @@ -136,6 +143,7 @@ def update_from_options( user: nil, level: nil, fingerprint: nil, + attachments: nil, **options ) self.contexts.merge!(contexts) if contexts @@ -283,6 +291,12 @@ def generate_propagation_context(env = nil) @propagation_context = PropagationContext.new(self, env) end + # Add a new attachment to the scope. + def add_attachment(**opts) + attachments << (attachment = Attachment.new(**opts)) + attachment + end + protected # for duplicating scopes internally @@ -303,6 +317,7 @@ def set_default_value @rack_env = {} @span = nil @session = nil + @attachments = [] generate_propagation_context set_new_breadcrumb_buffer end diff --git a/sentry-ruby/lib/sentry/session_flusher.rb b/sentry-ruby/lib/sentry/session_flusher.rb index 5971cfc59..8f0510832 100644 --- a/sentry-ruby/lib/sentry/session_flusher.rb +++ b/sentry-ruby/lib/sentry/session_flusher.rb @@ -10,6 +10,7 @@ def initialize(configuration, client) @pending_aggregates = {} @release = configuration.release @environment = configuration.environment + @mutex = Mutex.new log_debug("[Sessions] Sessions won't be captured without a valid release") unless @release end @@ -18,7 +19,6 @@ def flush return if @pending_aggregates.empty? @client.capture_envelope(pending_envelope) - @pending_aggregates = {} end alias_method :run, :flush @@ -42,11 +42,15 @@ def init_aggregates(aggregation_key) end def pending_envelope - envelope = Envelope.new - - header = { type: 'sessions' } - payload = { attrs: attrs, aggregates: @pending_aggregates.values } + aggregates = @mutex.synchronize do + aggregates = @pending_aggregates.values + @pending_aggregates = {} + aggregates + end + envelope = Envelope.new + header = { type: "sessions" } + payload = { attrs: attrs, aggregates: aggregates } envelope.add_item(header, payload) envelope end diff --git a/sentry-ruby/lib/sentry/span.rb b/sentry-ruby/lib/sentry/span.rb index 061b7b1e0..256d8bd2f 100644 --- a/sentry-ruby/lib/sentry/span.rb +++ b/sentry-ruby/lib/sentry/span.rb @@ -44,6 +44,11 @@ module DataConventions LINENO = "code.lineno" FUNCTION = "code.function" NAMESPACE = "code.namespace" + + MESSAGING_MESSAGE_ID = "messaging.message.id" + MESSAGING_DESTINATION_NAME = "messaging.destination.name" + MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency" + MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count" end STATUS_MAP = { @@ -160,6 +165,12 @@ def to_baggage transaction.get_baggage&.serialize end + # Returns the Dynamic Sampling Context from the transaction baggage. + # @return [Hash, nil] + def get_dynamic_sampling_context + transaction.get_baggage&.dynamic_sampling_context + end + # @return [Hash] def to_hash hash = { @@ -192,7 +203,8 @@ def get_trace_context description: @description, op: @op, status: @status, - origin: @origin + origin: @origin, + data: @data } end diff --git a/sentry-ruby/lib/sentry/test_helper.rb b/sentry-ruby/lib/sentry/test_helper.rb index 4d4987e92..d36dc0be0 100644 --- a/sentry-ruby/lib/sentry/test_helper.rb +++ b/sentry-ruby/lib/sentry/test_helper.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module Sentry module TestHelper - DUMMY_DSN = 'http://12345:67890@sentry.localdomain/sentry/42' + DUMMY_DSN = "http://12345:67890@sentry.localdomain/sentry/42" # Alters the existing SDK configuration with test-suitable options. Mainly: # - Sets a dummy DSN instead of `nil` or an actual DSN. @@ -50,6 +52,7 @@ def teardown_sentry_test if Sentry.get_current_hub.instance_variable_get(:@stack).size > 1 Sentry.get_current_hub.pop_scope end + Sentry::Scope.global_event_processors.clear end # @return [Transport] diff --git a/sentry-ruby/lib/sentry/transaction.rb b/sentry-ruby/lib/sentry/transaction.rb index 456b49e74..5e3ceb982 100644 --- a/sentry-ruby/lib/sentry/transaction.rb +++ b/sentry-ruby/lib/sentry/transaction.rb @@ -9,7 +9,7 @@ class Transaction < Span # @deprecated Use Sentry::PropagationContext::SENTRY_TRACE_REGEXP instead. SENTRY_TRACE_REGEXP = PropagationContext::SENTRY_TRACE_REGEXP - UNLABELD_NAME = "".freeze + UNLABELD_NAME = "" MESSAGE_PREFIX = "[Tracing]" # https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations @@ -85,7 +85,7 @@ def initialize( @effective_sample_rate = nil @contexts = {} @measurements = {} - @profiler = Profiler.new(@configuration) + @profiler = @configuration.profiler_class.new(@configuration) init_span_recorder end @@ -265,7 +265,8 @@ def finish(hub: nil, end_timestamp: nil) else is_backpressure = Sentry.backpressure_monitor&.downsample_factor&.positive? reason = is_backpressure ? :backpressure : :sample_rate - hub.current_client.transport.record_lost_event(reason, 'transaction') + hub.current_client.transport.record_lost_event(reason, "transaction") + hub.current_client.transport.record_lost_event(reason, "span") end end diff --git a/sentry-ruby/lib/sentry/transaction_event.rb b/sentry-ruby/lib/sentry/transaction_event.rb index d2e39dc1f..a1a8767d1 100644 --- a/sentry-ruby/lib/sentry/transaction_event.rb +++ b/sentry-ruby/lib/sentry/transaction_event.rb @@ -74,8 +74,7 @@ def populate_profile(transaction) id: event_id, name: transaction.name, trace_id: transaction.trace_id, - # TODO-neel-profiler stubbed for now, see thread_id note in profiler.rb - active_thead_id: '0' + active_thread_id: transaction.profiler.active_thread_id.to_s } ) diff --git a/sentry-ruby/lib/sentry/transport.rb b/sentry-ruby/lib/sentry/transport.rb index 564734791..d446387f2 100644 --- a/sentry-ruby/lib/sentry/transport.rb +++ b/sentry-ruby/lib/sentry/transport.rb @@ -5,7 +5,7 @@ module Sentry class Transport - PROTOCOL_VERSION = '7' + PROTOCOL_VERSION = "7" USER_AGENT = "sentry-ruby/#{Sentry::VERSION}" CLIENT_REPORT_INTERVAL = 30 @@ -134,28 +134,34 @@ def envelope_from_event(event) envelope = Envelope.new(envelope_headers) envelope.add_item( - { type: item_type, content_type: 'application/json' }, + { type: item_type, content_type: "application/json" }, event_payload ) if event.is_a?(TransactionEvent) && event.profile envelope.add_item( - { type: 'profile', content_type: 'application/json' }, + { type: "profile", content_type: "application/json" }, event.profile ) end + if event.is_a?(Event) && event.attachments.any? + event.attachments.each do |attachment| + envelope.add_item(attachment.to_envelope_headers, attachment.payload) + end + end + client_report_headers, client_report_payload = fetch_pending_client_report envelope.add_item(client_report_headers, client_report_payload) if client_report_headers envelope end - def record_lost_event(reason, data_category) + def record_lost_event(reason, data_category, num: 1) return unless @send_client_reports return unless CLIENT_REPORT_REASONS.include?(reason) - @discarded_events[[reason, data_category]] += 1 + @discarded_events[[reason, data_category]] += num end def flush @@ -179,7 +185,7 @@ def fetch_pending_client_report(force: false) { reason: reason, category: category, quantity: val } end - item_header = { type: 'client_report' } + item_header = { type: "client_report" } item_payload = { timestamp: Sentry.utc_now.iso8601, discarded_events: discarded_events_hash diff --git a/sentry-ruby/lib/sentry/transport/http_transport.rb b/sentry-ruby/lib/sentry/transport/http_transport.rb index 4a9ee620f..e79bd0634 100644 --- a/sentry-ruby/lib/sentry/transport/http_transport.rb +++ b/sentry-ruby/lib/sentry/transport/http_transport.rb @@ -7,7 +7,7 @@ module Sentry class HTTPTransport < Transport GZIP_ENCODING = "gzip" GZIP_THRESHOLD = 1024 * 30 - CONTENT_TYPE = 'application/x-sentry-envelope' + CONTENT_TYPE = "application/x-sentry-envelope" DEFAULT_DELAY = 60 RETRY_AFTER_HEADER = "retry-after" @@ -38,13 +38,13 @@ def send_data(data) end headers = { - 'Content-Type' => CONTENT_TYPE, - 'Content-Encoding' => encoding, - 'User-Agent' => USER_AGENT + "Content-Type" => CONTENT_TYPE, + "Content-Encoding" => encoding, + "User-Agent" => USER_AGENT } auth_header = generate_auth_header - headers['X-Sentry-Auth'] = auth_header if auth_header + headers["X-Sentry-Auth"] = auth_header if auth_header response = conn.start do |http| request = ::Net::HTTP::Post.new(endpoint, headers) @@ -60,7 +60,7 @@ def send_data(data) else error_info = "the server responded with status #{response.code}" error_info += "\nbody: #{response.body}" - error_info += " Error in headers is: #{response['x-sentry-error']}" if response['x-sentry-error'] + error_info += " Error in headers is: #{response['x-sentry-error']}" if response["x-sentry-error"] raise Sentry::ExternalError, error_info end @@ -78,13 +78,13 @@ def generate_auth_header now = Sentry.utc_now.to_i fields = { - 'sentry_version' => PROTOCOL_VERSION, - 'sentry_client' => USER_AGENT, - 'sentry_timestamp' => now, - 'sentry_key' => @dsn.public_key + "sentry_version" => PROTOCOL_VERSION, + "sentry_client" => USER_AGENT, + "sentry_timestamp" => now, + "sentry_key" => @dsn.public_key } - fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key - 'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ') + fields["sentry_secret"] = @dsn.secret_key if @dsn.secret_key + "Sentry " + fields.map { |key, value| "#{key}=#{value}" }.join(", ") end def conn diff --git a/sentry-ruby/lib/sentry/utils/env_helper.rb b/sentry-ruby/lib/sentry/utils/env_helper.rb new file mode 100644 index 000000000..a1a143d61 --- /dev/null +++ b/sentry-ruby/lib/sentry/utils/env_helper.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Sentry + module Utils + module EnvHelper + TRUTHY_ENV_VALUES = %w[t true yes y 1 on].freeze + FALSY_ENV_VALUES = %w[f false no n 0 off].freeze + + def self.env_to_bool(value, strict: false) + value = value.to_s + normalized = value.downcase + + return false if FALSY_ENV_VALUES.include?(normalized) + + return true if TRUTHY_ENV_VALUES.include?(normalized) + + strict ? nil : !(value.nil? || value.empty?) + end + end + end +end diff --git a/sentry-ruby/lib/sentry/utils/http_tracing.rb b/sentry-ruby/lib/sentry/utils/http_tracing.rb new file mode 100644 index 000000000..136e78d99 --- /dev/null +++ b/sentry-ruby/lib/sentry/utils/http_tracing.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Sentry + module Utils + module HttpTracing + def set_span_info(sentry_span, request_info, response_status) + sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}") + sentry_span.set_data(Span::DataConventions::URL, request_info[:url]) + sentry_span.set_data(Span::DataConventions::HTTP_METHOD, request_info[:method]) + sentry_span.set_data(Span::DataConventions::HTTP_QUERY, request_info[:query]) if request_info[:query] + sentry_span.set_data(Span::DataConventions::HTTP_STATUS_CODE, response_status) + end + + def set_propagation_headers(req) + Sentry.get_trace_propagation_headers&.each { |k, v| req[k] = v } + end + + def record_sentry_breadcrumb(request_info, response_status) + crumb = Sentry::Breadcrumb.new( + level: :info, + category: self.class::BREADCRUMB_CATEGORY, + type: "info", + data: { status: response_status, **request_info } + ) + + Sentry.add_breadcrumb(crumb) + end + + def record_sentry_breadcrumb? + Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger) + end + + def propagate_trace?(url) + url && + Sentry.initialized? && + Sentry.configuration.propagate_traces && + Sentry.configuration.trace_propagation_targets.any? { |target| url.match?(target) } + end + + # Kindly borrowed from Rack::Utils + def build_nested_query(value, prefix = nil) + case value + when Array + value.map { |v| + build_nested_query(v, "#{prefix}[]") + }.join("&") + when Hash + value.map { |k, v| + build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k) + }.delete_if(&:empty?).join("&") + when nil + URI.encode_www_form_component(prefix) + else + raise ArgumentError, "value must be a Hash" if prefix.nil? + "#{URI.encode_www_form_component(prefix)}=#{URI.encode_www_form_component(value)}" + end + end + end + end +end diff --git a/sentry-ruby/lib/sentry/utils/real_ip.rb b/sentry-ruby/lib/sentry/utils/real_ip.rb index d470051d7..08085bf19 100644 --- a/sentry-ruby/lib/sentry/utils/real_ip.rb +++ b/sentry-ruby/lib/sentry/utils/real_ip.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'ipaddr' +require "ipaddr" # Based on ActionDispatch::RemoteIp. All security-related precautions from that # middleware have been removed, because the Event IP just needs to be accurate, diff --git a/sentry-ruby/lib/sentry/vernier/output.rb b/sentry-ruby/lib/sentry/vernier/output.rb new file mode 100644 index 000000000..7002f82a1 --- /dev/null +++ b/sentry-ruby/lib/sentry/vernier/output.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require "json" +require "rbconfig" + +module Sentry + module Vernier + class Output + include Profiler::Helpers + + attr_reader :profile + + def initialize(profile, project_root:, in_app_pattern:, app_dirs_pattern:) + @profile = profile + @project_root = project_root + @in_app_pattern = in_app_pattern + @app_dirs_pattern = app_dirs_pattern + end + + def to_h + @to_h ||= { + frames: frames, + stacks: stacks, + samples: samples, + thread_metadata: thread_metadata + } + end + + private + + def thread_metadata + profile.threads.map { |thread_id, thread_info| + [thread_id, { name: thread_info[:name] }] + }.to_h + end + + def samples + profile.threads.flat_map { |thread_id, thread_info| + started_at = thread_info[:started_at] + samples, timestamps = thread_info.values_at(:samples, :timestamps) + + samples.zip(timestamps).map { |stack_id, timestamp| + elapsed_since_start_ns = timestamp - started_at + + next if elapsed_since_start_ns < 0 + + { + thread_id: thread_id.to_s, + stack_id: stack_id, + elapsed_since_start_ns: elapsed_since_start_ns.to_s + } + }.compact + } + end + + def frames + funcs = stack_table_hash[:frame_table].fetch(:func) + lines = stack_table_hash[:func_table].fetch(:first_line) + + funcs.map do |idx| + function, mod = split_module(stack_table_hash[:func_table][:name][idx]) + + abs_path = stack_table_hash[:func_table][:filename][idx] + in_app = in_app?(abs_path) + filename = compute_filename(abs_path, in_app) + + { + function: function, + module: mod, + filename: filename, + abs_path: abs_path, + lineno: (lineno = lines[idx]) > 0 ? lineno : nil, + in_app: in_app + }.compact + end + end + + def stacks + profile._stack_table.stack_count.times.map do |stack_id| + profile.stack(stack_id).frames.map(&:idx) + end + end + + def stack_table_hash + @stack_table_hash ||= profile._stack_table.to_h + end + end + end +end diff --git a/sentry-ruby/lib/sentry/vernier/profiler.rb b/sentry-ruby/lib/sentry/vernier/profiler.rb new file mode 100644 index 000000000..6ee6b6ced --- /dev/null +++ b/sentry-ruby/lib/sentry/vernier/profiler.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +require "securerandom" +require_relative "../profiler/helpers" +require_relative "output" + +module Sentry + module Vernier + class Profiler + EMPTY_RESULT = {}.freeze + + attr_reader :started, :event_id, :result + + def initialize(configuration) + @event_id = SecureRandom.uuid.delete("-") + + @started = false + @sampled = nil + + @profiling_enabled = defined?(Vernier) && configuration.profiling_enabled? + @profiles_sample_rate = configuration.profiles_sample_rate + @project_root = configuration.project_root + @app_dirs_pattern = configuration.app_dirs_pattern + @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}") + end + + def set_initial_sample_decision(transaction_sampled) + unless @profiling_enabled + @sampled = false + return + end + + unless transaction_sampled + @sampled = false + log("Discarding profile because transaction not sampled") + return + end + + case @profiles_sample_rate + when 0.0 + @sampled = false + log("Discarding profile because sample_rate is 0") + return + when 1.0 + @sampled = true + return + else + @sampled = Random.rand < @profiles_sample_rate + end + + log("Discarding profile due to sampling decision") unless @sampled + end + + def start + return unless @sampled + return if @started + + @started = ::Vernier.start_profile + + log("Started") + + @started + rescue RuntimeError => e + # TODO: once Vernier raises something more dedicated, we should catch that instead + if e.message.include?("Profile already started") + log("Not started since running elsewhere") + else + log("Failed to start: #{e.message}") + end + end + + def stop + return unless @sampled + return unless @started + + @result = ::Vernier.stop_profile + + log("Stopped") + rescue RuntimeError => e + if e.message.include?("Profile not started") + log("Not stopped since not started") + else + log("Failed to stop Vernier: #{e.message}") + end + end + + def active_thread_id + Thread.current.object_id + end + + def to_hash + return EMPTY_RESULT unless @started + + unless @sampled + record_lost_event(:sample_rate) + return EMPTY_RESULT + end + + { **profile_meta, profile: output.to_h } + end + + private + + def log(message) + Sentry.logger.debug(LOGGER_PROGNAME) { "[Profiler::Vernier] #{message}" } + end + + def record_lost_event(reason) + Sentry.get_current_client&.transport&.record_lost_event(reason, "profile") + end + + def profile_meta + { + event_id: @event_id, + version: "1", + platform: "ruby" + } + end + + def output + @output ||= Output.new( + result, + project_root: @project_root, + app_dirs_pattern: @app_dirs_pattern, + in_app_pattern: @in_app_pattern + ) + end + end + end +end diff --git a/sentry-ruby/lib/sentry/version.rb b/sentry-ruby/lib/sentry/version.rb index ebf58e46d..f7bb5503c 100644 --- a/sentry-ruby/lib/sentry/version.rb +++ b/sentry-ruby/lib/sentry/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Sentry - VERSION = "5.18.0" + VERSION = "5.22.1" end diff --git a/sentry-ruby/sentry-ruby-core.gemspec b/sentry-ruby/sentry-ruby-core.gemspec index 76a343c28..4bd423750 100644 --- a/sentry-ruby/sentry-ruby-core.gemspec +++ b/sentry-ruby/sentry-ruby-core.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "lib/sentry/version" Gem::Specification.new do |spec| @@ -12,7 +14,7 @@ Gem::Specification.new do |spec| spec.platform = Gem::Platform::RUBY spec.required_ruby_version = '>= 2.4' spec.extra_rdoc_files = ["README.md", "LICENSE.txt"] - spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples)'`.split("\n") + spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples|\.rubocop\.yml)'`.split("\n") spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage diff --git a/sentry-ruby/sentry-ruby.gemspec b/sentry-ruby/sentry-ruby.gemspec index 77e9711ec..ba183705c 100644 --- a/sentry-ruby/sentry-ruby.gemspec +++ b/sentry-ruby/sentry-ruby.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "lib/sentry/version" Gem::Specification.new do |spec| @@ -7,19 +9,25 @@ Gem::Specification.new do |spec| spec.description = spec.summary = "A gem that provides a client interface for the Sentry error logger" spec.email = "accounts@sentry.io" spec.license = 'MIT' - spec.homepage = "https://github.com/getsentry/sentry-ruby" spec.platform = Gem::Platform::RUBY spec.required_ruby_version = '>= 2.4' spec.extra_rdoc_files = ["README.md", "LICENSE.txt"] - spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples)'`.split("\n") + spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples|\.rubocop\.yml)'`.split("\n") + + github_root_uri = 'https://github.com/getsentry/sentry-ruby' + spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}" - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = spec.homepage - spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" + spec.metadata = { + "homepage_uri" => spec.homepage, + "source_code_uri" => spec.homepage, + "changelog_uri" => "#{github_root_uri}/blob/#{spec.version}/CHANGELOG.md", + "bug_tracker_uri" => "#{github_root_uri}/issues", + "documentation_uri" => "http://www.rubydoc.info/gems/#{spec.name}/#{spec.version}" + } spec.require_paths = ["lib"] - spec.add_dependency "concurrent-ruby", '~> 1.0', '>= 1.0.2' + spec.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2" spec.add_dependency "bigdecimal" end diff --git a/sentry-ruby/spec/contexts/with_request_mock.rb b/sentry-ruby/spec/contexts/with_request_mock.rb index bc103f7dc..e6c480256 100644 --- a/sentry-ruby/spec/contexts/with_request_mock.rb +++ b/sentry-ruby/spec/contexts/with_request_mock.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # because our patch on Net::HTTP is relatively low-level, we need to stub methods on socket level # which is not supported by most of the http mocking library # so we need to put something together ourselves diff --git a/sentry-ruby/spec/fixtures/attachment.txt b/sentry-ruby/spec/fixtures/attachment.txt new file mode 100644 index 000000000..3b18e512d --- /dev/null +++ b/sentry-ruby/spec/fixtures/attachment.txt @@ -0,0 +1 @@ +hello world diff --git a/sentry-ruby/spec/initialization_check_spec.rb b/sentry-ruby/spec/initialization_check_spec.rb index 039384c1d..129d5fdff 100644 --- a/sentry-ruby/spec/initialization_check_spec.rb +++ b/sentry-ruby/spec/initialization_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe "with uninitialized SDK" do diff --git a/sentry-ruby/spec/isolated/puma_spec.rb b/sentry-ruby/spec/isolated/puma_spec.rb index 0e2082519..6f416d6e9 100644 --- a/sentry-ruby/spec/isolated/puma_spec.rb +++ b/sentry-ruby/spec/isolated/puma_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "puma" require_relative "../spec_helper" diff --git a/sentry-ruby/spec/sentry/background_worker_spec.rb b/sentry-ruby/spec/sentry/background_worker_spec.rb index e1b900094..9e15b43ec 100644 --- a/sentry-ruby/spec/sentry/background_worker_spec.rb +++ b/sentry-ruby/spec/sentry/background_worker_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::BackgroundWorker do diff --git a/sentry-ruby/spec/sentry/backpressure_monitor_spec.rb b/sentry-ruby/spec/sentry/backpressure_monitor_spec.rb index 1419cffdb..e0147d87c 100644 --- a/sentry-ruby/spec/sentry/backpressure_monitor_spec.rb +++ b/sentry-ruby/spec/sentry/backpressure_monitor_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::BackpressureMonitor do diff --git a/sentry-ruby/spec/sentry/backtrace/lines_spec.rb b/sentry-ruby/spec/sentry/backtrace/lines_spec.rb index cdcb9809d..8a49ea1d8 100644 --- a/sentry-ruby/spec/sentry/backtrace/lines_spec.rb +++ b/sentry-ruby/spec/sentry/backtrace/lines_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Backtrace::Line do @@ -13,7 +15,7 @@ let(:in_app_pattern) do project_root = Sentry.configuration.project_root&.to_s - Regexp.new("^(#{project_root}/)?#{Sentry::Backtrace::APP_DIRS_PATTERN}") + Regexp.new("^(#{project_root}/)?#{Sentry::Configuration::APP_DIRS_PATTERN}") end describe ".parse" do diff --git a/sentry-ruby/spec/sentry/backtrace_spec.rb b/sentry-ruby/spec/sentry/backtrace_spec.rb index b373c45f4..648e4ee8e 100644 --- a/sentry-ruby/spec/sentry/backtrace_spec.rb +++ b/sentry-ruby/spec/sentry/backtrace_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Backtrace do diff --git a/sentry-ruby/spec/sentry/baggage_spec.rb b/sentry-ruby/spec/sentry/baggage_spec.rb index 3d72a5687..a9cc44873 100644 --- a/sentry-ruby/spec/sentry/baggage_spec.rb +++ b/sentry-ruby/spec/sentry/baggage_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Baggage do diff --git a/sentry-ruby/spec/sentry/breadcrumb/http_logger_spec.rb b/sentry-ruby/spec/sentry/breadcrumb/http_logger_spec.rb index 66146ef64..a4fab84ed 100644 --- a/sentry-ruby/spec/sentry/breadcrumb/http_logger_spec.rb +++ b/sentry-ruby/spec/sentry/breadcrumb/http_logger_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require 'contexts/with_request_mock' diff --git a/sentry-ruby/spec/sentry/breadcrumb/redis_logger_spec.rb b/sentry-ruby/spec/sentry/breadcrumb/redis_logger_spec.rb index 4d0d541df..4285a43f4 100644 --- a/sentry-ruby/spec/sentry/breadcrumb/redis_logger_spec.rb +++ b/sentry-ruby/spec/sentry/breadcrumb/redis_logger_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe :redis_logger do diff --git a/sentry-ruby/spec/sentry/breadcrumb/sentry_logger_spec.rb b/sentry-ruby/spec/sentry/breadcrumb/sentry_logger_spec.rb index baf180922..da3cd97c7 100644 --- a/sentry-ruby/spec/sentry/breadcrumb/sentry_logger_spec.rb +++ b/sentry-ruby/spec/sentry/breadcrumb/sentry_logger_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe "Sentry::Breadcrumbs::SentryLogger" do @@ -69,33 +71,35 @@ end # see https://github.com/getsentry/sentry-ruby/issues/1858 - it "noops on thread with cloned hub" do - mutex = Mutex.new - cv = ConditionVariable.new - - a = Thread.new do - expect(Sentry.get_current_hub).to be_a(Sentry::Hub) + unless RUBY_PLATFORM == "java" + it "noops on thread with cloned hub" do + mutex = Mutex.new + cv = ConditionVariable.new + + a = Thread.new do + expect(Sentry.get_current_hub).to be_a(Sentry::Hub) + + # close in another thread + b = Thread.new do + mutex.synchronize do + Sentry.close + cv.signal + end + end - # close in another thread - b = Thread.new do mutex.synchronize do - Sentry.close - cv.signal - end - end + # wait for other thread to close SDK + cv.wait(mutex) - mutex.synchronize do - # wait for other thread to close SDK - cv.wait(mutex) + expect(Sentry).not_to receive(:add_breadcrumb) + logger.info("foo") + end - expect(Sentry).not_to receive(:add_breadcrumb) - logger.info("foo") + b.join end - b.join + a.join end - - a.join end end end diff --git a/sentry-ruby/spec/sentry/breadcrumb_buffer_spec.rb b/sentry-ruby/spec/sentry/breadcrumb_buffer_spec.rb index 04a76fe3c..f68e223b3 100644 --- a/sentry-ruby/spec/sentry/breadcrumb_buffer_spec.rb +++ b/sentry-ruby/spec/sentry/breadcrumb_buffer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::BreadcrumbBuffer do diff --git a/sentry-ruby/spec/sentry/breadcrumb_spec.rb b/sentry-ruby/spec/sentry/breadcrumb_spec.rb index 66c2d1112..2eddb7101 100644 --- a/sentry-ruby/spec/sentry/breadcrumb_spec.rb +++ b/sentry-ruby/spec/sentry/breadcrumb_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Breadcrumb do diff --git a/sentry-ruby/spec/sentry/client/event_sending_spec.rb b/sentry-ruby/spec/sentry/client/event_sending_spec.rb index 86098a469..e66926758 100644 --- a/sentry-ruby/spec/sentry/client/event_sending_spec.rb +++ b/sentry-ruby/spec/sentry/client/event_sending_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Client do @@ -8,16 +10,23 @@ config.transport.transport_class = Sentry::DummyTransport end end - subject { Sentry::Client.new(configuration) } + subject(:client) { Sentry::Client.new(configuration) } let(:hub) do - Sentry::Hub.new(subject, Sentry::Scope.new) + Sentry::Hub.new(client, Sentry::Scope.new) + end + + let(:transaction) do + transaction = Sentry::Transaction.new(name: "test transaction", op: "rack.request", hub: hub) + 5.times { |i| transaction.with_child_span(description: "span_#{i}") { } } + transaction end + let(:transaction_event) { client.event_from_transaction(transaction) } describe "#capture_event" do let(:message) { "Test message" } let(:scope) { Sentry::Scope.new } - let(:event) { subject.event_from_message(message) } + let(:event) { client.event_from_message(message) } context "with sample_rate set" do before do @@ -28,26 +37,23 @@ context "with Event" do it "sends the event when it's sampled" do allow(Random).to receive(:rand).and_return(0.49) - subject.capture_event(event, scope) - expect(subject.transport.events.count).to eq(1) + client.capture_event(event, scope) + expect(client.transport.events.count).to eq(1) end it "doesn't send the event when it's not sampled" do allow(Random).to receive(:rand).and_return(0.51) - subject.capture_event(event, scope) - expect(subject.transport).to have_recorded_lost_event(:sample_rate, 'error') - expect(subject.transport.events.count).to eq(0) + client.capture_event(event, scope) + expect(client.transport).to have_recorded_lost_event(:sample_rate, 'error') + expect(client.transport.events.count).to eq(0) end end context "with TransactionEvent" do it "ignores the sampling" do - transaction_event = subject.event_from_transaction(Sentry::Transaction.new(hub: hub)) allow(Random).to receive(:rand).and_return(0.51) - - subject.capture_event(transaction_event, scope) - - expect(subject.transport.events.count).to eq(1) + client.capture_event(transaction_event, scope) + expect(client.transport.events.count).to eq(1) end end end @@ -55,7 +61,7 @@ context 'with config.async set' do let(:async_block) do lambda do |event| - subject.send_event(event) + client.send_event(event) end end @@ -69,10 +75,10 @@ it "executes the given block" do expect(async_block).to receive(:call).and_call_original - returned = subject.capture_event(event, scope) + returned = client.capture_event(event, scope) expect(returned).to be_a(Sentry::ErrorEvent) - expect(subject.transport.events.first).to eq(event.to_json_compatible) + expect(client.transport.events.first).to eq(event.to_json_compatible) end it "doesn't call the async block if not allow sending events" do @@ -80,7 +86,7 @@ expect(async_block).not_to receive(:call) - returned = subject.capture_event(event, scope) + returned = client.capture_event(event, scope) expect(returned).to eq(nil) end @@ -88,14 +94,14 @@ context "with to json conversion failed" do let(:logger) { ::Logger.new(string_io) } let(:string_io) { StringIO.new } - let(:event) { subject.event_from_message("Bad data '\x80\xF8'") } + let(:event) { client.event_from_message("Bad data '\x80\xF8'") } it "does not mask the exception" do configuration.logger = logger - subject.capture_event(event, scope) + client.capture_event(event, scope) - expect(string_io.string).to include("Converting event (#{event.event_id}) to JSON compatible hash failed: source sequence is illegal/malformed utf-8") + expect(string_io.string).to match(/Converting event \(#{event.event_id}\) to JSON compatible hash failed:.*illegal\/malformed utf-8/i) end end @@ -103,10 +109,10 @@ let(:async_block) { nil } it "doesn't cause any issue" do - returned = subject.capture_event(event, scope, { background: false }) + returned = client.capture_event(event, scope, { background: false }) expect(returned).to be_a(Sentry::ErrorEvent) - expect(subject.transport.events.first).to eq(event) + expect(client.transport.events.first).to eq(event) end end @@ -114,17 +120,17 @@ let(:async_block) do lambda do |event, hint| event["tags"]["hint"] = hint - subject.send_event(event) + client.send_event(event) end end it "serializes hint and supplies it as the second argument" do expect(configuration.async).to receive(:call).and_call_original - returned = subject.capture_event(event, scope, { foo: "bar" }) + returned = client.capture_event(event, scope, { foo: "bar" }) expect(returned).to be_a(Sentry::ErrorEvent) - event = subject.transport.events.first + event = client.transport.events.first expect(event.dig("tags", "hint")).to eq({ "foo" => "bar" }) end end @@ -140,20 +146,20 @@ end it "sends events asynchronously" do - subject.capture_event(event, scope) + client.capture_event(event, scope) - expect(subject.transport.events.count).to eq(0) + expect(client.transport.events.count).to eq(0) sleep(0.2) - expect(subject.transport.events.count).to eq(1) + expect(client.transport.events.count).to eq(1) end context "with hint: { background: false }" do it "sends the event immediately" do - subject.capture_event(event, scope, { background: false }) + client.capture_event(event, scope, { background: false }) - expect(subject.transport.events.count).to eq(1) + expect(client.transport.events.count).to eq(1) end end @@ -161,48 +167,57 @@ it "sends the event immediately" do configuration.background_worker_threads = 0 - subject.capture_event(event, scope) + client.capture_event(event, scope) - expect(subject.transport.events.count).to eq(1) + expect(client.transport.events.count).to eq(1) end end - it "records queue overflow" do + it "records queue overflow for error event" do + allow(Sentry.background_worker).to receive(:perform).and_return(false) + + client.capture_event(event, scope) + expect(client.transport).to have_recorded_lost_event(:queue_overflow, 'error') + + expect(client.transport.events.count).to eq(0) + sleep(0.2) + expect(client.transport.events.count).to eq(0) + end + + it "records queue overflow for transaction event with span counts" do allow(Sentry.background_worker).to receive(:perform).and_return(false) - subject.capture_event(event, scope) - expect(subject.transport).to have_recorded_lost_event(:queue_overflow, 'error') + client.capture_event(transaction_event, scope) + expect(client.transport).to have_recorded_lost_event(:queue_overflow, 'transaction') + expect(client.transport).to have_recorded_lost_event(:queue_overflow, 'span', num: 6) - expect(subject.transport.events.count).to eq(0) + expect(client.transport.events.count).to eq(0) sleep(0.2) - expect(subject.transport.events.count).to eq(0) + expect(client.transport.events.count).to eq(0) end end end describe "#send_event" do let(:event_object) do - subject.event_from_exception(ZeroDivisionError.new("divided by 0")) - end - let(:transaction_event_object) do - subject.event_from_transaction(Sentry::Transaction.new(hub: hub)) + client.event_from_exception(ZeroDivisionError.new("divided by 0")) end shared_examples "Event in send_event" do context "when there's an exception" do before do - expect(subject.transport).to receive(:send_event).and_raise(Sentry::ExternalError.new("networking error")) + expect(client.transport).to receive(:send_event).and_raise(Sentry::ExternalError.new("networking error")) end it "raises the error" do expect do - subject.send_event(event) + client.send_event(event) end.to raise_error(Sentry::ExternalError, "networking error") end end it "sends data through the transport" do - expect(subject.transport).to receive(:send_event).with(event) - subject.send_event(event) + expect(client.transport).to receive(:send_event).with(event) + client.send_event(event) end it "applies before_send callback before sending the event" do @@ -216,7 +231,7 @@ event end - subject.send_event(event) + client.send_event(event) if event.is_a?(Sentry::Event) expect(event.tags[:called]).to eq(true) @@ -231,7 +246,7 @@ configuration.before_send_transaction = dbl expect(dbl).not_to receive(:call) - subject.send_event(event) + client.send_event(event) end end @@ -245,7 +260,7 @@ shared_examples "TransactionEvent in send_event" do it "sends data through the transport" do - subject.send_event(event) + client.send_event(event) end it "doesn't apply before_send to TransactionEvent" do @@ -253,7 +268,7 @@ raise "shouldn't trigger me" end - subject.send_event(event) + client.send_event(event) end it "applies before_send_transaction callback before sending the event" do @@ -267,7 +282,7 @@ event end - subject.send_event(event) + client.send_event(event) if event.is_a?(Sentry::Event) expect(event.tags[:called]).to eq(true) @@ -278,11 +293,11 @@ end it_behaves_like "TransactionEvent in send_event" do - let(:event) { transaction_event_object } + let(:event) { transaction_event } end it_behaves_like "TransactionEvent in send_event" do - let(:event) { transaction_event_object.to_json_compatible } + let(:event) { transaction_event.to_json_compatible } end end @@ -300,7 +315,7 @@ let(:message) { "Test message" } let(:scope) { Sentry::Scope.new } - let(:event) { subject.event_from_message(message) } + let(:event) { client.event_from_message(message) } describe "#capture_event" do around do |example| @@ -317,11 +332,35 @@ end it "discards the event and logs a info" do - expect(subject.capture_event(event, scope)).to be_nil + expect(client.capture_event(event, scope)).to be_nil - expect(subject.transport).to have_recorded_lost_event(:event_processor, 'error') expect(string_io.string).to match(/Discarded event because one of the event processors returned nil/) end + + it "records correct client report for error event" do + client.capture_event(event, scope) + expect(client.transport).to have_recorded_lost_event(:event_processor, 'error') + end + + it "records correct transaction and span client reports for transaction event" do + client.capture_event(transaction_event, scope) + expect(client.transport).to have_recorded_lost_event(:event_processor, 'transaction') + expect(client.transport).to have_recorded_lost_event(:event_processor, 'span', num: 6) + end + end + + context "when scope.apply_to_event modifies spans" do + before do + scope.add_event_processor do |event, hint| + 2.times { event.spans.pop } + event + end + end + + it "records correct span delta client report for transaction event" do + client.capture_event(transaction_event, scope) + expect(client.transport).to have_recorded_lost_event(:event_processor, 'span', num: 2) + end end context "when scope.apply_to_event fails" do @@ -332,7 +371,7 @@ end it "swallows the event and logs the failure" do - expect(subject.capture_event(event, scope)).to be_nil + expect(client.capture_event(event, scope)).to be_nil expect(string_io.string).to match(/Event capturing failed: TypeError/) expect(string_io.string).not_to match(__FILE__) @@ -343,7 +382,7 @@ configuration.debug = true end it "logs the error with backtrace" do - expect(subject.capture_event(event, scope)).to be_nil + expect(client.capture_event(event, scope)).to be_nil expect(string_io.string).to match(/Event capturing failed: TypeError/) expect(string_io.string).to match(__FILE__) @@ -358,9 +397,8 @@ end it "swallows and logs Sentry::ExternalError (caused by transport's networking error)" do - expect(subject.capture_event(event, scope)).to be_nil + expect(client.capture_event(event, scope)).to be_nil - expect(subject.transport).to have_recorded_lost_event(:network_error, 'error') expect(string_io.string).to match(/Event sending failed: Failed to open TCP connection/) expect(string_io.string).to match(/Event capturing failed: Failed to open TCP connection/) end @@ -368,10 +406,21 @@ it "swallows and logs errors caused by the user (like in before_send)" do configuration.before_send = ->(_, _) { raise TypeError } - expect(subject.capture_event(event, scope)).to be_nil + expect(client.capture_event(event, scope)).to be_nil expect(string_io.string).to match(/Event sending failed: TypeError/) end + + it "captures client report for error event" do + client.capture_event(event, scope) + expect(client.transport).to have_recorded_lost_event(:network_error, 'error') + end + + it "captures client report for transaction event with span counts" do + client.capture_event(transaction_event, scope) + expect(client.transport).to have_recorded_lost_event(:network_error, 'transaction') + expect(client.transport).to have_recorded_lost_event(:network_error, 'span', num: 6) + end end context "when sending events in background causes error", retry: 3 do @@ -380,32 +429,44 @@ end it "swallows and logs Sentry::ExternalError (caused by transport's networking error)" do - expect(subject.capture_event(event, scope)).to be_a(Sentry::ErrorEvent) + expect(client.capture_event(event, scope)).to be_a(Sentry::ErrorEvent) sleep(0.2) - expect(subject.transport).to have_recorded_lost_event(:network_error, 'error') expect(string_io.string).to match(/Event sending failed: Failed to open TCP connection/) end it "swallows and logs errors caused by the user (like in before_send)" do configuration.before_send = ->(_, _) { raise TypeError } - expect(subject.capture_event(event, scope)).to be_a(Sentry::ErrorEvent) + expect(client.capture_event(event, scope)).to be_a(Sentry::ErrorEvent) sleep(0.2) expect(string_io.string).to match(/Event sending failed: TypeError/) end + + it "captures client report for error event" do + client.capture_event(event, scope) + sleep(0.2) + expect(client.transport).to have_recorded_lost_event(:network_error, 'error') + end + + it "captures client report for transaction event with span counts" do + client.capture_event(transaction_event, scope) + sleep(0.2) + expect(client.transport).to have_recorded_lost_event(:network_error, 'transaction') + expect(client.transport).to have_recorded_lost_event(:network_error, 'span', num: 6) + end end context "when config.async causes error" do before do - expect(subject).to receive(:send_event) + expect(client).to receive(:send_event) end it "swallows Redis related error and send the event synchronizely" do configuration.async = ->(_, _) { raise Redis::ConnectionError } - subject.capture_event(event, scope) + client.capture_event(event, scope) expect(string_io.string).to match(/Async event sending failed: Redis::ConnectionError/) end @@ -413,7 +474,7 @@ it "swallows and logs the exception" do configuration.async = ->(_, _) { raise TypeError } - subject.capture_event(event, scope) + client.capture_event(event, scope) expect(string_io.string).to match(/Async event sending failed: TypeError/) end @@ -424,7 +485,7 @@ context "error happens when sending the event" do it "raises the error" do expect do - subject.send_event(event) + client.send_event(event) end.to raise_error(Sentry::ExternalError) expect(string_io.string).to match(/Event sending failed: Failed to open TCP connection/) @@ -440,7 +501,7 @@ it "raises the error" do expect do - subject.send_event(event) + client.send_event(event) end.to raise_error(TypeError) expect(string_io.string).to match(/Event sending failed: TypeError/) @@ -453,7 +514,7 @@ it "logs the error with backtrace" do expect do - subject.send_event(event) + client.send_event(event) end.to raise_error(TypeError) expect(string_io.string).to match(/Event sending failed: TypeError/) @@ -469,9 +530,9 @@ end end - it "records lost event" do - subject.send_event(event) - expect(subject.transport).to have_recorded_lost_event(:before_send, 'error') + it "records lost error event" do + client.send_event(event) + expect(client.transport).to have_recorded_lost_event(:before_send, 'error') end end @@ -482,10 +543,24 @@ end end - it "records lost event" do - transaction_event = subject.event_from_transaction(Sentry::Transaction.new(hub: hub)) - subject.send_event(transaction_event) - expect(subject.transport).to have_recorded_lost_event(:before_send, 'transaction') + it "records lost transaction with span counts client reports" do + client.send_event(transaction_event) + expect(client.transport).to have_recorded_lost_event(:before_send, 'transaction') + expect(client.transport).to have_recorded_lost_event(:before_send, 'span', num: 6) + end + end + + context "before_send_transaction modifies spans" do + before do + configuration.before_send_transaction = lambda do |event, _hint| + 2.times { event.spans.pop } + event + end + end + + it "records lost span delta client reports" do + expect { client.send_event(transaction_event) }.to raise_error(Sentry::ExternalError) + expect(client.transport).to have_recorded_lost_event(:before_send, 'span', num: 2) end end end diff --git a/sentry-ruby/spec/sentry/client_spec.rb b/sentry-ruby/spec/sentry/client_spec.rb index bd1debcf2..e069ccc5b 100644 --- a/sentry-ruby/spec/sentry/client_spec.rb +++ b/sentry-ruby/spec/sentry/client_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' class ExceptionWithContext < StandardError @@ -341,7 +343,12 @@ def detailed_message(*) event = subject.event_from_exception(NonStringMessageError.new) hash = event.to_hash expect(event).to be_a(Sentry::ErrorEvent) - expect(hash[:exception][:values][0][:value]).to eq("{:foo=>\"bar\"}") + + if RUBY_VERSION >= "3.4" + expect(hash[:exception][:values][0][:value]).to eq("{foo: \"bar\"}") + else + expect(hash[:exception][:values][0][:value]).to eq("{:foo=>\"bar\"}") + end end end diff --git a/sentry-ruby/spec/sentry/configuration_spec.rb b/sentry-ruby/spec/sentry/configuration_spec.rb index a88559c4e..afd78625b 100644 --- a/sentry-ruby/spec/sentry/configuration_spec.rb +++ b/sentry-ruby/spec/sentry/configuration_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Configuration do @@ -26,12 +28,12 @@ describe "#background_worker_threads" do it "sets to have of the processors count" do - allow(Concurrent).to receive(:processor_count).and_return(8) + allow_any_instance_of(Sentry::Configuration).to receive(:processor_count).and_return(8) expect(subject.background_worker_threads).to eq(4) end it "sets to 1 with only 1 processor" do - allow(Concurrent).to receive(:processor_count).and_return(1) + allow_any_instance_of(Sentry::Configuration).to receive(:processor_count).and_return(1) expect(subject.background_worker_threads).to eq(1) end end @@ -278,9 +280,67 @@ end describe "#spotlight" do + before do + ENV.delete('SENTRY_SPOTLIGHT') + end + + after do + ENV.delete('SENTRY_SPOTLIGHT') + end + it "false by default" do expect(subject.spotlight).to eq(false) end + + it 'uses `SENTRY_SPOTLIGHT` env variable for truthy' do + ENV['SENTRY_SPOTLIGHT'] = 'on' + + expect(subject.spotlight).to eq(true) + end + + it 'uses `SENTRY_SPOTLIGHT` env variable for falsy' do + ENV['SENTRY_SPOTLIGHT'] = '0' + + expect(subject.spotlight).to eq(false) + end + + it 'uses `SENTRY_SPOTLIGHT` env variable for custom value' do + ENV['SENTRY_SPOTLIGHT'] = 'https://my.remote.server:8080/stream' + + expect(subject.spotlight).to eq('https://my.remote.server:8080/stream') + end + end + + describe "#debug" do + before do + ENV.delete('SENTRY_DEBUG') + end + + after do + ENV.delete('SENTRY_DEBUG') + end + + it "false by default" do + expect(subject.debug).to eq(false) + end + + it 'uses `SENTRY_DEBUG` env variable for truthy' do + ENV['SENTRY_DEBUG'] = 'on' + + expect(subject.debug).to eq(true) + end + + it 'uses `SENTRY_DEBUG` env variable for falsy' do + ENV['SENTRY_DEBUG'] = '0' + + expect(subject.debug).to eq(false) + end + + it 'uses `SENTRY_DEBUG` env variable to turn on random value' do + ENV['SENTRY_DEBUG'] = 'yabadabadoo' + + expect(subject.debug).to eq(true) + end end describe "#sending_allowed?" do @@ -635,4 +695,21 @@ class SentryConfigurationSample < Sentry::Configuration expect(subject.enabled_patches).to eq(%i[redis http]) end end + + describe "#profiler_class=" do + it "sets the profiler class to Vernier when it's available", when: :vernier_installed? do + subject.profiler_class = Sentry::Vernier::Profiler + expect(subject.profiler_class).to eq(Sentry::Vernier::Profiler) + end + + it "sets the profiler class to StackProf when Vernier is not available", when: { ruby_version?: [:<, "3.2"] } do + expect { subject.profiler_class = Sentry::Vernier::Profiler } + .to raise_error( + ArgumentError, + /Please add the 'vernier' gem to your Gemfile/ + ) + + expect(subject.profiler_class).to eq(Sentry::Profiler) + end + end end diff --git a/sentry-ruby/spec/sentry/cron/monitor_check_ins_spec.rb b/sentry-ruby/spec/sentry/cron/monitor_check_ins_spec.rb index fb8212df6..f34600699 100644 --- a/sentry-ruby/spec/sentry/cron/monitor_check_ins_spec.rb +++ b/sentry-ruby/spec/sentry/cron/monitor_check_ins_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Cron::MonitorCheckIns do @@ -121,6 +123,7 @@ def perform(a, b = 42, c: 99) expect(ok_event.monitor_slug).to eq('job') expect(ok_event.status).to eq(:ok) expect(ok_event.monitor_config).to be_nil + expect(ok_event.duration).to be > 0 end end @@ -160,6 +163,7 @@ def perform; work end expect(ok_event.monitor_slug).to eq('job') expect(ok_event.status).to eq(:ok) expect(ok_event.monitor_config).to be_nil + expect(ok_event.duration).to be > 0 end end diff --git a/sentry-ruby/spec/sentry/cron/monitor_config_spec.rb b/sentry-ruby/spec/sentry/cron/monitor_config_spec.rb index 68bbbe95a..1c934e8cc 100644 --- a/sentry-ruby/spec/sentry/cron/monitor_config_spec.rb +++ b/sentry-ruby/spec/sentry/cron/monitor_config_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Cron::MonitorConfig do diff --git a/sentry-ruby/spec/sentry/cron/monitor_schedule_spec.rb b/sentry-ruby/spec/sentry/cron/monitor_schedule_spec.rb index 8e1b422d1..909a9b4fa 100644 --- a/sentry-ruby/spec/sentry/cron/monitor_schedule_spec.rb +++ b/sentry-ruby/spec/sentry/cron/monitor_schedule_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Cron::MonitorSchedule::Crontab do diff --git a/sentry-ruby/spec/sentry/dsn_spec.rb b/sentry-ruby/spec/sentry/dsn_spec.rb index 24256f291..0a0eaecb4 100644 --- a/sentry-ruby/spec/sentry/dsn_spec.rb +++ b/sentry-ruby/spec/sentry/dsn_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::DSN do diff --git a/sentry-ruby/spec/sentry/envelope_spec.rb b/sentry-ruby/spec/sentry/envelope/item_spec.rb similarity index 92% rename from sentry-ruby/spec/sentry/envelope_spec.rb rename to sentry-ruby/spec/sentry/envelope/item_spec.rb index c12ee3052..ea9cf1019 100644 --- a/sentry-ruby/spec/sentry/envelope_spec.rb +++ b/sentry-ruby/spec/sentry/envelope/item_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Envelope::Item do @@ -7,6 +9,7 @@ ['sessions', 'session'], ['attachment', 'attachment'], ['transaction', 'transaction'], + ['span', 'span'], ['profile', 'profile'], ['check_in', 'monitor'], ['statsd', 'metric_bucket'], diff --git a/sentry-ruby/spec/sentry/event_spec.rb b/sentry-ruby/spec/sentry/event_spec.rb index a5ca457e7..90acb4da8 100644 --- a/sentry-ruby/spec/sentry/event_spec.rb +++ b/sentry-ruby/spec/sentry/event_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Event do @@ -66,7 +68,7 @@ end end - context 'rack context specified', rack: true do + context 'rack context specified', when: :rack_available? do require 'stringio' before do @@ -141,6 +143,12 @@ expect(event.to_hash[:user][:ip_address]).to eq("2.2.2.2") end + it "doesn't overwrite already set ip address" do + Sentry.set_user({ ip_address: "3.3.3.3" }) + Sentry.get_current_scope.apply_to_event(event) + expect(event.to_hash[:user][:ip_address]).to eq("3.3.3.3") + end + context "with config.trusted_proxies = [\"2.2.2.2\"]" do before do Sentry.configuration.trusted_proxies = ["2.2.2.2"] diff --git a/sentry-ruby/spec/sentry/excon_spec.rb b/sentry-ruby/spec/sentry/excon_spec.rb new file mode 100644 index 000000000..96574622a --- /dev/null +++ b/sentry-ruby/spec/sentry/excon_spec.rb @@ -0,0 +1,251 @@ +# frozen_string_literal: true + +require "spec_helper" +require "contexts/with_request_mock" +require "excon" + +RSpec.describe "Sentry::Excon" do + include Sentry::Utils::HttpTracing + include_context "with request mock" + + before do + Excon.defaults[:mock] = true + end + + after(:each) do + Excon.stubs.clear + end + + let(:string_io) { StringIO.new } + let(:logger) do + ::Logger.new(string_io) + end + + context "with IPv6 addresses" do + before do + perform_basic_setup do |config| + config.traces_sample_rate = 1.0 + config.enabled_patches += [:excon] unless config.enabled_patches.include?(:excon) + end + end + + it "correctly parses the short-hand IPv6 addresses" do + Excon.stub({}, { body: "", status: 200 }) + + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + _ = Excon.get("http://[::1]:8080/path", mock: true) + + expect(transaction.span_recorder.spans.count).to eq(2) + + request_span = transaction.span_recorder.spans.last + expect(request_span.data).to eq( + { "url" => "http://::1/path", "http.request.method" => "GET", "http.response.status_code" => 200 } + ) + end + end + + context "with tracing enabled" do + before do + perform_basic_setup do |config| + config.traces_sample_rate = 1.0 + config.transport.transport_class = Sentry::HTTPTransport + config.logger = logger + # the dsn needs to have a real host so we can make a real connection before sending a failed request + config.dsn = "http://foobarbaz@o447951.ingest.sentry.io/5434472" + config.enabled_patches += [:excon] unless config.enabled_patches.include?(:excon) + end + end + + context "with config.send_default_pii = true" do + before do + Sentry.configuration.send_default_pii = true + Sentry.configuration.breadcrumbs_logger = [:http_logger] + end + + it "records the request's span with query string in data" do + Excon.stub({}, { body: "", status: 200 }) + + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + response = Excon.get("http://example.com/path?foo=bar", mock: true) + + expect(response.status).to eq(200) + expect(transaction.span_recorder.spans.count).to eq(2) + + request_span = transaction.span_recorder.spans.last + expect(request_span.op).to eq("http.client") + expect(request_span.origin).to eq("auto.http.excon") + expect(request_span.start_timestamp).not_to be_nil + expect(request_span.timestamp).not_to be_nil + expect(request_span.start_timestamp).not_to eq(request_span.timestamp) + expect(request_span.description).to eq("GET http://example.com/path") + expect(request_span.data).to eq({ + "http.response.status_code" => 200, + "url" => "http://example.com/path", + "http.request.method" => "GET", + "http.query" => "foo=bar" + }) + end + + it "records the request's span with advanced query string in data" do + Excon.stub({}, { body: "", status: 200 }) + + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + connection = Excon.new("http://example.com/path") + response = connection.get(mock: true, query: build_nested_query({ foo: "bar", baz: [1, 2], qux: { a: 1, b: 2 } })) + + expect(response.status).to eq(200) + expect(transaction.span_recorder.spans.count).to eq(2) + + request_span = transaction.span_recorder.spans.last + expect(request_span.op).to eq("http.client") + expect(request_span.origin).to eq("auto.http.excon") + expect(request_span.start_timestamp).not_to be_nil + expect(request_span.timestamp).not_to be_nil + expect(request_span.start_timestamp).not_to eq(request_span.timestamp) + expect(request_span.description).to eq("GET http://example.com/path") + expect(request_span.data).to eq({ + "http.response.status_code" => 200, + "url" => "http://example.com/path", + "http.request.method" => "GET", + "http.query" => "foo=bar&baz%5B%5D=1&baz%5B%5D=2&qux%5Ba%5D=1&qux%5Bb%5D=2" + }) + end + + it "records breadcrumbs" do + Excon.stub({}, { body: "", status: 200 }) + + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + _response = Excon.get("http://example.com/path?foo=bar", mock: true) + + transaction.span_recorder.spans.last + + crumb = Sentry.get_current_scope.breadcrumbs.peek + + expect(crumb.category).to eq("http") + expect(crumb.data[:status]).to eq(200) + expect(crumb.data[:method]).to eq("GET") + expect(crumb.data[:url]).to eq("http://example.com/path") + expect(crumb.data[:query]).to eq("foo=bar") + expect(crumb.data[:body]).to be(nil) + end + end + + context "with config.send_default_pii = false" do + before do + Sentry.configuration.send_default_pii = false + end + + it "records the request's span without query string" do + Excon.stub({}, { body: "", status: 200 }) + + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + response = Excon.get("http://example.com/path?foo=bar", mock: true) + + expect(response.status).to eq(200) + expect(transaction.span_recorder.spans.count).to eq(2) + + request_span = transaction.span_recorder.spans.last + expect(request_span.op).to eq("http.client") + expect(request_span.origin).to eq("auto.http.excon") + expect(request_span.start_timestamp).not_to be_nil + expect(request_span.timestamp).not_to be_nil + expect(request_span.start_timestamp).not_to eq(request_span.timestamp) + expect(request_span.description).to eq("GET http://example.com/path") + expect(request_span.data).to eq({ + "http.response.status_code" => 200, + "url" => "http://example.com/path", + "http.request.method" => "GET" + }) + end + end + + context "when there're multiple requests" do + let(:transaction) { Sentry.start_transaction } + + before do + Sentry.get_current_scope.set_span(transaction) + end + + def verify_spans(transaction) + expect(transaction.span_recorder.spans.count).to eq(3) + expect(transaction.span_recorder.spans[0]).to eq(transaction) + + request_span = transaction.span_recorder.spans[1] + expect(request_span.op).to eq("http.client") + expect(request_span.origin).to eq("auto.http.excon") + expect(request_span.start_timestamp).not_to be_nil + expect(request_span.timestamp).not_to be_nil + expect(request_span.start_timestamp).not_to eq(request_span.timestamp) + expect(request_span.description).to eq("GET http://example.com/path") + expect(request_span.data).to eq({ + "http.response.status_code" => 200, + "url" => "http://example.com/path", + "http.request.method" => "GET" + }) + + request_span = transaction.span_recorder.spans[2] + expect(request_span.op).to eq("http.client") + expect(request_span.origin).to eq("auto.http.excon") + expect(request_span.start_timestamp).not_to be_nil + expect(request_span.timestamp).not_to be_nil + expect(request_span.start_timestamp).not_to eq(request_span.timestamp) + expect(request_span.description).to eq("GET http://example.com/path") + expect(request_span.data).to eq({ + "http.response.status_code" => 404, + "url" => "http://example.com/path", + "http.request.method" => "GET" + }) + end + + it "doesn't mess different requests' data together" do + Excon.stub({}, { body: "", status: 200 }) + response = Excon.get("http://example.com/path?foo=bar", mock: true) + expect(response.status).to eq(200) + + Excon.stub({}, { body: "", status: 404 }) + response = Excon.get("http://example.com/path?foo=bar", mock: true) + expect(response.status).to eq(404) + + verify_spans(transaction) + end + + context "with nested span" do + let(:span) { transaction.start_child(op: "child span") } + + before do + Sentry.get_current_scope.set_span(span) + end + + it "attaches http spans to the span instead of top-level transaction" do + Excon.stub({}, { body: "", status: 200 }) + response = Excon.get("http://example.com/path?foo=bar", mock: true) + expect(response.status).to eq(200) + + expect(transaction.span_recorder.spans.count).to eq(3) + expect(span.parent_span_id).to eq(transaction.span_id) + http_span = transaction.span_recorder.spans.last + expect(http_span.parent_span_id).to eq(span.span_id) + end + end + end + end + + context "without SDK" do + it "doesn't affect the HTTP lib anything" do + Excon.stub({}, { body: "", status: 200 }) + + response = Excon.get("http://example.com/path") + expect(response.status).to eq(200) + end + end +end diff --git a/sentry-ruby/spec/sentry/faraday_spec.rb b/sentry-ruby/spec/sentry/faraday_spec.rb new file mode 100644 index 000000000..0cb69fe39 --- /dev/null +++ b/sentry-ruby/spec/sentry/faraday_spec.rb @@ -0,0 +1,281 @@ +# frozen_string_literal: true + +require "faraday" +require_relative "../spec_helper" + +RSpec.describe Sentry::Faraday do + before(:all) do + perform_basic_setup do |config| + config.enabled_patches << :faraday + config.traces_sample_rate = 1.0 + config.logger = ::Logger.new(StringIO.new) + end + end + + after(:all) do + Sentry.configuration.enabled_patches = Sentry::Configuration::DEFAULT_PATCHES + end + + context "with tracing enabled" do + let(:http) do + Faraday.new(url) do |f| + f.request :json + + f.adapter Faraday::Adapter::Test do |stub| + stub.get("/test") do + [200, { "Content-Type" => "text/html" }, "

hello world

"] + end + end + end + end + + let(:url) { "http://example.com" } + + it "records the request's span" do + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + _response = http.get("/test") + + request_span = transaction.span_recorder.spans.last + + expect(request_span.op).to eq("http.client") + expect(request_span.origin).to eq("auto.http.faraday") + expect(request_span.start_timestamp).not_to be_nil + expect(request_span.timestamp).not_to be_nil + expect(request_span.start_timestamp).not_to eq(request_span.timestamp) + expect(request_span.description).to eq("GET http://example.com/test") + + expect(request_span.data).to eq({ + "http.response.status_code" => 200, + "url" => "http://example.com/test", + "http.request.method" => "GET" + }) + end + end + + context "with config.send_default_pii = true" do + let(:http) do + Faraday.new(url) do |f| + f.adapter Faraday::Adapter::Test do |stub| + stub.get("/test") do + [200, { "Content-Type" => "text/html" }, "

hello world

"] + end + + stub.post("/test") do + [200, { "Content-Type" => "application/json" }, { hello: "world" }.to_json] + end + end + end + end + + let(:url) { "http://example.com" } + + before do + Sentry.configuration.send_default_pii = true + Sentry.configuration.breadcrumbs_logger = [:http_logger] + end + + it "records the request's span with query string in data" do + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + _response = http.get("/test?foo=bar") + + request_span = transaction.span_recorder.spans.last + + expect(request_span.description).to eq("GET http://example.com/test") + + expect(request_span.data).to eq({ + "http.response.status_code" => 200, + "url" => "http://example.com/test", + "http.request.method" => "GET", + "http.query" => "foo=bar" + }) + end + + it "records breadcrumbs" do + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + _response = http.get("/test?foo=bar") + + transaction.span_recorder.spans.last + + crumb = Sentry.get_current_scope.breadcrumbs.peek + + expect(crumb.category).to eq("http") + expect(crumb.data[:status]).to eq(200) + expect(crumb.data[:method]).to eq("GET") + expect(crumb.data[:url]).to eq("http://example.com/test") + expect(crumb.data[:query]).to eq("foo=bar") + expect(crumb.data[:body]).to be(nil) + end + + it "records POST request body" do + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + body = { foo: "bar" }.to_json + _response = http.post("/test?foo=bar", body, "Content-Type" => "application/json") + + request_span = transaction.span_recorder.spans.last + + expect(request_span.description).to eq("POST http://example.com/test") + + expect(request_span.data).to eq({ + "http.response.status_code" => 200, + "url" => "http://example.com/test", + "http.request.method" => "POST", + "http.query" => "foo=bar" + }) + + crumb = Sentry.get_current_scope.breadcrumbs.peek + + expect(crumb.data[:body]).to eq(body) + end + + context "with custom trace_propagation_targets" do + let(:http) do + Faraday.new(url) do |f| + f.adapter Faraday::Adapter::Test do |stub| + stub.get("/test") do + [200, { "Content-Type" => "text/html" }, "

hello world

"] + end + end + end + end + + before do + Sentry.configuration.trace_propagation_targets = ["example.com", /foobar.org\/api\/v2/] + end + + context "when the request is not to the same target" do + let(:url) { "http://another.site" } + + it "doesn't add sentry headers to outgoing requests to different target" do + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + response = http.get("/test") + + request_span = transaction.span_recorder.spans.last + + expect(request_span.description).to eq("GET #{url}/test") + + expect(request_span.data).to eq({ + "http.response.status_code" => 200, + "url" => "#{url}/test", + "http.request.method" => "GET" + }) + + expect(response.headers.key?("sentry-trace")).to eq(false) + expect(response.headers.key?("baggage")).to eq(false) + end + end + + context "when the request is to the same target" do + let(:url) { "http://example.com" } + + before do + Sentry.configuration.trace_propagation_targets = ["example.com"] + end + + it "adds sentry headers to outgoing requests" do + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + response = http.get("/test") + + request_span = transaction.span_recorder.spans.last + + expect(request_span.description).to eq("GET #{url}/test") + + expect(request_span.data).to eq({ + "http.response.status_code" => 200, + "url" => "#{url}/test", + "http.request.method" => "GET" + }) + + expect(response.env.request_headers.key?("sentry-trace")).to eq(true) + expect(response.env.request_headers.key?("baggage")).to eq(true) + end + end + + context "when the request's url configured target regexp" do + let(:url) { "http://example.com" } + + before do + Sentry.configuration.trace_propagation_targets = [/example/] + end + + it "adds sentry headers to outgoing requests" do + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + response = http.get("/test") + + request_span = transaction.span_recorder.spans.last + + expect(request_span.description).to eq("GET #{url}/test") + + expect(request_span.data).to eq({ + "http.response.status_code" => 200, + "url" => "#{url}/test", + "http.request.method" => "GET" + }) + + expect(response.env.request_headers.key?("sentry-trace")).to eq(true) + expect(response.env.request_headers.key?("baggage")).to eq(true) + end + end + end + end + + context "when adapter is net/http" do + let(:http) do + Faraday.new(url) do |f| + f.request :json + f.adapter :net_http + end + end + + let(:url) { "http://example.com" } + + it "skips instrumentation" do + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + _response = http.get("/test") + + request_span = transaction.span_recorder.spans.last + + expect(request_span.op).to eq("http.client") + expect(request_span.origin).to eq("auto.http.net_http") + + expect(transaction.span_recorder.spans.map(&:origin)).not_to include("auto.http.faraday") + end + end + + context "when Sentry is not initialized" do + let(:http) do + Faraday.new(url) do |f| + f.adapter Faraday::Adapter::Test do |stub| + stub.get("/test") do + [200, { "Content-Type" => "text/html" }, "

hello world

"] + end + end + end + end + + let(:url) { "http://example.com" } + + it "skips instrumentation" do + allow(Sentry).to receive(:initialized?).and_return(false) + + response = http.get("/test") + + expect(response.status).to eq(200) + end + end +end diff --git a/sentry-ruby/spec/sentry/graphql_spec.rb b/sentry-ruby/spec/sentry/graphql_spec.rb index 5820b73d2..a90dfe6b0 100644 --- a/sentry-ruby/spec/sentry/graphql_spec.rb +++ b/sentry-ruby/spec/sentry/graphql_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' with_graphql = begin diff --git a/sentry-ruby/spec/sentry/hub_spec.rb b/sentry-ruby/spec/sentry/hub_spec.rb index 2448be413..735bc6acd 100644 --- a/sentry-ruby/spec/sentry/hub_spec.rb +++ b/sentry-ruby/spec/sentry/hub_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Hub do @@ -350,6 +352,14 @@ expect(string_io.string).to include("Options [:unsupported] are not supported and will not be applied to the event.") end + it "does not warn about unsupported options if all passed options are supported" do + expect do + subject.capture_event(event, level: 'DEBUG') + end.not_to raise_error + + expect(string_io.string).not_to include("Options [] are not supported and will not be applied to the event.") + end + context "when event is a transaction" do it "transaction.set_context merges and takes precedence over scope.set_context" do scope.set_context(:foo, { val: 42 }) @@ -511,9 +521,25 @@ describe "#pop_scope" do it "pops the current scope" do + prev_scope = subject.current_scope + subject.push_scope + scope = subject.current_scope expect(subject.current_scope).to eq(scope) subject.pop_scope - expect(subject.current_scope).to eq(nil) + expect(subject.current_scope).to eq(prev_scope) + end + + it "doesn't pop the last layer" do + expect(subject.instance_variable_get(:@stack).count).to eq(1) + + subject.pop_scope + + expect(subject.instance_variable_get(:@stack).count).to eq(1) + + # It should be a completely new scope + expect(subject.current_scope).not_to eq(scope) + # But it should be the same client + expect(subject.current_client).to eq(client) end end @@ -535,21 +561,6 @@ expect(subject.current_scope).not_to eq(scope) expect(subject.current_scope.tags).to eq({ foo: "bar" }) end - - context "when the current_scope is nil" do - before do - subject.pop_scope - expect(subject.current_scope).to eq(nil) - end - it "creates a new scope" do - scope.set_tags({ foo: "bar" }) - - subject.push_scope - - expect(subject.current_scope).not_to eq(scope) - expect(subject.current_scope.tags).to eq({}) - end - end end describe '#configure_scope' do diff --git a/sentry-ruby/spec/sentry/integrable_spec.rb b/sentry-ruby/spec/sentry/integrable_spec.rb index 269ba821e..6e7a03a07 100644 --- a/sentry-ruby/spec/sentry/integrable_spec.rb +++ b/sentry-ruby/spec/sentry/integrable_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require "sentry/integrable" diff --git a/sentry-ruby/spec/sentry/interface_spec.rb b/sentry-ruby/spec/sentry/interface_spec.rb index 7d95d5263..2d70cdc9b 100644 --- a/sentry-ruby/spec/sentry/interface_spec.rb +++ b/sentry-ruby/spec/sentry/interface_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'sentry/interface' diff --git a/sentry-ruby/spec/sentry/interfaces/request_interface_spec.rb b/sentry-ruby/spec/sentry/interfaces/request_interface_spec.rb index 580a610d3..a7a2225d0 100644 --- a/sentry-ruby/spec/sentry/interfaces/request_interface_spec.rb +++ b/sentry-ruby/spec/sentry/interfaces/request_interface_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' return unless defined?(Rack) diff --git a/sentry-ruby/spec/sentry/interfaces/stacktrace_builder_spec.rb b/sentry-ruby/spec/sentry/interfaces/stacktrace_builder_spec.rb index 637aff2ad..fd89360a9 100644 --- a/sentry-ruby/spec/sentry/interfaces/stacktrace_builder_spec.rb +++ b/sentry-ruby/spec/sentry/interfaces/stacktrace_builder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::StacktraceBuilder do @@ -11,8 +13,8 @@ let(:backtrace) do [ - "#{fixture_file}:6:in `bar'", - "#{fixture_file}:2:in `foo'" + "#{fixture_file}:8:in `bar'", + "#{fixture_file}:4:in `foo'" ] end @@ -36,8 +38,8 @@ expect(first_frame.filename).to match(/stacktrace_test_fixture.rb/) expect(first_frame.function).to eq("foo") - expect(first_frame.lineno).to eq(2) - expect(first_frame.pre_context).to eq([nil, nil, "def foo\n"]) + expect(first_frame.lineno).to eq(4) + expect(first_frame.pre_context).to eq(["# frozen_string_literal: true\n", "\n", "def foo\n"]) expect(first_frame.context_line).to eq(" bar\n") expect(first_frame.post_context).to eq(["end\n", "\n", "def bar\n"]) @@ -45,12 +47,27 @@ expect(second_frame.filename).to match(/stacktrace_test_fixture.rb/) expect(second_frame.function).to eq("bar") - expect(second_frame.lineno).to eq(6) + expect(second_frame.lineno).to eq(8) expect(second_frame.pre_context).to eq(["end\n", "\n", "def bar\n"]) expect(second_frame.context_line).to eq(" baz\n") expect(second_frame.post_context).to eq(["end\n", nil, nil]) end + context "when strip_backtrace_load_path is false" do + let(:configuration) do + Sentry::Configuration.new.tap do |config| + config.project_root = fixture_root + config.strip_backtrace_load_path = false + end + end + + it "does not strip load paths for filenames" do + interface = subject.build(backtrace: backtrace) + expect(interface.frames.first.filename).to eq(fixture_file) + expect(interface.frames.last.filename).to eq(fixture_file) + end + end + context "with block argument" do it "removes the frame if it's evaluated as nil" do interface = subject.build(backtrace: backtrace) do |frame| @@ -77,7 +94,7 @@ expect(second_frame.filename).to match(/stacktrace_test_fixture.rb/) expect(second_frame.function).to eq("bar") - expect(second_frame.lineno).to eq(6) + expect(second_frame.lineno).to eq(8) expect(second_frame.vars).to eq({ foo: "bar" }) end end @@ -89,7 +106,7 @@ expect(hash[:filename]).to match(/stacktrace_test_fixture.rb/) expect(hash[:function]).to eq("bar") - expect(hash[:lineno]).to eq(6) + expect(hash[:lineno]).to eq(8) expect(hash[:pre_context]).to eq(["end\n", "\n", "def bar\n"]) expect(hash[:context_line]).to eq(" baz\n") expect(hash[:post_context]).to eq(["end\n", nil, nil]) diff --git a/sentry-ruby/spec/sentry/interfaces/stacktrace_spec.rb b/sentry-ruby/spec/sentry/interfaces/stacktrace_spec.rb index 143dc369c..6ceb0b1d0 100644 --- a/sentry-ruby/spec/sentry/interfaces/stacktrace_spec.rb +++ b/sentry-ruby/spec/sentry/interfaces/stacktrace_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::StacktraceInterface::Frame do @@ -28,5 +30,15 @@ expect(second_frame.function).to eq("save_user") expect(second_frame.lineno).to eq(5) end + + it "does not strip load path when strip_backtrace_load_path is false" do + first_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.first, false) + expect(first_frame.filename).to eq(first_frame.abs_path) + expect(first_frame.filename).to eq(raw_lines.first.split(':').first) + + second_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.last, false) + expect(second_frame.filename).to eq(second_frame.abs_path) + expect(second_frame.filename).to eq(raw_lines.last.split(':').first) + end end end diff --git a/sentry-ruby/spec/sentry/linecache_spec.rb b/sentry-ruby/spec/sentry/linecache_spec.rb index 56c9e086b..097b3ed56 100644 --- a/sentry-ruby/spec/sentry/linecache_spec.rb +++ b/sentry-ruby/spec/sentry/linecache_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' # rubocop:disable Style/WordArray RSpec.describe Sentry::LineCache do diff --git a/sentry-ruby/spec/sentry/metrics/aggregator_spec.rb b/sentry-ruby/spec/sentry/metrics/aggregator_spec.rb index dcd78ab86..b54c6a0d9 100644 --- a/sentry-ruby/spec/sentry/metrics/aggregator_spec.rb +++ b/sentry-ruby/spec/sentry/metrics/aggregator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Metrics::Aggregator do diff --git a/sentry-ruby/spec/sentry/metrics/configuration_spec.rb b/sentry-ruby/spec/sentry/metrics/configuration_spec.rb index 2c7e22fbe..3d04ad3ea 100644 --- a/sentry-ruby/spec/sentry/metrics/configuration_spec.rb +++ b/sentry-ruby/spec/sentry/metrics/configuration_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Metrics::Configuration do diff --git a/sentry-ruby/spec/sentry/metrics/counter_metric_spec.rb b/sentry-ruby/spec/sentry/metrics/counter_metric_spec.rb index f92a0135f..f1c480b45 100644 --- a/sentry-ruby/spec/sentry/metrics/counter_metric_spec.rb +++ b/sentry-ruby/spec/sentry/metrics/counter_metric_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Metrics::CounterMetric do diff --git a/sentry-ruby/spec/sentry/metrics/distribution_metric_spec.rb b/sentry-ruby/spec/sentry/metrics/distribution_metric_spec.rb index 7e5b2f2d4..e8ac709f3 100644 --- a/sentry-ruby/spec/sentry/metrics/distribution_metric_spec.rb +++ b/sentry-ruby/spec/sentry/metrics/distribution_metric_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Metrics::DistributionMetric do diff --git a/sentry-ruby/spec/sentry/metrics/gauge_metric_spec.rb b/sentry-ruby/spec/sentry/metrics/gauge_metric_spec.rb index 44be08465..fce3141c6 100644 --- a/sentry-ruby/spec/sentry/metrics/gauge_metric_spec.rb +++ b/sentry-ruby/spec/sentry/metrics/gauge_metric_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Metrics::GaugeMetric do diff --git a/sentry-ruby/spec/sentry/metrics/local_aggregator_spec.rb b/sentry-ruby/spec/sentry/metrics/local_aggregator_spec.rb index 5c5ab0583..3056808da 100644 --- a/sentry-ruby/spec/sentry/metrics/local_aggregator_spec.rb +++ b/sentry-ruby/spec/sentry/metrics/local_aggregator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Metrics::LocalAggregator do diff --git a/sentry-ruby/spec/sentry/metrics/metric_spec.rb b/sentry-ruby/spec/sentry/metrics/metric_spec.rb index a5ed3fd93..ccb4fa222 100644 --- a/sentry-ruby/spec/sentry/metrics/metric_spec.rb +++ b/sentry-ruby/spec/sentry/metrics/metric_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Metrics::Metric do diff --git a/sentry-ruby/spec/sentry/metrics/set_metric_spec.rb b/sentry-ruby/spec/sentry/metrics/set_metric_spec.rb index fdf74dd0d..2cd01da83 100644 --- a/sentry-ruby/spec/sentry/metrics/set_metric_spec.rb +++ b/sentry-ruby/spec/sentry/metrics/set_metric_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Metrics::SetMetric do diff --git a/sentry-ruby/spec/sentry/metrics/timing_spec.rb b/sentry-ruby/spec/sentry/metrics/timing_spec.rb index 8142fbb4d..3b2ee2235 100644 --- a/sentry-ruby/spec/sentry/metrics/timing_spec.rb +++ b/sentry-ruby/spec/sentry/metrics/timing_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Metrics::Timing do diff --git a/sentry-ruby/spec/sentry/metrics_spec.rb b/sentry-ruby/spec/sentry/metrics_spec.rb index 335c31e5a..e50c55074 100644 --- a/sentry-ruby/spec/sentry/metrics_spec.rb +++ b/sentry-ruby/spec/sentry/metrics_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Metrics do diff --git a/sentry-ruby/spec/sentry/net/http_spec.rb b/sentry-ruby/spec/sentry/net/http_spec.rb index 064c4f36c..9de0a6f76 100644 --- a/sentry-ruby/spec/sentry/net/http_spec.rb +++ b/sentry-ruby/spec/sentry/net/http_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require 'contexts/with_request_mock' @@ -107,6 +109,22 @@ end end + it "supports non-ascii characters in the path" do + stub_normal_response + + uri = URI('http://example.com') + http = Net::HTTP.new(uri.host, uri.port) + request = Net::HTTP::Get.new('/path?q=øgreyfoss&å=vær') + + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + + response = http.request(request) + + expect(response.code).to eq("200") + end + it "adds sentry-trace header to the request header" do uri = URI("http://example.com/path") http = Net::HTTP.new(uri.host, uri.port) diff --git a/sentry-ruby/spec/sentry/profiler_spec.rb b/sentry-ruby/spec/sentry/profiler_spec.rb index 215e739be..883fac025 100644 --- a/sentry-ruby/spec/sentry/profiler_spec.rb +++ b/sentry-ruby/spec/sentry/profiler_spec.rb @@ -1,8 +1,8 @@ -require "spec_helper" +# frozen_string_literal: true -return unless defined?(StackProf) +require "spec_helper" -RSpec.describe Sentry::Profiler do +RSpec.describe Sentry::Profiler, when: :stack_prof_installed? do before do perform_basic_setup do |config| config.traces_sample_rate = 1.0 @@ -176,10 +176,11 @@ subject.set_initial_sample_decision(true) subject.start subject.stop + + allow(StackProf).to receive(:results).and_return([]) end it 'returns empty' do - expect(StackProf).to receive(:results).and_call_original expect(subject.to_hash).to eq({}) end diff --git a/sentry-ruby/spec/sentry/propagation_context_spec.rb b/sentry-ruby/spec/sentry/propagation_context_spec.rb index 644fc9530..7e16404b6 100644 --- a/sentry-ruby/spec/sentry/propagation_context_spec.rb +++ b/sentry-ruby/spec/sentry/propagation_context_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::PropagationContext do diff --git a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb index e03984cdf..21e1786ef 100644 --- a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb +++ b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb @@ -1,8 +1,9 @@ -require 'spec_helper' +# frozen_string_literal: true -return unless defined?(Rack) +require 'spec_helper' +require 'sentry/vernier/profiler' -RSpec.describe Sentry::Rack::CaptureExceptions, rack: true do +RSpec.describe 'Sentry::Rack::CaptureExceptions', when: :rack_available? do let(:exception) { ZeroDivisionError.new("divided by 0") } let(:additional_headers) { {} } let(:env) { Rack::MockRequest.env_for("/test", additional_headers) } @@ -14,7 +15,7 @@ it 'captures the exception from direct raise' do app = ->(_e) { raise exception } - stack = described_class.new(app) + stack = Sentry::Rack::CaptureExceptions.new(app) expect { stack.call(env) }.to raise_error(ZeroDivisionError) @@ -27,7 +28,7 @@ it 'has the correct mechanism' do app = ->(_e) { raise exception } - stack = described_class.new(app) + stack = Sentry::Rack::CaptureExceptions.new(app) expect { stack.call(env) }.to raise_error(ZeroDivisionError) @@ -41,7 +42,7 @@ e['rack.exception'] = exception [200, {}, ['okay']] end - stack = described_class.new(app) + stack = Sentry::Rack::CaptureExceptions.new(app) expect do stack.call(env) @@ -57,7 +58,7 @@ e['sinatra.error'] = exception [200, {}, ['okay']] end - stack = described_class.new(app) + stack = Sentry::Rack::CaptureExceptions.new(app) expect do stack.call(env) @@ -72,7 +73,7 @@ e['rack.exception'] = exception [200, {}, ['okay']] end - stack = described_class.new(app) + stack = Sentry::Rack::CaptureExceptions.new(app) stack.call(env) @@ -88,7 +89,7 @@ [200, { 'content-type' => 'text/plain' }, ['OK']] end - stack = described_class.new(Rack::Lint.new(app)) + stack = Sentry::Rack::CaptureExceptions.new(Rack::Lint.new(app)) expect { stack.call(env) }.to_not raise_error expect(env.key?("sentry.error_event_id")).to eq(false) end @@ -111,7 +112,7 @@ a / b end - stack = described_class.new(app) + stack = Sentry::Rack::CaptureExceptions.new(app) expect { stack.call(env) }.to raise_error(ZeroDivisionError) @@ -135,7 +136,7 @@ def inspect a / b end - stack = described_class.new(app) + stack = Sentry::Rack::CaptureExceptions.new(app) expect { stack.call(env) }.to raise_error(ZeroDivisionError) @@ -153,7 +154,7 @@ def inspect a / b end - stack = described_class.new(app) + stack = Sentry::Rack::CaptureExceptions.new(app) expect { stack.call(env) }.to raise_error(ZeroDivisionError) @@ -181,7 +182,7 @@ def inspect [200, {}, ["ok"]] end - app_1 = described_class.new(request_1) + app_1 = Sentry::Rack::CaptureExceptions.new(request_1) app_1.call(env) @@ -195,7 +196,7 @@ def inspect Sentry.capture_message("test") [200, {}, ["ok"]] end - app_1 = described_class.new(request_1) + app_1 = Sentry::Rack::CaptureExceptions.new(request_1) app_1.call(env) @@ -209,7 +210,7 @@ def inspect e['rack.exception'] = Exception.new [200, {}, ["ok"]] end - app_1 = described_class.new(request_1) + app_1 = Sentry::Rack::CaptureExceptions.new(request_1) app_1.call(env) event = last_sentry_event @@ -221,7 +222,7 @@ def inspect e['rack.exception'] = Exception.new [200, {}, ["ok"]] end - app_2 = described_class.new(request_2) + app_2 = Sentry::Rack::CaptureExceptions.new(request_2) app_2.call(env) event = last_sentry_event @@ -250,7 +251,7 @@ def inspect end let(:stack) do - described_class.new( + Sentry::Rack::CaptureExceptions.new( ->(_) do [200, {}, ["ok"]] end @@ -438,7 +439,7 @@ def will_be_sampled_by_sdk [200, {}, ["ok"]] end - stack = described_class.new(app) + stack = Sentry::Rack::CaptureExceptions.new(app) stack.call(env) @@ -462,7 +463,7 @@ def will_be_sampled_by_sdk end end - stack = described_class.new(app) + stack = Sentry::Rack::CaptureExceptions.new(app) stack.call(env) @@ -496,7 +497,7 @@ def will_be_sampled_by_sdk [200, {}, ["ok"]] end - stack = described_class.new(app) + stack = Sentry::Rack::CaptureExceptions.new(app) stack.call(env) @@ -514,7 +515,7 @@ def will_be_sampled_by_sdk raise "foo" end - stack = described_class.new(app) + stack = Sentry::Rack::CaptureExceptions.new(app) expect do stack.call(env) @@ -541,7 +542,7 @@ def will_be_sampled_by_sdk end let(:stack) do - described_class.new( + Sentry::Rack::CaptureExceptions.new( ->(_) do [200, {}, ["ok"]] end @@ -588,7 +589,7 @@ def will_be_sampled_by_sdk let(:stack) do app = ->(_e) { raise exception } - described_class.new(app) + Sentry::Rack::CaptureExceptions.new(app) end before { perform_basic_setup } @@ -620,7 +621,7 @@ def will_be_sampled_by_sdk expect_any_instance_of(Sentry::Hub).not_to receive(:start_session) expect(Sentry.session_flusher).to be_nil - stack = described_class.new(app) + stack = Sentry::Rack::CaptureExceptions.new(app) stack.call(env) expect(sentry_envelopes.count).to eq(0) @@ -646,7 +647,7 @@ def will_be_sampled_by_sdk end end - stack = described_class.new(app) + stack = Sentry::Rack::CaptureExceptions.new(app) expect(Sentry.session_flusher).not_to be_nil @@ -681,74 +682,106 @@ def will_be_sampled_by_sdk end end - if defined?(StackProf) - describe "profiling" do - context "when profiling is enabled" do - before do - perform_basic_setup do |config| - config.traces_sample_rate = 1.0 - config.profiles_sample_rate = 1.0 - config.release = "test-release" - end - end + shared_examples "a profiled transaction" do + it "collects a profile", retry: 3 do + stack = Sentry::Rack::CaptureExceptions.new(app) + stack.call(env) + event = last_sentry_event - let(:stackprof_results) do - data = StackProf::Report.from_file('spec/support/stackprof_results.json').data - # relative dir differs on each machine - data[:frames].each { |_id, fra| fra[:file].gsub!(//, Dir.pwd) } - data - end + profile = event.profile + expect(profile).not_to be_nil - before do - StackProf.stop - allow(StackProf).to receive(:results).and_return(stackprof_results) - end + expect(profile[:event_id]).not_to be_nil + expect(profile[:platform]).to eq("ruby") + expect(profile[:version]).to eq("1") + expect(profile[:environment]).to eq("development") + expect(profile[:release]).to eq("test-release") + expect { Time.parse(profile[:timestamp]) }.not_to raise_error - it "collects a profile" do - app = ->(_) do - [200, {}, "ok"] - end + expect(profile[:device]).to include(:architecture) + expect(profile[:os]).to include(:name, :version) + expect(profile[:runtime]).to include(:name, :version) - stack = described_class.new(app) - stack.call(env) - event = last_sentry_event + expect(profile[:transaction]).to include(:id, :name, :trace_id, :active_thread_id) + expect(profile[:transaction][:id]).to eq(event.event_id) + expect(profile[:transaction][:name]).to eq(event.transaction) + expect(profile[:transaction][:trace_id]).to eq(event.contexts[:trace][:trace_id]) + + thread_id_mapping = { + Sentry::Profiler => "0", + Sentry::Vernier::Profiler => Thread.current.object_id.to_s + } + + expect(profile[:transaction][:active_thread_id]).to eq(thread_id_mapping[Sentry.configuration.profiler_class]) + + # detailed checking of content is done in profiler_spec, + # just check basic structure here + frames = profile[:profile][:frames] + expect(frames).to be_a(Array) + expect(frames.first).to include(:function, :filename, :abs_path, :in_app) + + stacks = profile[:profile][:stacks] + expect(stacks).to be_a(Array) + expect(stacks.first).to be_a(Array) + expect(stacks.first.first).to be_a(Integer) - profile = event.profile - expect(profile).not_to be_nil + samples = profile[:profile][:samples] + expect(samples).to be_a(Array) + expect(samples.first).to include(:stack_id, :thread_id, :elapsed_since_start_ns) + end + end + + describe "profiling with StackProf", when: [:stack_prof_installed?, :rack_available?] do + context "when profiling is enabled" do + let(:app) do + ->(_) do + [200, {}, "ok"] + end + end - expect(profile[:event_id]).not_to be_nil - expect(profile[:platform]).to eq("ruby") - expect(profile[:version]).to eq("1") - expect(profile[:environment]).to eq("development") - expect(profile[:release]).to eq("test-release") - expect { Time.parse(profile[:timestamp]) }.not_to raise_error + let(:stackprof_results) do + data = StackProf::Report.from_file('spec/support/stackprof_results.json').data + # relative dir differs on each machine + data[:frames].each { |_id, fra| fra[:file].gsub!(//, Dir.pwd) } + data + end + + before do + perform_basic_setup do |config| + config.traces_sample_rate = 1.0 + config.profiles_sample_rate = 1.0 + config.release = "test-release" + end - expect(profile[:device]).to include(:architecture) - expect(profile[:os]).to include(:name, :version) - expect(profile[:runtime]).to include(:name, :version) + StackProf.stop - expect(profile[:transaction]).to include(:id, :name, :trace_id, :active_thead_id) - expect(profile[:transaction][:id]).to eq(event.event_id) - expect(profile[:transaction][:name]).to eq(event.transaction) - expect(profile[:transaction][:trace_id]).to eq(event.contexts[:trace][:trace_id]) - expect(profile[:transaction][:active_thead_id]).to eq("0") + allow(StackProf).to receive(:results).and_return(stackprof_results) + end - # detailed checking of content is done in profiler_spec, - # just check basic structure here - frames = profile[:profile][:frames] - expect(frames).to be_a(Array) - expect(frames.first).to include(:function, :filename, :abs_path, :in_app) + include_examples "a profiled transaction" + end + end - stacks = profile[:profile][:stacks] - expect(stacks).to be_a(Array) - expect(stacks.first).to be_a(Array) - expect(stacks.first.first).to be_a(Integer) + describe "profiling with vernier", when: [:vernier_installed?, :rack_available?] do + context "when profiling is enabled" do + let(:app) do + ->(_) do + ProfilerTest::Bar.bar + [200, {}, "ok"] + end + end - samples = profile[:profile][:samples] - expect(samples).to be_a(Array) - expect(samples.first).to include(:stack_id, :thread_id, :elapsed_since_start_ns) + before do + perform_basic_setup do |config| + config.traces_sample_rate = 1.0 + config.profiles_sample_rate = 1.0 + config.release = "test-release" + config.profiler_class = Sentry::Vernier::Profiler + config.project_root = Dir.pwd end end + + include_examples "a profiled transaction" end end end diff --git a/sentry-ruby/spec/sentry/rake_spec.rb b/sentry-ruby/spec/sentry/rake_spec.rb index a1eb22998..904f19170 100644 --- a/sentry-ruby/spec/sentry/rake_spec.rb +++ b/sentry-ruby/spec/sentry/rake_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe "rake auto-reporting" do diff --git a/sentry-ruby/spec/sentry/redis_spec.rb b/sentry-ruby/spec/sentry/redis_spec.rb index 2722e0498..50e86ac55 100644 --- a/sentry-ruby/spec/sentry/redis_spec.rb +++ b/sentry-ruby/spec/sentry/redis_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Redis do diff --git a/sentry-ruby/spec/sentry/rspec/matchers_spec.rb b/sentry-ruby/spec/sentry/rspec/matchers_spec.rb new file mode 100644 index 000000000..45e914f52 --- /dev/null +++ b/sentry-ruby/spec/sentry/rspec/matchers_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require "spec_helper" +require "sentry/rspec" + +RSpec.describe "Sentry RSpec Matchers" do + include Sentry::TestHelper + + before do + # simulate normal user setup + Sentry.init do |config| + config.dsn = 'https://2fb45f003d054a7ea47feb45898f7649@o447951.ingest.sentry.io/5434472' + config.enabled_environments = ["production"] + config.environment = :test + end + + setup_sentry_test + end + + after do + teardown_sentry_test + end + + let(:exception) { StandardError.new("Gaah!") } + + describe "include_sentry_event" do + it "matches events with the given message" do + Sentry.capture_message("Ooops") + + expect(sentry_events).to include_sentry_event("Ooops") + end + + it "does not match events with a different message" do + Sentry.capture_message("Ooops") + + expect(sentry_events).not_to include_sentry_event("Different message") + end + + it "matches events with exception" do + Sentry.capture_exception(exception) + + expect(sentry_events).to include_sentry_event(exception: exception.class, message: exception.message) + end + + it "does not match events with different exception" do + exception = StandardError.new("Gaah!") + + Sentry.capture_exception(exception) + + expect(sentry_events).not_to include_sentry_event(exception: StandardError, message: "Oops!") + end + + it "matches events with context" do + Sentry.set_context("rails.error", { some: "stuff" }) + Sentry.capture_message("Ooops") + + expect(sentry_events).to include_sentry_event("Ooops") + .with_context("rails.error" => { some: "stuff" }) + end + + it "does not match events with different context" do + Sentry.set_context("rails.error", { some: "stuff" }) + Sentry.capture_message("Ooops") + + expect(sentry_events).not_to include_sentry_event("Ooops") + .with_context("rails.error" => { other: "data" }) + end + + it "matches events with tags" do + Sentry.set_tags(foo: "bar", baz: "qux") + Sentry.capture_message("Ooops") + + expect(sentry_events).to include_sentry_event("Ooops") + .with_tags({ foo: "bar", baz: "qux" }) + end + + it "does not match events with missing tags" do + Sentry.set_tags(foo: "bar") + Sentry.capture_message("Ooops") + + expect(sentry_events).not_to include_sentry_event("Ooops") + .with_tags({ foo: "bar", baz: "qux" }) + end + + it "matches error events with tags and context" do + Sentry.set_tags(foo: "bar", baz: "qux") + Sentry.set_context("rails.error", { some: "stuff" }) + + Sentry.capture_exception(exception) + + expect(sentry_events).to include_sentry_event(exception: exception.class, message: exception.message) + .with_tags({ foo: "bar", baz: "qux" }) + .with_context("rails.error" => { some: "stuff" }) + end + + it "matches error events with tags and context provided as arguments" do + Sentry.set_tags(foo: "bar", baz: "qux") + Sentry.set_context("rails.error", { some: "stuff" }) + + Sentry.capture_exception(exception) + + expect(sentry_events).to include_sentry_event( + exception: exception.class, + message: exception.message, + tags: { foo: "bar", baz: "qux" }, + context: { "rails.error" => { some: "stuff" } } + ) + end + + it "produces a useful failure message" do + Sentry.capture_message("Actual message") + + expect { + expect(sentry_events).to include_sentry_event("Expected message") + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) do |error| + expect(error.message).to include("Failed to find event matching:") + expect(error.message).to include("message: \"Expected message\"") + expect(error.message).to include("Captured events:") + expect(error.message).to include("\"message\": \"Actual message\"") + end + end + end +end diff --git a/sentry-ruby/spec/sentry/scope/setters_spec.rb b/sentry-ruby/spec/sentry/scope/setters_spec.rb index 3ca240488..39a2e25c1 100644 --- a/sentry-ruby/spec/sentry/scope/setters_spec.rb +++ b/sentry-ruby/spec/sentry/scope/setters_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Scope do @@ -7,7 +9,7 @@ new_breadcrumb end - describe "#set_rack_env", rack: true do + describe "#set_rack_env", when: :rack_available? do let(:env) do Rack::MockRequest.env_for("/test", {}) end diff --git a/sentry-ruby/spec/sentry/scope_spec.rb b/sentry-ruby/spec/sentry/scope_spec.rb index 651c995d1..4bacae275 100644 --- a/sentry-ruby/spec/sentry/scope_spec.rb +++ b/sentry-ruby/spec/sentry/scope_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Scope do @@ -201,6 +203,7 @@ scope.set_user({ id: 1 }) scope.set_transaction_name("WelcomeController#index", source: :view) scope.set_fingerprint(["foo"]) + scope.add_attachment(bytes: "file-data", filename: "test.txt") scope end @@ -219,6 +222,10 @@ expect(event.contexts).to include(:trace) expect(event.contexts[:os].keys).to match_array([:name, :version, :build, :kernel_version, :machine]) expect(event.contexts.dig(:runtime, :version)).to match(/ruby/) + + attachment = event.attachments.first + expect(attachment.filename).to eql("test.txt") + expect(attachment.bytes).to eql("file-data") end it "does not apply the contextual data to a check-in event" do @@ -288,7 +295,7 @@ end end - it "sets trace context from span if there's a span" do + it "sets trace context and dynamic_sampling_context from span if there's a span" do transaction = Sentry::Transaction.new(op: "foo", hub: hub) subject.set_span(transaction) @@ -296,6 +303,7 @@ expect(event.contexts[:trace]).to eq(transaction.get_trace_context) expect(event.contexts.dig(:trace, :op)).to eq("foo") + expect(event.dynamic_sampling_context).to eq(transaction.get_dynamic_sampling_context) end it "sets trace context and dynamic_sampling_context from propagation context if there's no span" do @@ -304,7 +312,7 @@ expect(event.dynamic_sampling_context).to eq(subject.propagation_context.get_dynamic_sampling_context) end - context "with Rack", rack: true do + context "with Rack", when: :rack_available? do let(:env) do Rack::MockRequest.env_for("/test", {}) end @@ -361,4 +369,23 @@ expect(result).to eq([:foo, :bar]) end end + + describe "#add_attachment" do + before { perform_basic_setup } + + let(:opts) do + { bytes: "file-data", filename: "test.txt" } + end + + subject do + described_class.new + end + + it "adds a new attachment" do + attachment = subject.add_attachment(**opts) + + expect(attachment.bytes).to eq("file-data") + expect(attachment.filename).to eq("test.txt") + end + end end diff --git a/sentry-ruby/spec/sentry/session_flusher_spec.rb b/sentry-ruby/spec/sentry/session_flusher_spec.rb index 0c0fbb4eb..376f71dd4 100644 --- a/sentry-ruby/spec/sentry/session_flusher_spec.rb +++ b/sentry-ruby/spec/sentry/session_flusher_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::SessionFlusher do diff --git a/sentry-ruby/spec/sentry/span_spec.rb b/sentry-ruby/spec/sentry/span_spec.rb index 0bc187eec..0d1fd225c 100644 --- a/sentry-ruby/spec/sentry/span_spec.rb +++ b/sentry-ruby/spec/sentry/span_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Span do @@ -28,12 +30,15 @@ it "returns correct context data" do context = subject.get_trace_context + subject.set_data(:foo, "bar") + expect(context[:op]).to eq("sql.query") expect(context[:description]).to eq("SELECT * FROM users;") expect(context[:status]).to eq("ok") expect(context[:trace_id].length).to eq(32) expect(context[:span_id].length).to eq(16) expect(context[:origin]).to eq('manual') + expect(context[:data]).to eq(foo: "bar") end end @@ -133,6 +138,35 @@ end end + describe "#get_dynamic_sampling_context" do + before do + # because initializing transactions requires an active hub + perform_basic_setup + end + + subject do + baggage = Sentry::Baggage.from_incoming_header( + "other-vendor-value-1=foo;bar;baz, "\ + "sentry-trace_id=771a43a4192642f0b136d5159a501700, "\ + "sentry-public_key=49d0f7386ad645858ae85020e393bef3, "\ + "sentry-sample_rate=0.01337, "\ + "sentry-user_id=Am%C3%A9lie, "\ + "other-vendor-value-2=foo;bar;" + ) + + Sentry::Transaction.new(hub: Sentry.get_current_hub, baggage: baggage).start_child + end + + it "propagates sentry dynamic_sampling_context" do + expect(subject.get_dynamic_sampling_context).to eq({ + "sample_rate" => "0.01337", + "public_key" => "49d0f7386ad645858ae85020e393bef3", + "trace_id" => "771a43a4192642f0b136d5159a501700", + "user_id" => "Amélie" + }) + end + end + describe "#start_child" do before do # because initializing transactions requires an active hub diff --git a/sentry-ruby/spec/sentry/test_helper_spec.rb b/sentry-ruby/spec/sentry/test_helper_spec.rb index 200356661..8303464c8 100644 --- a/sentry-ruby/spec/sentry/test_helper_spec.rb +++ b/sentry-ruby/spec/sentry/test_helper_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::TestHelper do @@ -127,6 +129,12 @@ expect(Sentry.get_current_scope.tags).to eq({}) end + it "clears global processors" do + Sentry.add_global_event_processor { |event| event } + teardown_sentry_test + expect(Sentry::Scope.global_event_processors).to eq([]) + end + context "when the configuration is mutated" do it "rolls back client changes" do Sentry.configuration.environment = "quack" diff --git a/sentry-ruby/spec/sentry/transaction_spec.rb b/sentry-ruby/spec/sentry/transaction_spec.rb index 6dafc1371..7cc83aa64 100644 --- a/sentry-ruby/spec/sentry/transaction_spec.rb +++ b/sentry-ruby/spec/sentry/transaction_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Transaction do @@ -494,6 +496,7 @@ it "records lost event with reason sample_rate" do subject.finish expect(Sentry.get_current_client.transport).to have_recorded_lost_event(:sample_rate, 'transaction') + expect(Sentry.get_current_client.transport).to have_recorded_lost_event(:sample_rate, 'span') end end @@ -514,6 +517,7 @@ subject.finish expect(Sentry.get_current_client.transport).to have_recorded_lost_event(:backpressure, 'transaction') + expect(Sentry.get_current_client.transport).to have_recorded_lost_event(:backpressure, 'span') end end diff --git a/sentry-ruby/spec/sentry/transport/configuration_spec.rb b/sentry-ruby/spec/sentry/transport/configuration_spec.rb index 676a4800c..fa3edd87b 100644 --- a/sentry-ruby/spec/sentry/transport/configuration_spec.rb +++ b/sentry-ruby/spec/sentry/transport/configuration_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Transport::Configuration do diff --git a/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb b/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb index ac83e3b16..30dc42bbb 100644 --- a/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb +++ b/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'contexts/with_request_mock' diff --git a/sentry-ruby/spec/sentry/transport/http_transport_spec.rb b/sentry-ruby/spec/sentry/transport/http_transport_spec.rb index 3c7b1b051..a0973ef75 100644 --- a/sentry-ruby/spec/sentry/transport/http_transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport/http_transport_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'contexts/with_request_mock' diff --git a/sentry-ruby/spec/sentry/transport/spotlight_transport_spec.rb b/sentry-ruby/spec/sentry/transport/spotlight_transport_spec.rb index 04de57af6..fc095efeb 100644 --- a/sentry-ruby/spec/sentry/transport/spotlight_transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport/spotlight_transport_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::SpotlightTransport do diff --git a/sentry-ruby/spec/sentry/transport_spec.rb b/sentry-ruby/spec/sentry/transport_spec.rb index 35ebed86a..059a8109b 100644 --- a/sentry-ruby/spec/sentry/transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Transport do @@ -145,6 +147,37 @@ expect(profile_payload).to eq(profile.to_json) end end + + context "allows bigger item size" do + let(:profile) do + { + environment: "test", + release: "release", + profile: { + frames: Array.new(10000) { |i| { function: "function_#{i}", filename: "file_#{i}", lineno: i } }, + stacks: Array.new(10000) { |i| [i] }, + samples: Array.new(10000) { |i| { stack_id: i, elapsed_since_start_ns: i * 1000, thread_id: i % 10 } } + } + } + end + + let(:event_with_profile) do + event.profile = profile + event + end + + let(:envelope) { subject.envelope_from_event(event_with_profile) } + + it "adds profile item to envelope" do + result, _ = subject.serialize_envelope(envelope) + + _profile_header, profile_payload_json = result.split("\n").last(2) + + profile_payload = JSON.parse(profile_payload_json) + + expect(profile_payload["profile"]).to_not be(nil) + end + end end context "client report" do @@ -153,6 +186,7 @@ before do 5.times { subject.record_lost_event(:ratelimit_backoff, 'error') } 3.times { subject.record_lost_event(:queue_overflow, 'transaction') } + 2.times { subject.record_lost_event(:network_error, 'span', num: 5) } end it "incudes client report in envelope" do @@ -170,7 +204,8 @@ timestamp: Time.now.utc.iso8601, discarded_events: [ { reason: :ratelimit_backoff, category: 'error', quantity: 5 }, - { reason: :queue_overflow, category: 'transaction', quantity: 3 } + { reason: :queue_overflow, category: 'transaction', quantity: 3 }, + { reason: :network_error, category: 'span', quantity: 10 } ] }.to_json ) @@ -248,7 +283,7 @@ let(:in_app_pattern) do project_root = "/fake/project_root" - Regexp.new("^(#{project_root}/)?#{Sentry::Backtrace::APP_DIRS_PATTERN}") + Regexp.new("^(#{project_root}/)?#{Sentry::Configuration::APP_DIRS_PATTERN}") end let(:frame_list_limit) { 500 } let(:frame_list_size) { frame_list_limit * 20 } @@ -433,6 +468,24 @@ end end end + + context "event with attachments" do + let(:event) { client.event_from_exception(ZeroDivisionError.new("divided by 0")) } + let(:envelope) { subject.envelope_from_event(event) } + + before do + event.attachments << Sentry::Attachment.new(filename: "test-1.txt", bytes: "test") + event.attachments << Sentry::Attachment.new(path: fixture_path("attachment.txt")) + end + + it "sends the event and logs the action" do + expect(subject).to receive(:send_data) + + subject.send_envelope(envelope) + + expect(io.string).to match(/Sending envelope with items \[event, attachment, attachment\]/) + end + end end describe "#send_event" do diff --git a/sentry-ruby/spec/sentry/utils/real_ip_spec.rb b/sentry-ruby/spec/sentry/utils/real_ip_spec.rb index 7aa6c7559..42197b9f2 100644 --- a/sentry-ruby/spec/sentry/utils/real_ip_spec.rb +++ b/sentry-ruby/spec/sentry/utils/real_ip_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Utils::RealIp do diff --git a/sentry-ruby/spec/sentry/utils/request_id_spec.rb b/sentry-ruby/spec/sentry/utils/request_id_spec.rb index 7fa7a7301..955bd8894 100644 --- a/sentry-ruby/spec/sentry/utils/request_id_spec.rb +++ b/sentry-ruby/spec/sentry/utils/request_id_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Utils::RequestId do diff --git a/sentry-ruby/spec/sentry/vernier/profiler_spec.rb b/sentry-ruby/spec/sentry/vernier/profiler_spec.rb new file mode 100644 index 000000000..46a696fee --- /dev/null +++ b/sentry-ruby/spec/sentry/vernier/profiler_spec.rb @@ -0,0 +1,325 @@ +# frozen_string_literal: true + +require "spec_helper" + +require "sentry/vernier/profiler" + +RSpec.describe Sentry::Vernier::Profiler, when: { ruby_version?: [:>=, "3.2.1"] } do + subject(:profiler) { described_class.new(Sentry.configuration) } + + before do + # TODO: replace with some public API once available + Vernier.stop_profile if Vernier.instance_variable_get(:@collector) + + perform_basic_setup do |config| + config.traces_sample_rate = traces_sample_rate + config.profiles_sample_rate = profiles_sample_rate + config.app_dirs_pattern = %r{spec/support} + end + end + + let(:profiles_sample_rate) { 1.0 } + let(:traces_sample_rate) { 1.0 } + + describe '#start' do + context "when profiles_sample_rate is 0" do + let(:profiles_sample_rate) { 0.0 } + + it "does not start Vernier" do + profiler.set_initial_sample_decision(true) + + expect(Vernier).not_to receive(:start_profile) + profiler.start + expect(profiler.started).to eq(false) + end + end + + context "when profiles_sample_rate is between 0.0 and 1.0" do + let(:profiles_sample_rate) { 0.4 } + + it "randomizes profiling" do + profiler.set_initial_sample_decision(true) + + expect([nil, true]).to include(profiler.start) + end + end + + context "when traces_sample_rate is nil" do + let(:traces_sample_rate) { nil } + + it "does not start Vernier" do + profiler.set_initial_sample_decision(true) + + expect(Vernier).not_to receive(:start_profile) + profiler.start + expect(profiler.started).to eq(false) + end + end + + context 'without sampling decision' do + it 'does not start Vernier' do + expect(Vernier).not_to receive(:start_profile) + profiler.start + expect(profiler.started).to eq(false) + end + + it 'does not start Vernier if not sampled' do + expect(Vernier).not_to receive(:start_profile) + profiler.start + expect(profiler.started).to eq(false) + end + end + + context 'with sampling decision' do + before do + profiler.set_initial_sample_decision(true) + end + + it 'starts Vernier if sampled' do + expect(Vernier).to receive(:start_profile).and_return(true) + + profiler.start + + expect(profiler.started).to eq(true) + end + + it 'does not start Vernier again if already started' do + expect(Vernier).to receive(:start_profile).and_return(true).once + + profiler.start + profiler.start + + expect(profiler.started).to be(true) + end + end + + context "when Vernier crashes" do + it "logs the error and does not raise" do + profiler.set_initial_sample_decision(true) + + expect(Vernier).to receive(:start_profile).and_raise("boom") + + expect { profiler.start }.to_not raise_error("boom") + end + + it "doesn't start if Vernier raises that it already started" do + profiler.set_initial_sample_decision(true) + + expect(Vernier).to receive(:start_profile).and_raise(RuntimeError.new("Profile already started")) + + profiler.start + + expect(profiler.started).to eq(false) + end + end + end + + describe '#stop' do + it 'does not stop Vernier if not sampled' do + profiler.set_initial_sample_decision(false) + expect(Vernier).not_to receive(:stop_profile) + profiler.stop + end + + it 'does not stop Vernier if sampled but not started' do + profiler.set_initial_sample_decision(true) + expect(Vernier).not_to receive(:stop_profile) + profiler.stop + end + + it 'stops Vernier if sampled and started' do + profiler.set_initial_sample_decision(true) + profiler.start + expect(Vernier).to receive(:stop_profile) + profiler.stop + end + + it 'does not crash when Vernier was already stopped' do + profiler.set_initial_sample_decision(true) + profiler.start + Vernier.stop_profile + profiler.stop + end + + it 'does not crash when stopping Vernier crashed' do + profiler.set_initial_sample_decision(true) + profiler.start + expect(Vernier).to receive(:stop_profile).and_raise(RuntimeError.new("Profile not started")) + profiler.stop + end + end + + describe "#to_hash" do + let (:transport) { Sentry.get_current_client.transport } + + + it "records lost event if not sampled" do + expect(transport).to receive(:record_lost_event).with(:sample_rate, "profile") + + profiler.set_initial_sample_decision(true) + profiler.start + profiler.set_initial_sample_decision(false) + + expect(profiler.to_hash).to eq({}) + end + end + + context 'with sampling decision' do + before do + profiler.set_initial_sample_decision(true) + end + + describe '#to_hash' do + it "returns empty hash if not started" do + expect(profiler.to_hash).to eq({}) + end + + context 'with single-thread profiled code' do + before do + profiler.start + ProfilerTest::Bar.bar + profiler.stop + end + + it 'has correct frames' do + frames = profiler.to_hash[:profile][:frames] + + foo_frame = frames.find { |f| f[:function] =~ /foo/ } + + expect(foo_frame[:function]).to eq('Foo.foo') + expect(foo_frame[:module]).to eq('ProfilerTest::Bar') + expect(foo_frame[:in_app]).to eq(true) + expect(foo_frame[:lineno]).to eq(6) + expect(foo_frame[:filename]).to eq('spec/support/profiler.rb') + expect(foo_frame[:abs_path]).to include('sentry-ruby/sentry-ruby/spec/support/profiler.rb') + end + + it 'has correct stacks' do + profile = profiler.to_hash[:profile] + frames = profile[:frames] + stacks = profile[:stacks] + + stack_tops = stacks.map { |s| s.take(3) }.map { |s| s.map { |i| frames[i][:function] } } + + expect(stack_tops.any? { |tops| tops.include?("Foo.foo") }).to be(true) + expect(stack_tops.any? { |tops| tops.include?("Bar.bar") }).to be(true) + expect(stack_tops.any? { |tops| tops.include?("Integer#times") }).to be(true) + + stacks.each do |stack| + stack.each do |frame_idx| + expect(frames[frame_idx][:function]).to be_a(String) + end + end + end + + it 'has correct samples' do + profile = profiler.to_hash[:profile] + samples = profile[:samples] + last_elapsed = 0 + + samples.group_by { |sample| sample[:thread_id] }.each do |thread_id, thread_samples| + expect(thread_id.to_i).to be > 0 + + last_elapsed = 0 + + thread_samples.each do |sample| + expect(sample[:stack_id]).to be > 0 + + elapsed = sample[:elapsed_since_start_ns].to_i + + expect(elapsed).to be > 0.0 + expect(elapsed).to be > last_elapsed + + last_elapsed = elapsed + end + end + end + end + + context 'with multi-thread profiled code' do + before do + profiler.start + + 2.times.map do |i| + Thread.new do + Thread.current.name = "thread-bar-#{i}" + + ProfilerTest::Bar.bar + end + end.map(&:join) + + profiler.stop + end + + it "has correct thread metadata" do + thread_metadata = profiler.to_hash[:profile][:thread_metadata] + + main_thread = thread_metadata.values.find { |metadata| metadata[:name].include?("rspec") } + thread1 = thread_metadata.values.find { |metadata| metadata[:name] == "thread-bar-0" } + thread2 = thread_metadata.values.find { |metadata| metadata[:name] == "thread-bar-1" } + + thread_metadata.each do |thread_id, metadata| + expect(thread_id.to_i).to be > 0 + end + + expect(main_thread[:name]).to include("rspec") + expect(thread1[:name]).to eq("thread-bar-0") + expect(thread2[:name]).to eq("thread-bar-1") + end + + it 'has correct frames', when: { ruby_version?: [:>=, "3.3"] } do + frames = profiler.to_hash[:profile][:frames] + + foo_frame = frames.find { |f| f[:function] =~ /foo/ } + + expect(foo_frame[:function]).to eq('Foo.foo') + expect(foo_frame[:module]).to eq('ProfilerTest::Bar') + expect(foo_frame[:in_app]).to eq(true) + expect(foo_frame[:lineno]).to eq(6) + expect(foo_frame[:filename]).to eq('spec/support/profiler.rb') + expect(foo_frame[:abs_path]).to include('sentry-ruby/sentry-ruby/spec/support/profiler.rb') + end + + it 'has correct stacks', when: { ruby_version?: [:>=, "3.3"] } do + profile = profiler.to_hash[:profile] + frames = profile[:frames] + stacks = profile[:stacks] + + stack_tops = stacks.map { |s| s.take(3) }.map { |s| s.map { |i| frames[i][:function] } } + + expect(stack_tops.any? { |tops| tops.include?("Foo.foo") }).to be(true) + expect(stack_tops.any? { |tops| tops.include?("Bar.bar") }).to be(true) + expect(stack_tops.any? { |tops| tops.include?("Integer#times") }).to be(true) + + stacks.each do |stack| + stack.each do |frame_idx| + expect(frames[frame_idx][:function]).to be_a(String) + end + end + end + + it 'has correct samples' do + profile = profiler.to_hash[:profile] + samples = profile[:samples] + + samples.group_by { |sample| sample[:thread_id] }.each do |thread_id, thread_samples| + expect(thread_id.to_i).to be > 0 + + last_elapsed = 0 + + thread_samples.each do |sample| + expect(sample[:stack_id]).to be > 0 + + elapsed = sample[:elapsed_since_start_ns].to_i + + expect(elapsed).to be > 0.0 + expect(elapsed).to be > last_elapsed + + last_elapsed = elapsed + end + end + end + end + end + end +end diff --git a/sentry-ruby/spec/sentry_spec.rb b/sentry-ruby/spec/sentry_spec.rb index 0e30d150e..9a188e9f3 100644 --- a/sentry-ruby/spec/sentry_spec.rb +++ b/sentry-ruby/spec/sentry_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require 'contexts/with_request_mock' @@ -713,6 +715,57 @@ end end + describe ".add_attachment" do + it "adds a new attachment to the current scope with provided filename and bytes" do + described_class.add_attachment(filename: "test.txt", bytes: "test") + + expect(described_class.get_current_scope.attachments.size).to be(1) + + attachment = described_class.get_current_scope.attachments.first + expect(attachment.filename).to eq("test.txt") + expect(attachment.bytes).to eq("test") + end + + it "adds a new attachment to the current scope with provided path to a file" do + described_class.add_attachment(path: fixture_path("attachment.txt")) + + expect(described_class.get_current_scope.attachments.size).to be(1) + + attachment = described_class.get_current_scope.attachments.first + expect(attachment.filename).to eq("attachment.txt") + expect(attachment.payload).to eq("hello world\n") + end + + it "adds a new attachment to the current scope favoring bytes over path" do + described_class.add_attachment(path: fixture_path("attachment.txt"), bytes: "test", content_type: "text/plain") + + expect(described_class.get_current_scope.attachments.size).to be(1) + + attachment = described_class.get_current_scope.attachments.first + expect(attachment.filename).to eq("attachment.txt") + expect(attachment.content_type).to eq("text/plain") + expect(attachment.payload).to eq("test") + end + + it "raises meaningful error when path is invalid" do + described_class.add_attachment(path: "/not-here/oops") + + expect(described_class.get_current_scope.attachments.size).to be(1) + + attachment = described_class.get_current_scope.attachments.first + + expect { attachment.payload } + .to raise_error( + Sentry::Attachment::PathNotFoundError, + "Failed to read attachment file, file not found: /not-here/oops" + ) + end + + it "requires either filename or path" do + expect { described_class.add_attachment(bytes: "test") }.to raise_error(ArgumentError, "filename or path is required") + end + end + describe ".csp_report_uri" do it "returns the csp_report_uri generated from the main Configuration" do expect(Sentry.configuration).to receive(:csp_report_uri).and_call_original @@ -1182,5 +1235,15 @@ def foo; end expect(target_class.instance_methods).to include(:foo) end end + + context "with patch and block" do + it "raises error" do + expect do + described_class.register_patch(:bad_patch, module_patch, target_class) do + target_class.send(:prepend, module_patch) + end + end.to raise_error(ArgumentError, "Please provide either a patch and its target OR a block, but not both") + end + end end end diff --git a/sentry-ruby/spec/spec_helper.rb b/sentry-ruby/spec/spec_helper.rb index 1d4a6ff15..e0d1c9a1e 100644 --- a/sentry-ruby/spec/spec_helper.rb +++ b/sentry-ruby/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler/setup" begin require "debug/prelude" @@ -8,6 +10,7 @@ require "rspec/retry" require "redis" require "stackprof" unless RUBY_PLATFORM == "java" +require "vernier" unless RUBY_PLATFORM == "java" || RUBY_VERSION < "3.2" SimpleCov.start do project_name "sentry-ruby" @@ -23,6 +26,8 @@ require "sentry-ruby" require "sentry/test_helper" +Dir[Pathname(__dir__).join("support/**/*.rb")].sort.each { |f| require f } + RSpec.configure do |config| # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" @@ -46,17 +51,57 @@ ENV.delete('RACK_ENV') end - config.before(:each, rack: true) do - skip("skip rack related tests") unless defined?(Rack) + config.before(:each, when: true) do |example| + guards = + case value = example.metadata[:when] + when Symbol then [value] + when Array then value + when Hash then value.map { |k, v| [k, v].flatten } + else + raise ArgumentError, "Invalid `when` metadata: #{value.inspect}" + end + + skip_examples = guards.any? do |meth, *args| + !TestHelpers.public_send(meth, *args) + end + + skip("Skipping because one or more guards `#{guards.inspect}` returned false") if skip_examples end - RSpec::Matchers.define :have_recorded_lost_event do |reason, data_category| + RSpec::Matchers.define :have_recorded_lost_event do |reason, data_category, num: 1| match do |transport| - expect(transport.discarded_events[[reason, data_category]]).to be > 0 + expect(transport.discarded_events[[reason, data_category]]).to eq(num) end end end +module TestHelpers + def self.stack_prof_installed? + defined?(StackProf) + end + + def self.vernier_installed? + require "sentry/vernier/profiler" + defined?(::Vernier) + end + + def self.rack_available? + defined?(Rack) + end + + def self.ruby_version?(op, version) + RUBY_VERSION.public_send(op, version) + end +end + +def fixtures_root + @fixtures_root ||= Pathname(__dir__).join("fixtures") +end + +def fixture_path(name) + fixtures_root.join(name).realpath +end + def build_exception_with_cause(cause = "exception a") begin raise cause diff --git a/sentry-ruby/spec/support/Rakefile.rb b/sentry-ruby/spec/support/Rakefile.rb index 2304ba3a4..77b245297 100644 --- a/sentry-ruby/spec/support/Rakefile.rb +++ b/sentry-ruby/spec/support/Rakefile.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rake" require "sentry-ruby" diff --git a/sentry-ruby/spec/support/profiler.rb b/sentry-ruby/spec/support/profiler.rb new file mode 100644 index 000000000..ee83cc4dd --- /dev/null +++ b/sentry-ruby/spec/support/profiler.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module ProfilerTest + module Bar + module Foo + def self.foo + 1e6.to_i.times { 2**2 } + end + end + + def self.bar + Foo.foo + sleep 0.1 + end + end +end diff --git a/sentry-ruby/spec/support/stackprof_results.json b/sentry-ruby/spec/support/stackprof_results.json index 1527df8cd..712c3b768 100644 --- a/sentry-ruby/spec/support/stackprof_results.json +++ b/sentry-ruby/spec/support/stackprof_results.json @@ -1 +1,718 @@ -{"version":1.2,"mode":"wall","interval":9900.990099009901,"samples":15,"gc_samples":0,"missed_samples":0,"metadata":{},"frames":{"140370219074600":{"name":"Integer#times","file":"","line":null,"total_samples":5,"samples":2},"140370222379560":{"name":"Bar::Foo.foo","file":"/spec/sentry/profiler_spec.rb","line":7,"total_samples":5,"samples":2},"140370222379480":{"name":"Bar.bar","file":"/spec/sentry/profiler_spec.rb","line":12,"total_samples":15,"samples":0},"140370262463760":{"name":"block (4 levels) in ","file":"/spec/sentry/profiler_spec.rb","line":169,"total_samples":15,"samples":0},"140370219081000":{"name":"BasicObject#instance_exec","file":"","line":null,"total_samples":15,"samples":0},"140369934628760":{"name":"RSpec::Core::Example#instance_exec","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb","line":456,"total_samples":15,"samples":0},"140370262209000":{"name":"RSpec::Core::Hooks::BeforeHook#run","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb","line":364,"total_samples":15,"samples":0},"140370262206960":{"name":"RSpec::Core::Hooks::HookCollections#run_owned_hooks_for","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb","line":527,"total_samples":15,"samples":0},"140370219016480":{"name":"Array#each","file":"","line":null,"total_samples":15,"samples":0},"140370262206440":{"name":"RSpec::Core::Hooks::HookCollections#run_example_hooks_for","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb","line":613,"total_samples":15,"samples":0},"140370219016360":{"name":"Array#reverse_each","file":"","line":null,"total_samples":15,"samples":0},"140370262207720":{"name":"RSpec::Core::Hooks::HookCollections#run","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb","line":475,"total_samples":15,"samples":0},"140369934628120":{"name":"RSpec::Core::Example#run_before_example","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb","line":503,"total_samples":15,"samples":0},"140369934656240":{"name":"RSpec::Core::Example#run","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb","line":246,"total_samples":15,"samples":0},"140369934628080":{"name":"RSpec::Core::Example#with_around_and_singleton_context_hooks","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb","line":508,"total_samples":15,"samples":0},"140369934628640":{"name":"RSpec::Core::Example#with_around_example_hooks","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb","line":466,"total_samples":15,"samples":0},"140370262206400":{"name":"RSpec::Core::Hooks::HookCollections#run_around_example_hooks_for","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb","line":619,"total_samples":15,"samples":0},"140369934629600":{"name":"RSpec::Core::Example::Procsy#call","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb","line":350,"total_samples":15,"samples":0},"140369936132760":{"name":"RSpec::Retry#run","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-retry-0.6.2/lib/rspec/retry.rb","line":107,"total_samples":15,"samples":0},"140370203515000":{"name":"Kernel#loop","file":"","line":null,"total_samples":15,"samples":0},"140369936134000":{"name":"RSpec::Core::Example::Procsy#run_with_retry","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-retry-0.6.2/lib/rspec_ext/rspec_ext.rb","line":11,"total_samples":15,"samples":0},"140369936133560":{"name":"RSpec::Retry.setup","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-retry-0.6.2/lib/rspec/retry.rb","line":7,"total_samples":15,"samples":0},"140370262208320":{"name":"RSpec::Core::Hooks::AroundHook#execute_with","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb","line":389,"total_samples":15,"samples":0},"140369934629640":{"name":"RSpec::Core::Example::Procsy#call","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb","line":350,"total_samples":15,"samples":0},"140370221599800":{"name":"RSpec::Core::ExampleGroup.run_examples","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb","line":641,"total_samples":15,"samples":0},"140370219015440":{"name":"Array#map","file":"","line":null,"total_samples":15,"samples":0},"140370221599880":{"name":"RSpec::Core::ExampleGroup.run","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb","line":599,"total_samples":15,"samples":0},"140369934821040":{"name":"RSpec::Core::Runner#run_specs","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb","line":113,"total_samples":15,"samples":0},"140370221475080":{"name":"RSpec::Core::Configuration#with_suite_hooks","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/configuration.rb","line":2062,"total_samples":15,"samples":0},"140370262307680":{"name":"RSpec::Core::Reporter#report","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/reporter.rb","line":71,"total_samples":15,"samples":0},"140369934821120":{"name":"RSpec::Core::Runner#run","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb","line":85,"total_samples":15,"samples":0},"140369934821200":{"name":"RSpec::Core::Runner.run","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb","line":64,"total_samples":15,"samples":0},"140369934821320":{"name":"RSpec::Core::Runner.invoke","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb","line":43,"total_samples":15,"samples":0},"140369934947600":{"name":"","file":"/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/exe/rspec","total_samples":15,"samples":0},"140370220922120":{"name":"Kernel#load","file":"","line":null,"total_samples":15,"samples":0},"140369934788840":{"name":"
","file":"/Users/neel/.rvm/gems/ruby-3.0.0/bin/rspec","line":1,"total_samples":15,"samples":0},"140370219331600":{"name":"Kernel#eval","file":"","line":null,"total_samples":15,"samples":0},"140369933326520":{"name":"
","file":"/Users/neel/.rvm/gems/ruby-3.0.0/bin/ruby_executable_hooks","total_samples":15,"samples":0},"140370218844600":{"name":"
","file":"/Users/neel/.rvm/gems/ruby-3.0.0/bin/ruby_executable_hooks","total_samples":15,"samples":0},"140370219073720":{"name":"Integer#**","file":"","line":null,"total_samples":1,"samples":1},"140370265410040":{"name":"Kernel#sleep","file":"","line":null,"total_samples":10,"samples":10}},"raw":[62,140370218844600,140369933326520,140370219331600,140369934788840,140370220922120,140369934947600,140369934821320,140369934821200,140369934821120,140369934821040,140370262307680,140369934821040,140370221475080,140369934821040,140370219015440,140369934821040,140370221599880,140370219015440,140370221599880,140370221599880,140370219015440,140370221599880,140370221599880,140370221599800,140370219015440,140370221599800,140369934656240,140369934628080,140369934628640,140370262207720,140370262206400,140369934629640,140370262206400,140370262208320,140369934628760,140370219081000,140369936133560,140369936134000,140369936132760,140370203515000,140369936132760,140369934629600,140370262206400,140370262207720,140369934628640,140369934628080,140369934656240,140369934628120,140370262207720,140370262206440,140370219016360,140370262206440,140370262206960,140370219016480,140370262206960,140370262209000,140369934628760,140370219081000,140370262463760,140370222379480,140370222379560,140370219074600,1,63,140370218844600,140369933326520,140370219331600,140369934788840,140370220922120,140369934947600,140369934821320,140369934821200,140369934821120,140369934821040,140370262307680,140369934821040,140370221475080,140369934821040,140370219015440,140369934821040,140370221599880,140370219015440,140370221599880,140370221599880,140370219015440,140370221599880,140370221599880,140370221599800,140370219015440,140370221599800,140369934656240,140369934628080,140369934628640,140370262207720,140370262206400,140369934629640,140370262206400,140370262208320,140369934628760,140370219081000,140369936133560,140369936134000,140369936132760,140370203515000,140369936132760,140369934629600,140370262206400,140370262207720,140369934628640,140369934628080,140369934656240,140369934628120,140370262207720,140370262206440,140370219016360,140370262206440,140370262206960,140370219016480,140370262206960,140370262209000,140369934628760,140370219081000,140370262463760,140370222379480,140370222379560,140370219074600,140370222379560,1,62,140370218844600,140369933326520,140370219331600,140369934788840,140370220922120,140369934947600,140369934821320,140369934821200,140369934821120,140369934821040,140370262307680,140369934821040,140370221475080,140369934821040,140370219015440,140369934821040,140370221599880,140370219015440,140370221599880,140370221599880,140370219015440,140370221599880,140370221599880,140370221599800,140370219015440,140370221599800,140369934656240,140369934628080,140369934628640,140370262207720,140370262206400,140369934629640,140370262206400,140370262208320,140369934628760,140370219081000,140369936133560,140369936134000,140369936132760,140370203515000,140369936132760,140369934629600,140370262206400,140370262207720,140369934628640,140369934628080,140369934656240,140369934628120,140370262207720,140370262206440,140370219016360,140370262206440,140370262206960,140370219016480,140370262206960,140370262209000,140369934628760,140370219081000,140370262463760,140370222379480,140370222379560,140370219074600,1,64,140370218844600,140369933326520,140370219331600,140369934788840,140370220922120,140369934947600,140369934821320,140369934821200,140369934821120,140369934821040,140370262307680,140369934821040,140370221475080,140369934821040,140370219015440,140369934821040,140370221599880,140370219015440,140370221599880,140370221599880,140370219015440,140370221599880,140370221599880,140370221599800,140370219015440,140370221599800,140369934656240,140369934628080,140369934628640,140370262207720,140370262206400,140369934629640,140370262206400,140370262208320,140369934628760,140370219081000,140369936133560,140369936134000,140369936132760,140370203515000,140369936132760,140369934629600,140370262206400,140370262207720,140369934628640,140369934628080,140369934656240,140369934628120,140370262207720,140370262206440,140370219016360,140370262206440,140370262206960,140370219016480,140370262206960,140370262209000,140369934628760,140370219081000,140370262463760,140370222379480,140370222379560,140370219074600,140370222379560,140370219073720,1,63,140370218844600,140369933326520,140370219331600,140369934788840,140370220922120,140369934947600,140369934821320,140369934821200,140369934821120,140369934821040,140370262307680,140369934821040,140370221475080,140369934821040,140370219015440,140369934821040,140370221599880,140370219015440,140370221599880,140370221599880,140370219015440,140370221599880,140370221599880,140370221599800,140370219015440,140370221599800,140369934656240,140369934628080,140369934628640,140370262207720,140370262206400,140369934629640,140370262206400,140370262208320,140369934628760,140370219081000,140369936133560,140369936134000,140369936132760,140370203515000,140369936132760,140369934629600,140370262206400,140370262207720,140369934628640,140369934628080,140369934656240,140369934628120,140370262207720,140370262206440,140370219016360,140370262206440,140370262206960,140370219016480,140370262206960,140370262209000,140369934628760,140370219081000,140370262463760,140370222379480,140370222379560,140370219074600,140370222379560,1,61,140370218844600,140369933326520,140370219331600,140369934788840,140370220922120,140369934947600,140369934821320,140369934821200,140369934821120,140369934821040,140370262307680,140369934821040,140370221475080,140369934821040,140370219015440,140369934821040,140370221599880,140370219015440,140370221599880,140370221599880,140370219015440,140370221599880,140370221599880,140370221599800,140370219015440,140370221599800,140369934656240,140369934628080,140369934628640,140370262207720,140370262206400,140369934629640,140370262206400,140370262208320,140369934628760,140370219081000,140369936133560,140369936134000,140369936132760,140370203515000,140369936132760,140369934629600,140370262206400,140370262207720,140369934628640,140369934628080,140369934656240,140369934628120,140370262207720,140370262206440,140370219016360,140370262206440,140370262206960,140370219016480,140370262206960,140370262209000,140369934628760,140370219081000,140370262463760,140370222379480,140370265410040,10],"raw_sample_timestamps":[1295905330989,1295905338595,1295905348330,1295905358336,1295905368777,1295905379129,1295905388495,1295905398492,1295905407835,1295905418826,1295905428669,1295905438609,1295905447579,1295905458008,1295905468407],"raw_timestamp_deltas":[12403,7531,9689,9986,10420,10338,9320,9949,9269,10920,9770,9864,8899,10354,10323]} +{ + "version": 1.2, + "mode": "wall", + "interval": 9900.990099009901, + "samples": 15, + "gc_samples": 0, + "missed_samples": 0, + "metadata": {}, + "frames": { + "140370219074600": { + "name": "Integer#times", + "file": "", + "line": null, + "total_samples": 5, + "samples": 2 + }, + "140370222379560": { + "name": "Bar::Foo.foo", + "file": "/spec/sentry/profiler_spec.rb", + "line": 7, + "total_samples": 5, + "samples": 2 + }, + "140370222379480": { + "name": "Bar.bar", + "file": "/spec/sentry/profiler_spec.rb", + "line": 12, + "total_samples": 15, + "samples": 0 + }, + "140370262463760": { + "name": "block (4 levels) in ", + "file": "/spec/sentry/profiler_spec.rb", + "line": 169, + "total_samples": 15, + "samples": 0 + }, + "140370219081000": { + "name": "BasicObject#instance_exec", + "file": "", + "line": null, + "total_samples": 15, + "samples": 0 + }, + "140369934628760": { + "name": "RSpec::Core::Example#instance_exec", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb", + "line": 456, + "total_samples": 15, + "samples": 0 + }, + "140370262209000": { + "name": "RSpec::Core::Hooks::BeforeHook#run", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb", + "line": 364, + "total_samples": 15, + "samples": 0 + }, + "140370262206960": { + "name": "RSpec::Core::Hooks::HookCollections#run_owned_hooks_for", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb", + "line": 527, + "total_samples": 15, + "samples": 0 + }, + "140370219016480": { + "name": "Array#each", + "file": "", + "line": null, + "total_samples": 15, + "samples": 0 + }, + "140370262206440": { + "name": "RSpec::Core::Hooks::HookCollections#run_example_hooks_for", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb", + "line": 613, + "total_samples": 15, + "samples": 0 + }, + "140370219016360": { + "name": "Array#reverse_each", + "file": "", + "line": null, + "total_samples": 15, + "samples": 0 + }, + "140370262207720": { + "name": "RSpec::Core::Hooks::HookCollections#run", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb", + "line": 475, + "total_samples": 15, + "samples": 0 + }, + "140369934628120": { + "name": "RSpec::Core::Example#run_before_example", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb", + "line": 503, + "total_samples": 15, + "samples": 0 + }, + "140369934656240": { + "name": "RSpec::Core::Example#run", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb", + "line": 246, + "total_samples": 15, + "samples": 0 + }, + "140369934628080": { + "name": "RSpec::Core::Example#with_around_and_singleton_context_hooks", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb", + "line": 508, + "total_samples": 15, + "samples": 0 + }, + "140369934628640": { + "name": "RSpec::Core::Example#with_around_example_hooks", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb", + "line": 466, + "total_samples": 15, + "samples": 0 + }, + "140370262206400": { + "name": "RSpec::Core::Hooks::HookCollections#run_around_example_hooks_for", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb", + "line": 619, + "total_samples": 15, + "samples": 0 + }, + "140369934629600": { + "name": "RSpec::Core::Example::Procsy#call", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb", + "line": 350, + "total_samples": 15, + "samples": 0 + }, + "140369936132760": { + "name": "RSpec::Retry#run", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-retry-0.6.2/lib/rspec/retry.rb", + "line": 107, + "total_samples": 15, + "samples": 0 + }, + "140370203515000": { + "name": "Kernel#loop", + "file": "", + "line": null, + "total_samples": 15, + "samples": 0 + }, + "140369936134000": { + "name": "RSpec::Core::Example::Procsy#run_with_retry", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-retry-0.6.2/lib/rspec_ext/rspec_ext.rb", + "line": 11, + "total_samples": 15, + "samples": 0 + }, + "140369936133560": { + "name": "RSpec::Retry.setup", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-retry-0.6.2/lib/rspec/retry.rb", + "line": 7, + "total_samples": 15, + "samples": 0 + }, + "140370262208320": { + "name": "RSpec::Core::Hooks::AroundHook#execute_with", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb", + "line": 389, + "total_samples": 15, + "samples": 0 + }, + "140369934629640": { + "name": "RSpec::Core::Example::Procsy#call", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example.rb", + "line": 350, + "total_samples": 15, + "samples": 0 + }, + "140370221599800": { + "name": "RSpec::Core::ExampleGroup.run_examples", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb", + "line": 641, + "total_samples": 15, + "samples": 0 + }, + "140370219015440": { + "name": "Array#map", + "file": "", + "line": null, + "total_samples": 15, + "samples": 0 + }, + "140370221599880": { + "name": "RSpec::Core::ExampleGroup.run", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb", + "line": 599, + "total_samples": 15, + "samples": 0 + }, + "140369934821040": { + "name": "RSpec::Core::Runner#run_specs", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb", + "line": 113, + "total_samples": 15, + "samples": 0 + }, + "140370221475080": { + "name": "RSpec::Core::Configuration#with_suite_hooks", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/configuration.rb", + "line": 2062, + "total_samples": 15, + "samples": 0 + }, + "140370262307680": { + "name": "RSpec::Core::Reporter#report", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/reporter.rb", + "line": 71, + "total_samples": 15, + "samples": 0 + }, + "140369934821120": { + "name": "RSpec::Core::Runner#run", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb", + "line": 85, + "total_samples": 15, + "samples": 0 + }, + "140369934821200": { + "name": "RSpec::Core::Runner.run", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb", + "line": 64, + "total_samples": 15, + "samples": 0 + }, + "140369934821320": { + "name": "RSpec::Core::Runner.invoke", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb", + "line": 43, + "total_samples": 15, + "samples": 0 + }, + "140369934947600": { + "name": "", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/gems/rspec-core-3.11.0/exe/rspec", + "total_samples": 15, + "samples": 0 + }, + "140370220922120": { + "name": "Kernel#load", + "file": "", + "line": null, + "total_samples": 15, + "samples": 0 + }, + "140369934788840": { + "name": "
", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/bin/rspec", + "line": 1, + "total_samples": 15, + "samples": 0 + }, + "140370219331600": { + "name": "Kernel#eval", + "file": "", + "line": null, + "total_samples": 15, + "samples": 0 + }, + "140369933326520": { + "name": "
", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/bin/ruby_executable_hooks", + "total_samples": 15, + "samples": 0 + }, + "140370218844600": { + "name": "
", + "file": "/Users/neel/.rvm/gems/ruby-3.0.0/bin/ruby_executable_hooks", + "total_samples": 15, + "samples": 0 + }, + "140370219073720": { + "name": "Integer#**", + "file": "", + "line": null, + "total_samples": 1, + "samples": 1 + }, + "140370265410040": { + "name": "Kernel#sleep", + "file": "", + "line": null, + "total_samples": 10, + "samples": 10 + } + }, + "raw": [ + 62, + 140370218844600, + 140369933326520, + 140370219331600, + 140369934788840, + 140370220922120, + 140369934947600, + 140369934821320, + 140369934821200, + 140369934821120, + 140369934821040, + 140370262307680, + 140369934821040, + 140370221475080, + 140369934821040, + 140370219015440, + 140369934821040, + 140370221599880, + 140370219015440, + 140370221599880, + 140370221599880, + 140370219015440, + 140370221599880, + 140370221599880, + 140370221599800, + 140370219015440, + 140370221599800, + 140369934656240, + 140369934628080, + 140369934628640, + 140370262207720, + 140370262206400, + 140369934629640, + 140370262206400, + 140370262208320, + 140369934628760, + 140370219081000, + 140369936133560, + 140369936134000, + 140369936132760, + 140370203515000, + 140369936132760, + 140369934629600, + 140370262206400, + 140370262207720, + 140369934628640, + 140369934628080, + 140369934656240, + 140369934628120, + 140370262207720, + 140370262206440, + 140370219016360, + 140370262206440, + 140370262206960, + 140370219016480, + 140370262206960, + 140370262209000, + 140369934628760, + 140370219081000, + 140370262463760, + 140370222379480, + 140370222379560, + 140370219074600, + 1, + 63, + 140370218844600, + 140369933326520, + 140370219331600, + 140369934788840, + 140370220922120, + 140369934947600, + 140369934821320, + 140369934821200, + 140369934821120, + 140369934821040, + 140370262307680, + 140369934821040, + 140370221475080, + 140369934821040, + 140370219015440, + 140369934821040, + 140370221599880, + 140370219015440, + 140370221599880, + 140370221599880, + 140370219015440, + 140370221599880, + 140370221599880, + 140370221599800, + 140370219015440, + 140370221599800, + 140369934656240, + 140369934628080, + 140369934628640, + 140370262207720, + 140370262206400, + 140369934629640, + 140370262206400, + 140370262208320, + 140369934628760, + 140370219081000, + 140369936133560, + 140369936134000, + 140369936132760, + 140370203515000, + 140369936132760, + 140369934629600, + 140370262206400, + 140370262207720, + 140369934628640, + 140369934628080, + 140369934656240, + 140369934628120, + 140370262207720, + 140370262206440, + 140370219016360, + 140370262206440, + 140370262206960, + 140370219016480, + 140370262206960, + 140370262209000, + 140369934628760, + 140370219081000, + 140370262463760, + 140370222379480, + 140370222379560, + 140370219074600, + 140370222379560, + 1, + 62, + 140370218844600, + 140369933326520, + 140370219331600, + 140369934788840, + 140370220922120, + 140369934947600, + 140369934821320, + 140369934821200, + 140369934821120, + 140369934821040, + 140370262307680, + 140369934821040, + 140370221475080, + 140369934821040, + 140370219015440, + 140369934821040, + 140370221599880, + 140370219015440, + 140370221599880, + 140370221599880, + 140370219015440, + 140370221599880, + 140370221599880, + 140370221599800, + 140370219015440, + 140370221599800, + 140369934656240, + 140369934628080, + 140369934628640, + 140370262207720, + 140370262206400, + 140369934629640, + 140370262206400, + 140370262208320, + 140369934628760, + 140370219081000, + 140369936133560, + 140369936134000, + 140369936132760, + 140370203515000, + 140369936132760, + 140369934629600, + 140370262206400, + 140370262207720, + 140369934628640, + 140369934628080, + 140369934656240, + 140369934628120, + 140370262207720, + 140370262206440, + 140370219016360, + 140370262206440, + 140370262206960, + 140370219016480, + 140370262206960, + 140370262209000, + 140369934628760, + 140370219081000, + 140370262463760, + 140370222379480, + 140370222379560, + 140370219074600, + 1, + 64, + 140370218844600, + 140369933326520, + 140370219331600, + 140369934788840, + 140370220922120, + 140369934947600, + 140369934821320, + 140369934821200, + 140369934821120, + 140369934821040, + 140370262307680, + 140369934821040, + 140370221475080, + 140369934821040, + 140370219015440, + 140369934821040, + 140370221599880, + 140370219015440, + 140370221599880, + 140370221599880, + 140370219015440, + 140370221599880, + 140370221599880, + 140370221599800, + 140370219015440, + 140370221599800, + 140369934656240, + 140369934628080, + 140369934628640, + 140370262207720, + 140370262206400, + 140369934629640, + 140370262206400, + 140370262208320, + 140369934628760, + 140370219081000, + 140369936133560, + 140369936134000, + 140369936132760, + 140370203515000, + 140369936132760, + 140369934629600, + 140370262206400, + 140370262207720, + 140369934628640, + 140369934628080, + 140369934656240, + 140369934628120, + 140370262207720, + 140370262206440, + 140370219016360, + 140370262206440, + 140370262206960, + 140370219016480, + 140370262206960, + 140370262209000, + 140369934628760, + 140370219081000, + 140370262463760, + 140370222379480, + 140370222379560, + 140370219074600, + 140370222379560, + 140370219073720, + 1, + 63, + 140370218844600, + 140369933326520, + 140370219331600, + 140369934788840, + 140370220922120, + 140369934947600, + 140369934821320, + 140369934821200, + 140369934821120, + 140369934821040, + 140370262307680, + 140369934821040, + 140370221475080, + 140369934821040, + 140370219015440, + 140369934821040, + 140370221599880, + 140370219015440, + 140370221599880, + 140370221599880, + 140370219015440, + 140370221599880, + 140370221599880, + 140370221599800, + 140370219015440, + 140370221599800, + 140369934656240, + 140369934628080, + 140369934628640, + 140370262207720, + 140370262206400, + 140369934629640, + 140370262206400, + 140370262208320, + 140369934628760, + 140370219081000, + 140369936133560, + 140369936134000, + 140369936132760, + 140370203515000, + 140369936132760, + 140369934629600, + 140370262206400, + 140370262207720, + 140369934628640, + 140369934628080, + 140369934656240, + 140369934628120, + 140370262207720, + 140370262206440, + 140370219016360, + 140370262206440, + 140370262206960, + 140370219016480, + 140370262206960, + 140370262209000, + 140369934628760, + 140370219081000, + 140370262463760, + 140370222379480, + 140370222379560, + 140370219074600, + 140370222379560, + 1, + 61, + 140370218844600, + 140369933326520, + 140370219331600, + 140369934788840, + 140370220922120, + 140369934947600, + 140369934821320, + 140369934821200, + 140369934821120, + 140369934821040, + 140370262307680, + 140369934821040, + 140370221475080, + 140369934821040, + 140370219015440, + 140369934821040, + 140370221599880, + 140370219015440, + 140370221599880, + 140370221599880, + 140370219015440, + 140370221599880, + 140370221599880, + 140370221599800, + 140370219015440, + 140370221599800, + 140369934656240, + 140369934628080, + 140369934628640, + 140370262207720, + 140370262206400, + 140369934629640, + 140370262206400, + 140370262208320, + 140369934628760, + 140370219081000, + 140369936133560, + 140369936134000, + 140369936132760, + 140370203515000, + 140369936132760, + 140369934629600, + 140370262206400, + 140370262207720, + 140369934628640, + 140369934628080, + 140369934656240, + 140369934628120, + 140370262207720, + 140370262206440, + 140370219016360, + 140370262206440, + 140370262206960, + 140370219016480, + 140370262206960, + 140370262209000, + 140369934628760, + 140370219081000, + 140370262463760, + 140370222379480, + 140370265410040, + 10 + ], + "raw_sample_timestamps": [ + 1295905330989, + 1295905338595, + 1295905348330, + 1295905358336, + 1295905368777, + 1295905379129, + 1295905388495, + 1295905398492, + 1295905407835, + 1295905418826, + 1295905428669, + 1295905438609, + 1295905447579, + 1295905458008, + 1295905468407 + ], + "raw_timestamp_deltas": [ + 12403, + 7531, + 9689, + 9986, + 10420, + 10338, + 9320, + 9949, + 9269, + 10920, + 9770, + 9864, + 8899, + 10354, + 10323 + ] +} diff --git a/sentry-ruby/spec/support/stacktrace_test_fixture.rb b/sentry-ruby/spec/support/stacktrace_test_fixture.rb index 7921fb53b..0c8d7933e 100644 --- a/sentry-ruby/spec/support/stacktrace_test_fixture.rb +++ b/sentry-ruby/spec/support/stacktrace_test_fixture.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + def foo bar end diff --git a/sentry-sidekiq/.rubocop.yml b/sentry-sidekiq/.rubocop.yml new file mode 120000 index 000000000..7cc18e076 --- /dev/null +++ b/sentry-sidekiq/.rubocop.yml @@ -0,0 +1 @@ +../.rubocop.yml \ No newline at end of file diff --git a/sentry-sidekiq/Gemfile b/sentry-sidekiq/Gemfile index 5ce453b72..8448e3b77 100644 --- a/sentry-sidekiq/Gemfile +++ b/sentry-sidekiq/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" git_source(:github) { |name| "https://github.com/#{name}.git" } @@ -10,9 +12,6 @@ gem "sentry-rails", path: "../sentry-rails" # loofah changed the required ruby version in a patch so we need to explicitly pin it gem "loofah", "2.20.0" if RUBY_VERSION.to_f < 2.5 -# For https://github.com/ruby/psych/issues/655 -gem "psych", "5.1.0" - sidekiq_version = ENV["SIDEKIQ_VERSION"] sidekiq_version = "7.0" if sidekiq_version.nil? sidekiq_version = Gem::Version.new(sidekiq_version) @@ -26,4 +25,6 @@ end gem "rails", "> 5.0.0" +gem "timecop" + eval_gemfile File.expand_path("../Gemfile", __dir__) diff --git a/sentry-sidekiq/Makefile b/sentry-sidekiq/Makefile index 63010b098..62ad039eb 100644 --- a/sentry-sidekiq/Makefile +++ b/sentry-sidekiq/Makefile @@ -1,7 +1,3 @@ build: bundle install gem build sentry-sidekiq.gemspec - -test: - WITH_SENTRY_RAILS=1 bundle exec rspec spec/sentry/rails_spec.rb - bundle exec rspec diff --git a/sentry-sidekiq/Rakefile b/sentry-sidekiq/Rakefile index 7b2756854..13afab191 100644 --- a/sentry-sidekiq/Rakefile +++ b/sentry-sidekiq/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler/gem_tasks" require "rspec/core/rake_task" diff --git a/sentry-sidekiq/bin/console b/sentry-sidekiq/bin/console index 660c7a889..f0f5a7b6a 100755 --- a/sentry-sidekiq/bin/console +++ b/sentry-sidekiq/bin/console @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require "bundler/setup" require "sentry/ruby" diff --git a/sentry-sidekiq/example/Gemfile b/sentry-sidekiq/example/Gemfile index d038af521..9834edf07 100644 --- a/sentry-sidekiq/example/Gemfile +++ b/sentry-sidekiq/example/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" gem "sidekiq" diff --git a/sentry-sidekiq/example/error_worker.rb b/sentry-sidekiq/example/error_worker.rb index 220d80f63..8782dd2d7 100644 --- a/sentry-sidekiq/example/error_worker.rb +++ b/sentry-sidekiq/example/error_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "sidekiq" require "sentry-sidekiq" diff --git a/sentry-sidekiq/lib/sentry-sidekiq.rb b/sentry-sidekiq/lib/sentry-sidekiq.rb index b7318b9f9..a126782f2 100644 --- a/sentry-sidekiq/lib/sentry-sidekiq.rb +++ b/sentry-sidekiq/lib/sentry-sidekiq.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "sidekiq" require "sentry-ruby" require "sentry/integrable" diff --git a/sentry-sidekiq/lib/sentry/sidekiq/configuration.rb b/sentry-sidekiq/lib/sentry/sidekiq/configuration.rb index cc71908b5..eea6f4c35 100644 --- a/sentry-sidekiq/lib/sentry/sidekiq/configuration.rb +++ b/sentry-sidekiq/lib/sentry/sidekiq/configuration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry class Configuration attr_reader :sidekiq @@ -9,7 +11,10 @@ class Configuration end module Sidekiq - IGNORE_DEFAULT = ["Sidekiq::JobRetry::Skip"] + IGNORE_DEFAULT = [ + "Sidekiq::JobRetry::Skip", + "Sidekiq::JobRetry::Handled" + ] class Configuration # Set this option to true if you want Sentry to only capture the last job diff --git a/sentry-sidekiq/lib/sentry/sidekiq/context_filter.rb b/sentry-sidekiq/lib/sentry/sidekiq/context_filter.rb index 8512b5ab5..ff9f014c3 100644 --- a/sentry-sidekiq/lib/sentry/sidekiq/context_filter.rb +++ b/sentry-sidekiq/lib/sentry/sidekiq/context_filter.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module Sentry module Sidekiq class ContextFilter - ACTIVEJOB_RESERVED_PREFIX_REGEX = /^_aj_/.freeze - SIDEKIQ_NAME = "Sidekiq".freeze + ACTIVEJOB_RESERVED_PREFIX_REGEX = /^_aj_/ + SIDEKIQ_NAME = "Sidekiq" attr_reader :context diff --git a/sentry-sidekiq/lib/sentry/sidekiq/cron/job.rb b/sentry-sidekiq/lib/sentry/sidekiq/cron/job.rb index 4098d7564..954e98311 100644 --- a/sentry-sidekiq/lib/sentry/sidekiq/cron/job.rb +++ b/sentry-sidekiq/lib/sentry/sidekiq/cron/job.rb @@ -12,6 +12,32 @@ module Sentry module Sidekiq module Cron module Job + def self.enqueueing_method + ::Sidekiq::Cron::Job.instance_methods.include?(:enque!) ? :enque! : :enqueue! + end + + define_method(enqueueing_method) do |*args| + # make sure the current thread has a clean hub + Sentry.clone_hub_to_current_thread + + Sentry.with_scope do |scope| + Sentry.with_session_tracking do + begin + scope.set_transaction_name("#{name} (#{klass})") + + transaction = start_transaction(scope) + scope.set_span(transaction) if transaction + super(*args) + + finish_transaction(transaction, 200) + rescue + finish_transaction(transaction, 500) + raise + end + end + end + end + def save # validation failed, do nothing return false unless super @@ -28,12 +54,28 @@ def save unless klass_const.send(:ancestors).include?(Sentry::Cron::MonitorCheckIns) klass_const.send(:include, Sentry::Cron::MonitorCheckIns) klass_const.send(:sentry_monitor_check_ins, - slug: name, - monitor_config: Sentry::Cron::MonitorConfig.from_crontab(cron)) + slug: name.to_s, + monitor_config: Sentry::Cron::MonitorConfig.from_crontab(parsed_cron.original)) end true end + + def start_transaction(scope) + Sentry.start_transaction( + name: scope.transaction_name, + source: scope.transaction_source, + op: "queue.sidekiq-cron", + origin: "auto.queue.sidekiq.cron" + ) + end + + def finish_transaction(transaction, status_code) + return unless transaction + + transaction.set_http_status(status_code) + transaction.finish + end end end end diff --git a/sentry-sidekiq/lib/sentry/sidekiq/error_handler.rb b/sentry-sidekiq/lib/sentry/sidekiq/error_handler.rb index 41fd9e933..64659df1e 100644 --- a/sentry-sidekiq/lib/sentry/sidekiq/error_handler.rb +++ b/sentry-sidekiq/lib/sentry/sidekiq/error_handler.rb @@ -1,4 +1,6 @@ -require 'sentry/sidekiq/context_filter' +# frozen_string_literal: true + +require "sentry/sidekiq/context_filter" module Sentry module Sidekiq @@ -29,6 +31,19 @@ def call(ex, context, sidekiq_config = nil) end end + # Check if the retry count is below the attempt_threshold + attempt_threshold = context.dig(:job, "attempt_threshold") + if attempt_threshold && retryable?(context) + attempt_threshold = attempt_threshold.to_i + retry_count = context.dig(:job, "retry_count") + # attempt 1 - retry_count is nil + # attempt 2 - this is your first retry so retry_count is 0 + # attempt 3 - you have retried once, retry_count is 1 + attempt = retry_count.nil? ? 1 : retry_count.to_i + 2 + + return if attempt < attempt_threshold + end + Sentry::Sidekiq.capture_exception( ex, contexts: { sidekiq: context_filter.filtered }, diff --git a/sentry-sidekiq/lib/sentry/sidekiq/sentry_context_middleware.rb b/sentry-sidekiq/lib/sentry/sidekiq/sentry_context_middleware.rb index 748ca0155..85c115e93 100644 --- a/sentry-sidekiq/lib/sentry/sidekiq/sentry_context_middleware.rb +++ b/sentry-sidekiq/lib/sentry/sidekiq/sentry_context_middleware.rb @@ -1,14 +1,27 @@ # frozen_string_literal: true -require 'sentry/sidekiq/context_filter' +require "sentry/sidekiq/context_filter" module Sentry module Sidekiq + module Helpers + def set_span_data(span, id:, queue:, latency: nil, retry_count: nil) + return unless span + + span.set_data(Span::DataConventions::MESSAGING_MESSAGE_ID, id) + span.set_data(Span::DataConventions::MESSAGING_DESTINATION_NAME, queue) + span.set_data(Span::DataConventions::MESSAGING_MESSAGE_RECEIVE_LATENCY, latency) if latency + span.set_data(Span::DataConventions::MESSAGING_MESSAGE_RETRY_COUNT, retry_count) if retry_count + end + end + class SentryContextServerMiddleware - OP_NAME = "queue.sidekiq" + include Sentry::Sidekiq::Helpers + + OP_NAME = "queue.process" SPAN_ORIGIN = "auto.queue.sidekiq" - def call(_worker, job, queue) + def call(worker, job, queue) return yield unless Sentry.initialized? context_filter = Sentry::Sidekiq::ContextFilter.new(job) @@ -23,7 +36,19 @@ def call(_worker, job, queue) scope.set_contexts(sidekiq: job.merge("queue" => queue)) scope.set_transaction_name(context_filter.transaction_name, source: :task) transaction = start_transaction(scope, job["trace_propagation_headers"]) - scope.set_span(transaction) if transaction + + if transaction + scope.set_span(transaction) + + latency = ((Time.now.to_f - job["enqueued_at"]) * 1000).to_i if job["enqueued_at"] + set_span_data( + transaction, + id: job["jid"], + queue: queue, + latency: latency, + retry_count: job["retry_count"] || 0 + ) + end begin yield @@ -63,13 +88,20 @@ def finish_transaction(transaction, status) end class SentryContextClientMiddleware - def call(_worker_class, job, _queue, _redis_pool) + include Sentry::Sidekiq::Helpers + + def call(worker_class, job, queue, _redis_pool) return yield unless Sentry.initialized? user = Sentry.get_current_scope.user job["sentry_user"] = user unless user.empty? job["trace_propagation_headers"] ||= Sentry.get_trace_propagation_headers - yield + + Sentry.with_child_span(op: "queue.publish", description: worker_class.to_s) do |span| + set_span_data(span, id: job["jid"], queue: queue) + + yield + end end end end diff --git a/sentry-sidekiq/lib/sentry/sidekiq/version.rb b/sentry-sidekiq/lib/sentry/sidekiq/version.rb index a717827f3..e58872566 100644 --- a/sentry-sidekiq/lib/sentry/sidekiq/version.rb +++ b/sentry-sidekiq/lib/sentry/sidekiq/version.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module Sentry module Sidekiq - VERSION = "5.18.0" + VERSION = "5.22.1" end end diff --git a/sentry-sidekiq/sentry-sidekiq.gemspec b/sentry-sidekiq/sentry-sidekiq.gemspec index 837008d5a..924954e58 100644 --- a/sentry-sidekiq/sentry-sidekiq.gemspec +++ b/sentry-sidekiq/sentry-sidekiq.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "lib/sentry/sidekiq/version" Gem::Specification.new do |spec| @@ -7,21 +9,27 @@ Gem::Specification.new do |spec| spec.description = spec.summary = "A gem that provides Sidekiq integration for the Sentry error logger" spec.email = "accounts@sentry.io" spec.license = 'MIT' - spec.homepage = "https://github.com/getsentry/sentry-ruby" spec.platform = Gem::Platform::RUBY spec.required_ruby_version = '>= 2.4' spec.extra_rdoc_files = ["README.md", "LICENSE.txt"] - spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples)'`.split("\n") + spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples|\.rubocop\.yml)'`.split("\n") + + github_root_uri = 'https://github.com/getsentry/sentry-ruby' + spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}" - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = spec.homepage - spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" + spec.metadata = { + "homepage_uri" => spec.homepage, + "source_code_uri" => spec.homepage, + "changelog_uri" => "#{github_root_uri}/blob/#{spec.version}/CHANGELOG.md", + "bug_tracker_uri" => "#{github_root_uri}/issues", + "documentation_uri" => "http://www.rubydoc.info/gems/#{spec.name}/#{spec.version}" + } spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_dependency "sentry-ruby", "~> 5.18.0" + spec.add_dependency "sentry-ruby", "~> 5.22.1" spec.add_dependency "sidekiq", ">= 3.0" end diff --git a/sentry-sidekiq/spec/fixtures/sidekiq-cron-schedule.yml b/sentry-sidekiq/spec/fixtures/sidekiq-cron-schedule.yml index 65b5945a6..fe2798262 100644 --- a/sentry-sidekiq/spec/fixtures/sidekiq-cron-schedule.yml +++ b/sentry-sidekiq/spec/fixtures/sidekiq-cron-schedule.yml @@ -6,6 +6,10 @@ manual: cron: "* * * * *" class: "SadWorkerWithCron" +human_readable_cron: + cron: "every 5 minutes" + class: HappyWorkerWithHumanReadableCron + invalid_cron: cron: "not a crontab" class: "ReportingWorker" diff --git a/sentry-sidekiq/spec/sentry/rails_spec.rb b/sentry-sidekiq/spec/sentry/rails_spec.rb index 903f8ab38..36c6fdd31 100644 --- a/sentry-sidekiq/spec/sentry/rails_spec.rb +++ b/sentry-sidekiq/spec/sentry/rails_spec.rb @@ -1,9 +1,13 @@ +# frozen_string_literal: true + return unless ENV["WITH_SENTRY_RAILS"] require "rails" require "sentry-rails" require "spec_helper" +require "action_controller/railtie" + class TestApp < Rails::Application end diff --git a/sentry-sidekiq/spec/sentry/sidekiq-scheduler/scheduler_spec.rb b/sentry-sidekiq/spec/sentry/sidekiq-scheduler/scheduler_spec.rb index 879e34a55..02efd1ccc 100644 --- a/sentry-sidekiq/spec/sentry/sidekiq-scheduler/scheduler_spec.rb +++ b/sentry-sidekiq/spec/sentry/sidekiq-scheduler/scheduler_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' return unless defined?(SidekiqScheduler::Scheduler) diff --git a/sentry-sidekiq/spec/sentry/sidekiq/configuration_spec.rb b/sentry-sidekiq/spec/sentry/sidekiq/configuration_spec.rb index 2fdcdfbc5..2bcab6c58 100644 --- a/sentry-sidekiq/spec/sentry/sidekiq/configuration_spec.rb +++ b/sentry-sidekiq/spec/sentry/sidekiq/configuration_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Sidekiq::Configuration do @@ -13,6 +15,12 @@ expect(config.excluded_exceptions).to include("Sidekiq::JobRetry::Skip") end + it "adds Sidekiq::JobRetry::Handled to the ignore list" do + config = Sentry::Configuration.new + + expect(config.excluded_exceptions).to include("Sidekiq::JobRetry::Handled") + end + describe "#report_after_job_retries" do it "has correct default value" do expect(subject.report_after_job_retries).to eq(false) diff --git a/sentry-sidekiq/spec/sentry/sidekiq/context_filter_spec.rb b/sentry-sidekiq/spec/sentry/sidekiq/context_filter_spec.rb index dc5354b9c..099ce329b 100644 --- a/sentry-sidekiq/spec/sentry/sidekiq/context_filter_spec.rb +++ b/sentry-sidekiq/spec/sentry/sidekiq/context_filter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe Sentry::Sidekiq::ContextFilter do diff --git a/sentry-sidekiq/spec/sentry/sidekiq/cron/job_spec.rb b/sentry-sidekiq/spec/sentry/sidekiq/cron/job_spec.rb index 44f02f86e..d5d637a7c 100644 --- a/sentry-sidekiq/spec/sentry/sidekiq/cron/job_spec.rb +++ b/sentry-sidekiq/spec/sentry/sidekiq/cron/job_spec.rb @@ -1,15 +1,31 @@ +# frozen_string_literal: true + require 'spec_helper' return unless defined?(Sidekiq::Cron::Job) RSpec.describe Sentry::Sidekiq::Cron::Job do + let(:processor) do + new_processor + end + + let(:transport) do + Sentry.get_current_client.transport + end + before do - perform_basic_setup { |c| c.enabled_patches += [:sidekiq_cron] } + perform_basic_setup do |c| + c.enabled_patches += [:sidekiq_cron] + c.traces_sample_rate = 1.0 + end end before do + Sidekiq::Cron::Job.destroy_all! + Sidekiq::Queue.all.each(&:clear) schedule_file = 'spec/fixtures/sidekiq-cron-schedule.yml' schedule = Sidekiq::Cron::Support.load_yaml(ERB.new(IO.read(schedule_file)).result) + schedule = schedule.merge(symbol_name: { cron: '* * * * *', class: HappyWorkerWithSymbolName }) # sidekiq-cron 2.0+ accepts second argument to `load_from_hash!` with options, # such as {source: 'schedule'}, but sidekiq-cron 1.9.1 (last version to support Ruby 2.6) does not. # Since we're not using the source option in our code anyway, it's safe to not pass the 2nd arg. @@ -17,7 +33,7 @@ end before do - stub_const('Job', Class.new { def perform; end }) + stub_const('Job', Class.new { include Sidekiq::Worker; def perform; end }) end it 'patches class' do @@ -48,6 +64,23 @@ expect(HappyWorkerForCron.sentry_monitor_config.schedule.value).to eq('* * * * *') end + it 'patches HappyWorkerWithHumanReadableCron' do + expect(HappyWorkerWithHumanReadableCron.ancestors).to include(Sentry::Cron::MonitorCheckIns) + expect(HappyWorkerWithHumanReadableCron.sentry_monitor_slug).to eq('human_readable_cron') + expect(HappyWorkerWithHumanReadableCron.sentry_monitor_config).to be_a(Sentry::Cron::MonitorConfig) + expect(HappyWorkerWithHumanReadableCron.sentry_monitor_config.schedule).to be_a(Sentry::Cron::MonitorSchedule::Crontab) + expect(HappyWorkerWithHumanReadableCron.sentry_monitor_config.schedule.value).to eq('*/5 * * * *') + end + + it 'patches HappyWorkerWithSymbolName' do + expect(HappyWorkerWithSymbolName.ancestors).to include(Sentry::Cron::MonitorCheckIns) + expect(HappyWorkerWithSymbolName.sentry_monitor_slug).to eq('symbol_name') + expect(HappyWorkerWithSymbolName.sentry_monitor_config).to be_a(Sentry::Cron::MonitorConfig) + expect(HappyWorkerWithSymbolName.sentry_monitor_config.schedule).to be_a(Sentry::Cron::MonitorSchedule::Crontab) + expect(HappyWorkerWithSymbolName.sentry_monitor_config.schedule.value).to eq('* * * * *') + end + + it 'does not override SadWorkerWithCron manually set values' do expect(SadWorkerWithCron.ancestors).to include(Sentry::Cron::MonitorCheckIns) expect(SadWorkerWithCron.sentry_monitor_slug).to eq('failed_job') @@ -59,4 +92,37 @@ it 'does not patch ReportingWorker because of invalid schedule' do expect(ReportingWorker.ancestors).not_to include(Sentry::Cron::MonitorSchedule) end + + describe 'sidekiq-cron' do + it 'adds job to sidekiq within transaction' do + job = Sidekiq::Cron::Job.new(name: 'test', cron: 'not a crontab', class: 'HappyWorkerForCron') + job.send(Sentry::Sidekiq::Cron::Job.enqueueing_method) + + expect(::Sidekiq::Queue.new.size).to eq(1) + expect(transport.events.count).to eq(1) + event = transport.events.last + expect(event.spans.count).to eq(1) + expect(event.spans[0][:op]).to eq("queue.publish") + expect(event.spans[0][:data]['messaging.destination.name']).to eq('default') + end + + it 'adds job to sidekiq within transaction' do + job = Sidekiq::Cron::Job.new(name: 'test', cron: 'not a crontab', class: 'HappyWorkerForCron') + job.send(Sentry::Sidekiq::Cron::Job.enqueueing_method) + # Time passes. + job.send(Sentry::Sidekiq::Cron::Job.enqueueing_method) + + expect(::Sidekiq::Queue.new.size).to eq(2) + expect(transport.events.count).to eq(2) + events = transport.events + expect(events[0].spans.count).to eq(1) + expect(events[0].spans[0][:op]).to eq("queue.publish") + expect(events[0].spans[0][:data]['messaging.destination.name']).to eq('default') + expect(events[1].spans.count).to eq(1) + expect(events[1].spans[0][:op]).to eq("queue.publish") + expect(events[1].spans[0][:data]['messaging.destination.name']).to eq('default') + + expect(events[0].dynamic_sampling_context['trace_id']).to_not eq(events[1].dynamic_sampling_context['trace_id']) + end + end end diff --git a/sentry-sidekiq/spec/sentry/sidekiq/error_handler_spec.rb b/sentry-sidekiq/spec/sentry/sidekiq/error_handler_spec.rb index 15053609e..5e144bf9a 100644 --- a/sentry-sidekiq/spec/sentry/sidekiq/error_handler_spec.rb +++ b/sentry-sidekiq/spec/sentry/sidekiq/error_handler_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::Sidekiq::ErrorHandler do diff --git a/sentry-sidekiq/spec/sentry/sidekiq/sentry_context_middleware_spec.rb b/sentry-sidekiq/spec/sentry/sidekiq/sentry_context_middleware_spec.rb index 8d28577e7..45907adb4 100644 --- a/sentry-sidekiq/spec/sentry/sidekiq/sentry_context_middleware_spec.rb +++ b/sentry-sidekiq/spec/sentry/sidekiq/sentry_context_middleware_spec.rb @@ -1,4 +1,8 @@ +# frozen_string_literal: true + require "spec_helper" +require "timecop" +require 'sidekiq/api' RSpec.shared_context "sidekiq", shared_context: :metadata do let(:user) { { "id" => rand(10_000) } } @@ -63,6 +67,56 @@ expect(transaction.contexts.dig(:trace, :origin)).to eq('auto.queue.sidekiq') end + context "span data for Queues module" do + it "adds a queue.process transaction with correct data" do + Timecop.freeze do + execute_worker(processor, HappyWorker) + end + + expect(transport.events.count).to eq(1) + + transaction = transport.events[0] + expect(transaction).not_to be_nil + expect(transaction.spans.count).to eq(0) + expect(transaction.contexts[:trace][:data]['messaging.message.id']).to eq('123123') # Default defined in #execute_worker + expect(transaction.contexts[:trace][:data]['messaging.destination.name']).to eq('default') + expect(transaction.contexts[:trace][:data]['messaging.message.receive.latency']).to eq(0) + expect(transaction.contexts[:trace][:data]['messaging.message.retry.count']).to eq(0) + end + + it "adds a queue.process transaction with correct latency data" do + Timecop.freeze do + execute_worker(processor, HappyWorker, jid: '123456', timecop_delay: Time.now + 86400) + end + + expect(transport.events.count).to eq(1) + + transaction = transport.events[0] + expect(transaction).not_to be_nil + expect(transaction.spans.count).to eq(0) + expect(transaction.contexts[:trace][:data]['messaging.message.id']).to eq('123456') # Explicitly set above. + expect(transaction.contexts[:trace][:data]['messaging.destination.name']).to eq('default') + expect(transaction.contexts[:trace][:data]['messaging.message.receive.latency']).to eq(86400000) + expect(transaction.contexts[:trace][:data]['messaging.message.retry.count']).to eq(0) + end + + if MIN_SIDEKIQ_6 + it "does not fail for latency when performed inline" do + HappyWorker.perform_inline + + expect(transport.events.count).to eq(1) + + transaction = transport.events[0] + expect(transaction).not_to be_nil + expect(transaction.spans.count).to eq(0) + expect(transaction.contexts[:trace][:data]['messaging.message.id']).to be_a(String) + expect(transaction.contexts[:trace][:data]['messaging.destination.name']).to eq('default') + expect(transaction.contexts[:trace][:data]['messaging.message.receive.latency']).to be_nil + expect(transaction.contexts[:trace][:data]['messaging.message.retry.count']).to eq(0) + end + end + end + context "with trace_propagation_headers" do let(:parent_transaction) { Sentry.start_transaction(op: "sidekiq") } @@ -71,6 +125,7 @@ execute_worker(processor, HappyWorker, trace_propagation_headers: trace_propagation_headers) expect(transport.events.count).to eq(1) + transaction = transport.events[0] expect(transaction).not_to be_nil expect(transaction.contexts.dig(:trace, :trace_id)).to eq(parent_transaction.trace_id) @@ -154,5 +209,18 @@ expect(second_headers["sentry-trace"]).to eq(transaction.to_sentry_trace) expect(second_headers["baggage"]).to eq(transaction.to_baggage) end + + it "has a queue.publish span" do + message_id = client.push('queue' => 'default', 'class' => HappyWorker, 'args' => []) + + transaction.finish + + expect(transport.events.count).to eq(1) + event = transport.events.last + expect(event.spans.count).to eq(1) + expect(event.spans[0][:op]).to eq("queue.publish") + expect(event.spans[0][:data]['messaging.message.id']).to eq(message_id) + expect(event.spans[0][:data]['messaging.destination.name']).to eq('default') + end end end diff --git a/sentry-sidekiq/spec/sentry/sidekiq_spec.rb b/sentry-sidekiq/spec/sentry/sidekiq_spec.rb index 2e7cfb99a..110ea850a 100644 --- a/sentry-sidekiq/spec/sentry/sidekiq_spec.rb +++ b/sentry-sidekiq/spec/sentry/sidekiq_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" require 'sidekiq/manager' require 'sidekiq/api' @@ -107,18 +109,45 @@ expect(retry_set.count).to eq(1) end + def retry_last_failed_job + retry_set.first.add_to_queue + job = queue.first + work = Sidekiq::BasicFetch::UnitOfWork.new('queue:default', job.value) + process_work(processor, work) + end + + context "with attempt_threshold" do + it "doesn't report the error until attempts equal the threshold" do + worker = Class.new(SadWorker) + worker.sidekiq_options attempt_threshold: 3 + + execute_worker(processor, worker) + expect(transport.events.count).to eq(0) + + retry_last_failed_job + expect(transport.events.count).to eq(0) + + retry_last_failed_job + expect(transport.events.count).to eq(1) + end + + it "doesn't report the error when threshold is not reached" do + worker = Class.new(SadWorker) + worker.sidekiq_options attempt_threshold: 3 + + execute_worker(processor, worker) + expect(transport.events.count).to eq(0) + + retry_last_failed_job + expect(transport.events.count).to eq(0) + end + end + context "with config.report_after_job_retries = true" do before do Sentry.configuration.sidekiq.report_after_job_retries = true end - def retry_last_failed_job - retry_set.first.add_to_queue - job = queue.first - work = Sidekiq::BasicFetch::UnitOfWork.new('queue:default', job.value) - process_work(processor, work) - end - context "when retry: is specified" do it "doesn't report the error until retries are exhuasted" do worker = Class.new(SadWorker) @@ -221,7 +250,7 @@ def retry_last_failed_job expect(transaction.contexts.dig(:trace, :trace_id)).to be_a(String) expect(transaction.contexts.dig(:trace, :span_id)).to be_a(String) expect(transaction.contexts.dig(:trace, :status)).to eq("ok") - expect(transaction.contexts.dig(:trace, :op)).to eq("queue.sidekiq") + expect(transaction.contexts.dig(:trace, :op)).to eq("queue.process") end it "records transaction with exception" do diff --git a/sentry-sidekiq/spec/spec_helper.rb b/sentry-sidekiq/spec/spec_helper.rb index 83e55d7da..e712a6a0f 100644 --- a/sentry-sidekiq/spec/spec_helper.rb +++ b/sentry-sidekiq/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler/setup" begin require "debug/prelude" @@ -139,6 +141,8 @@ class HappyWorkerForCron < HappyWorker; end class HappyWorkerForScheduler < HappyWorker; end class HappyWorkerForSchedulerWithTimezone < HappyWorker; end class EveryHappyWorker < HappyWorker; end +class HappyWorkerWithHumanReadableCron < HappyWorker; end +class HappyWorkerWithSymbolName < HappyWorker; end class HappyWorkerWithCron < HappyWorker include Sentry::Cron::MonitorCheckIns @@ -225,15 +229,20 @@ def sidekiq_config(opts) def execute_worker(processor, klass, **options) klass_options = klass.sidekiq_options_hash || {} - # for Ruby < 2.6 klass_options.each do |k, v| options[k.to_sym] = v end - msg = Sidekiq.dump_json(jid: "123123", class: klass, args: [], **options) + jid = options.delete(:jid) || "123123" + timecop_delay = options.delete(:timecop_delay) + + msg = Sidekiq.dump_json(created_at: Time.now.to_f, enqueued_at: Time.now.to_f, jid: jid, class: klass, args: [], **options) + Timecop.freeze(timecop_delay) if timecop_delay work = Sidekiq::BasicFetch::UnitOfWork.new('queue:default', msg) process_work(processor, work) +ensure + Timecop.return if timecop_delay end def process_work(processor, work)