sentry-ruby-core-5.3.0/0000755000004100000410000000000014232765714015013 5ustar www-datawww-datasentry-ruby-core-5.3.0/Makefile0000644000004100000410000000013214232765714016447 0ustar www-datawww-databuild: bundle install gem build sentry-ruby-core.gemspec gem build sentry-ruby.gemspec sentry-ruby-core-5.3.0/.rspec0000644000004100000410000000006514232765714016131 0ustar www-datawww-data--format documentation --color --require spec_helper sentry-ruby-core-5.3.0/README.md0000644000004100000410000001526614232765714016304 0ustar www-datawww-data


_Bad software is everywhere, and we're tired of it. Sentry is on a mission to help developers write better software faster, so we can get back to enjoying technology. If you want to join us [**Check out our open positions**](https://sentry.io/careers/)_ Sentry SDK for Ruby =========== | current version | build | coverage | downloads | | --- | ----- | -------- | --------- | | [![Gem Version](https://img.shields.io/gem/v/sentry-ruby?label=sentry-ruby)](https://rubygems.org/gems/sentry-ruby) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-ruby%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_ruby_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-ruby.svg)](https://rubygems.org/gems/sentry-ruby/) | | [![Gem Version](https://img.shields.io/gem/v/sentry-rails?label=sentry-rails)](https://rubygems.org/gems/sentry-rails) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-rails%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_rails_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-rails.svg)](https://rubygems.org/gems/sentry-rails/) | | [![Gem Version](https://img.shields.io/gem/v/sentry-sidekiq?label=sentry-sidekiq)](https://rubygems.org/gems/sentry-sidekiq) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-sidekiq%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_sidekiq_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-sidekiq.svg)](https://rubygems.org/gems/sentry-sidekiq/) | | [![Gem Version](https://img.shields.io/gem/v/sentry-delayed_job?label=sentry-delayed_job)](https://rubygems.org/gems/sentry-delayed_job) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-delayed_job%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_delayed_job_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-delayed_job.svg)](https://rubygems.org/gems/sentry-delayed_job/) | | [![Gem Version](https://img.shields.io/gem/v/sentry-resque?label=sentry-resque)](https://rubygems.org/gems/sentry-resque) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-resque%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_resque_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-resque.svg)](https://rubygems.org/gems/sentry-resque/) | ## Migrate From sentry-raven **The old `sentry-raven` client has entered maintenance mode and was moved to [here](https://github.com/getsentry/sentry-ruby/tree/master/sentry-raven).** If you're using `sentry-raven`, we recommend you to migrate to this new SDK. You can find the benefits of migrating and how to do it in our [migration guide](https://docs.sentry.io/platforms/ruby/migration/). ## Requirements We test on Ruby 2.4, 2.5, 2.6, 2.7, 3.0, and 3.1 at the latest patchlevel/teeny version. We also support JRuby 9.0. If you use self-hosted Sentry, please also make sure its version is above `20.6.0`. ## Getting Started ### Install ```ruby gem "sentry-ruby" ``` and depends on the integrations you want to have, you might also want to install these: ```ruby gem "sentry-rails" gem "sentry-sidekiq" gem "sentry-delayed_job" gem "sentry-resque" ``` ### Configuration You can use `Sentry.init` to initialize and configure your SDK: ```ruby Sentry.init do |config| config.dsn = "MY_DSN" end ``` To learn more about available configuration options, please visit the [official documentation](https://docs.sentry.io/platforms/ruby/configuration/options/). ### Performance Monitoring You can activate [performance monitoring](https://docs.sentry.io/platforms/ruby/performance) by enabling traces sampling: ```ruby Sentry.init do |config| # set a uniform sample rate between 0.0 and 1.0 config.traces_sample_rate = 0.2 # you can also use traces_sampler for more fine-grained sampling # please click the link below to learn more end ``` To learn more about sampling transactions, please visit the [official documentation](https://docs.sentry.io/platforms/ruby/configuration/sampling/#configuring-the-transaction-sample-rate). ### [Migration Guide](https://docs.sentry.io/platforms/ruby/migration/) ### Integrations - [Rack](https://docs.sentry.io/platforms/ruby/guides/rack/) - [Rails](https://docs.sentry.io/platforms/ruby/guides/rails/) - [Sidekiq](https://docs.sentry.io/platforms/ruby/guides/sidekiq/) - [DelayedJob](https://docs.sentry.io/platforms/ruby/guides/delayed_job/) - [Resque](https://docs.sentry.io/platforms/ruby/guides/resque/) ### Enriching Events - [Add more data to the current scope](https://docs.sentry.io/platforms/ruby/guides/rack/enriching-events/scopes/) - [Add custom breadcrumbs](https://docs.sentry.io/platforms/ruby/guides/rack/enriching-events/breadcrumbs/) - [Add contextual data](https://docs.sentry.io/platforms/ruby/guides/rack/enriching-events/context/) - [Add tags](https://docs.sentry.io/platforms/ruby/guides/rack/enriching-events/tags/) ## Resources * [![Ruby docs](https://img.shields.io/badge/documentation-sentry.io-green.svg?label=ruby%20docs)](https://docs.sentry.io/platforms/ruby/) * [![Forum](https://img.shields.io/badge/forum-sentry-green.svg)](https://forum.sentry.io/c/sdks) * [![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) sentry-ruby-core-5.3.0/bin/0000755000004100000410000000000014232765714015563 5ustar www-datawww-datasentry-ruby-core-5.3.0/bin/console0000755000004100000410000000073014232765714017153 0ustar www-datawww-data#!/usr/bin/env ruby require "bundler/setup" require "sentry-ruby" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start # Sentry.init do |config| # config.dsn = 'https://2fb45f003d054a7ea47feb45898f7649@o447951.ingest.sentry.io/5434472' # end require "irb" IRB.start(__FILE__) sentry-ruby-core-5.3.0/bin/setup0000755000004100000410000000020314232765714016644 0ustar www-datawww-data#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here sentry-ruby-core-5.3.0/CHANGELOG.md0000644000004100000410000003615014232765714016631 0ustar www-datawww-data# Changelog Individual gem's changelog has been deprecated. Please check the [project changelog](https://github.com/getsentry/sentry-ruby/blob/master/CHANGELOG.md). ## 4.4.2 - Fix NoMethodError when SDK's dsn is nil [#1433](https://github.com/getsentry/sentry-ruby/pull/1433) - fix: Update protocol version to 7 [#1434](https://github.com/getsentry/sentry-ruby/pull/1434) - Fixes [#867](https://github.com/getsentry/sentry-ruby/issues/867) ## 4.4.1 - Apply patches when initializing the SDK [#1432](https://github.com/getsentry/sentry-ruby/pull/1432) ## 4.4.0 ### Features #### Support category-based rate limiting [#1336](https://github.com/getsentry/sentry-ruby/pull/1336) Sentry rate limits different types of events. And when rate limiting is enabled, it sends back a `429` response to the SDK. Currently, the SDK would then raise an error like this: ``` Unable to record event with remote Sentry server (Sentry::Error - the server responded with status 429 body: {"detail":"event rejected due to rate limit"}): ``` This change improves the SDK's handling on such responses by: - Not treating them as errors, so you don't see the noise anymore. - Halting event sending for a while according to the duration provided in the response. And warns you with a message like: ``` Envelope [event] not sent: Excluded by random sample ``` #### Record request span from Net::HTTP library [#1381](https://github.com/getsentry/sentry-ruby/pull/1381) Now any outgoing requests will be recorded as a tracing span. Example: net:http span example #### Record breadcrumb for Net::HTTP requests [#1394](https://github.com/getsentry/sentry-ruby/pull/1394) With the new `http_logger` breadcrumbs logger: ```ruby config.breadcrumbs_logger = [:http_logger] ``` The SDK now records a new `net.http` breadcrumb whenever the user makes a request with the `Net::HTTP` library. net http breadcrumb #### Support config.debug configuration option [#1400](https://github.com/getsentry/sentry-ruby/pull/1400) It'll determine whether the SDK should run in the debugging mode. Default is `false`. When set to true, SDK errors will be logged with backtrace. #### Add the third tracing state [#1402](https://github.com/getsentry/sentry-ruby/pull/1402) - `rate == 0` - Tracing enabled. Rejects all locally created transactions but respects sentry-trace. - `1 > rate > 0` - Tracing enabled. Samples locally created transactions with the rate and respects sentry-trace. - `rate < 0` or `rate > 1` - Tracing disabled. ### Refactorings - Let Transaction constructor take an optional hub argument [#1384](https://github.com/getsentry/sentry-ruby/pull/1384) - Introduce LoggingHelper [#1385](https://github.com/getsentry/sentry-ruby/pull/1385) - Raise exception if a Transaction is initialized without a hub [#1391](https://github.com/getsentry/sentry-ruby/pull/1391) - Make hub a required argument for Transaction constructor [#1401](https://github.com/getsentry/sentry-ruby/pull/1401) ### Bug Fixes - Check `Scope#set_context`'s value argument [#1415](https://github.com/getsentry/sentry-ruby/pull/1415) - Disable tracing if events are not allowed to be sent [#1421](https://github.com/getsentry/sentry-ruby/pull/1421) ## 4.3.2 - Correct type attribute's usages [#1354](https://github.com/getsentry/sentry-ruby/pull/1354) - Fix sampling decision precedence [#1335](https://github.com/getsentry/sentry-ruby/pull/1335) - Fix set_contexts [#1375](https://github.com/getsentry/sentry-ruby/pull/1375) - Use thread variable instead of fiber variable to store the hub [#1380](https://github.com/getsentry/sentry-ruby/pull/1380) - Fixes [#1374](https://github.com/getsentry/sentry-ruby/issues/1374) - Fix Span/Transaction's nesting issue [#1382](https://github.com/getsentry/sentry-ruby/pull/1382) - Fixes [#1372](https://github.com/getsentry/sentry-ruby/issues/1372) ## 4.3.1 - Add Sentry.set_context helper [#1337](https://github.com/getsentry/sentry-ruby/pull/1337) - Fix handle the case where the logger messages is not of String type [#1341](https://github.com/getsentry/sentry-ruby/pull/1341) - Don't report Sentry::ExternalError to Sentry [#1353](https://github.com/getsentry/sentry-ruby/pull/1353) - Sentry.add_breadcrumb should call Hub#add_breadcrumb [#1358](https://github.com/getsentry/sentry-ruby/pull/1358) - Fixes [#1357](https://github.com/getsentry/sentry-ruby/issues/1357) ## 4.3.0 ### Features - Allow configuring BreadcrumbBuffer's size limit [#1310](https://github.com/getsentry/sentry-ruby/pull/1310) ```ruby # the SDK will only store 10 breadcrumbs (default is 100) config.max_breadcrumbs = 10 ``` - Compress event payload by default [#1314](https://github.com/getsentry/sentry-ruby/pull/1314) ### Refatorings - Refactor interface construction [#1296](https://github.com/getsentry/sentry-ruby/pull/1296) - Refactor tracing implementation [#1309](https://github.com/getsentry/sentry-ruby/pull/1309) ### Bug Fixes - Improve SDK's error handling [#1298](https://github.com/getsentry/sentry-ruby/pull/1298) - Fixes [#1246](https://github.com/getsentry/sentry-ruby/issues/1246) and [#1289](https://github.com/getsentry/sentry-ruby/issues/1289) - Please read [#1290](https://github.com/getsentry/sentry-ruby/issues/1290) to see the full specification - Treat query string as pii too [#1302](https://github.com/getsentry/sentry-ruby/pull/1302) - Fixes [#1301](https://github.com/getsentry/sentry-ruby/issues/1301) - Ignore sentry-trace when tracing is not enabled [#1308](https://github.com/getsentry/sentry-ruby/pull/1308) - Fixes [#1307](https://github.com/getsentry/sentry-ruby/issues/1307) - Return nil from logger methods instead of breadcrumb buffer [#1299](https://github.com/getsentry/sentry-ruby/pull/1299) - Exceptions with nil message shouldn't cause issues [#1327](https://github.com/getsentry/sentry-ruby/pull/1327) - Fixes [#1323](https://github.com/getsentry/sentry-ruby/issues/1323) - Fix sampling decision with sentry-trace and add more tests [#1326](https://github.com/getsentry/sentry-ruby/pull/1326) ## 4.2.2 - Add thread_id to Exception interface [#1291](https://github.com/getsentry/sentry-ruby/pull/1291) - always convert trusted proxies to string [#1288](https://github.com/getsentry/sentry-ruby/pull/1288) - fixes [#1274](https://github.com/getsentry/sentry-ruby/issues/1274) ## 4.2.1 ### Bug Fixes - Ignore invalid values for sentry-trace header that don't match the required format [#1265](https://github.com/getsentry/sentry-ruby/pull/1265) - Transaction created by `.from_sentry_trace` should inherit sampling decision [#1269](https://github.com/getsentry/sentry-ruby/pull/1269) - Transaction's sample rate should accept any numeric value [#1278](https://github.com/getsentry/sentry-ruby/pull/1278) ## 4.2.0 ### Features - Add configuration option for trusted proxies [#1126](https://github.com/getsentry/sentry-ruby/pull/1126) ```ruby config.trusted_proxies = ["2.2.2.2"] # this ip address will be skipped when computing users' ip addresses ``` - Add ThreadsInterface [#1178](https://github.com/getsentry/sentry-ruby/pull/1178) an exception event that has the new threads interface - Support `config.before_breadcrumb` [#1253](https://github.com/getsentry/sentry-ruby/pull/1253) ```ruby # this will be called before every breadcrumb is added to the breadcrumb buffer # you can use it to # - remove the data you don't want to send # - add additional info to the data config.before_breadcrumb = lambda do |breadcrumb, hint| breadcrumb.message = "foo" breadcrumb end ``` - Add ability to have many post initialization callbacks [#1261](https://github.com/getsentry/sentry-ruby/pull/1261) ### Bug Fixes - Inspect exception cause by default & don't exclude ActiveJob::DeserializationError [#1180](https://github.com/getsentry/sentry-ruby/pull/1180) - Fixes [#1071](https://github.com/getsentry/sentry-ruby/issues/1071) ## 4.1.6 - Don't detect project root for Rails apps [#1243](https://github.com/getsentry/sentry-ruby/pull/1243) - Separate individual breadcrumb's data serialization [#1250](https://github.com/getsentry/sentry-ruby/pull/1250) - Capture sentry-trace with the correct http header key [#1260](https://github.com/getsentry/sentry-ruby/pull/1260) ## 4.1.5 - Serialize event hint before passing it to the async block [#1231](https://github.com/getsentry/sentry-ruby/pull/1231) - Fixes [#1227](https://github.com/getsentry/sentry-ruby/issues/1227) - Require the English library [#1233](https://github.com/getsentry/sentry-ruby/pull/1233) (by @dentarg) - Allow `Sentry.init` without block argument [#1235](https://github.com/getsentry/sentry-ruby/pull/1235) (by @sue445) ## 4.1.5-beta.1 - No change ## 4.1.5-beta.0 - Inline global method [#1213](https://github.com/getsentry/sentry-ruby/pull/1213) (by @tricknotes) - Event message and exception message should have a size limit [#1221](https://github.com/getsentry/sentry-ruby/pull/1221) - Add sentry-ruby-core as a more flexible option [#1226](https://github.com/getsentry/sentry-ruby/pull/1226) ## 4.1.4 - Fix headers serialization for sentry-ruby [#1197](https://github.com/getsentry/sentry-ruby/pull/1197) (by @moofkit) - Support capturing "sentry-trace" header from the middleware [#1205](https://github.com/getsentry/sentry-ruby/pull/1205) - Document public APIs on the Sentry module [#1208](https://github.com/getsentry/sentry-ruby/pull/1208) - Check the argument type of capture_exception and capture_event helpers [#1209](https://github.com/getsentry/sentry-ruby/pull/1209) ## 4.1.3 - rm reference to old constant (from Rails v2.2) [#1184](https://github.com/getsentry/sentry-ruby/pull/1184) - Use copied env in events [#1186](https://github.com/getsentry/sentry-ruby/pull/1186) - Fixes [#1183](https://github.com/getsentry/sentry-ruby/issues/1183) - Refactor RequestInterface [#1187](https://github.com/getsentry/sentry-ruby/pull/1187) - Supply event hint to async callback when possible [#1189](https://github.com/getsentry/sentry-ruby/pull/1189) - Fixes [#1188](https://github.com/getsentry/sentry-ruby/issues/1188) - Refactor stacktrace parsing and increase test coverage [#1190](https://github.com/getsentry/sentry-ruby/pull/1190) - Sentry.send_event should also take a hint [#1192](https://github.com/getsentry/sentry-ruby/pull/1192) ## 4.1.2 - before_send callback shouldn't be applied to transaction events [#1167](https://github.com/getsentry/sentry-ruby/pull/1167) - Transaction improvements [#1170](https://github.com/getsentry/sentry-ruby/pull/1170) - Support Ruby 3 [#1172](https://github.com/getsentry/sentry-ruby/pull/1172) - Add Integrable module [#1177](https://github.com/getsentry/sentry-ruby/pull/1177) ## 4.1.1 - Fix NoMethodError when sending is not allowed [#1161](https://github.com/getsentry/sentry-ruby/pull/1161) - Add notification for users who still use deprecated middlewares [#1160](https://github.com/getsentry/sentry-ruby/pull/1160) - Improve top-level api safety [#1162](https://github.com/getsentry/sentry-ruby/pull/1162) ## 4.1.0 - Separate rack integration [#1138](https://github.com/getsentry/sentry-ruby/pull/1138) - Fixes [#1136](https://github.com/getsentry/sentry-ruby/pull/1136) - Fix event sampling [#1144](https://github.com/getsentry/sentry-ruby/pull/1144) - Merge & rename 2 Rack middlewares [#1147](https://github.com/getsentry/sentry-ruby/pull/1147) - Fixes [#1153](https://github.com/getsentry/sentry-ruby/pull/1153) - Removed `Sentry::Rack::Tracing` middleware and renamed `Sentry::Rack::CaptureException` to `Sentry::Rack::CaptureExceptions`. - Deep-copy spans [#1148](https://github.com/getsentry/sentry-ruby/pull/1148) - Move span recorder related code from Span to Transaction [#1149](https://github.com/getsentry/sentry-ruby/pull/1149) - Check SDK initialization before running integrations [#1151](https://github.com/getsentry/sentry-ruby/pull/1151) - Fixes [#1145](https://github.com/getsentry/sentry-ruby/pull/1145) - Refactor transport [#1154](https://github.com/getsentry/sentry-ruby/pull/1154) - Implement non-blocking event sending [#1155](https://github.com/getsentry/sentry-ruby/pull/1155) - Added `background_worker_threads` configuration option. ### Noticeable Changes #### Middleware Changes `Sentry::Rack::Tracing` is now removed. And `Sentry::Rack::CaptureException` has been renamed to `Sentry::Rack::CaptureExceptions`. #### Events Are Sent Asynchronously `sentry-ruby` now sends events asynchronously by default. The functionality works like this: 1. When the SDK is initialized, a `Sentry::BackgroundWorker` will be initialized too. 2. When an event is passed to `Client#capture_event`, instead of sending it directly with `Client#send_event`, we'll let the worker do it. 3. The worker will have a number of threads. And the one of the idle threads will pick the job and call `Client#send_event`. - If all the threads are busy, new jobs will be put into a queue, which has a limit of 30. - If the queue size is exceeded, new events will be dropped. However, if you still prefer to use your own async approach, that's totally fine. If you have `config.async` set, the worker won't initialize a thread pool and won't be used either. This functionality also introduces a new `background_worker_threads` config option. It allows you to decide how many threads should the worker hold. By default, the value will be the number of the processors your machine has. For example, if your machine has 4 processors, the value would be 4. Of course, you can always override the value to fit your use cases, like ```ruby config.background_worker_threads = 5 # the worker will have 5 threads for sending events ``` You can also disable this new non-blocking behaviour by giving a `0` value: ```ruby config.background_worker_threads = 0 # all events will be sent synchronously ``` ## 4.0.1 - Add rake integration: [1137](https://github.com/getsentry/sentry-ruby/pull/1137) - Make Event's interfaces accessible: [1135](https://github.com/getsentry/sentry-ruby/pull/1135) - ActiveSupportLogger should only record events that has a started time: [1132](https://github.com/getsentry/sentry-ruby/pull/1132) ## 4.0.0 - Only documents update for the official release and no API/feature changes. ## 0.3.0 - Major API changes: [1123](https://github.com/getsentry/sentry-ruby/pull/1123) - Support event hint: [1122](https://github.com/getsentry/sentry-ruby/pull/1122) - Add request-id tag to events: [1120](https://github.com/getsentry/sentry-ruby/pull/1120) (by @tvec) ## 0.2.0 - Multiple fixes and refactorings - Tracing support ## 0.1.3 Fix require reference ## 0.1.2 - Fix: Fix async callback [1098](https://github.com/getsentry/sentry-ruby/pull/1098) - Refactor: Some code cleanup [1100](https://github.com/getsentry/sentry-ruby/pull/1100) - Refactor: Remove Event options [1101](https://github.com/getsentry/sentry-ruby/pull/1101) ## 0.1.1 - Feature: Allow passing custom scope to Hub#capture* helpers [1086](https://github.com/getsentry/sentry-ruby/pull/1086) ## 0.1.0 First version sentry-ruby-core-5.3.0/.gitignore0000644000004100000410000000016114232765714017001 0ustar www-datawww-data/.bundle/ /.yardoc /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ # rspec failure tracking .rspec_status sentry-ruby-core-5.3.0/CODE_OF_CONDUCT.md0000644000004100000410000000623714232765714017622 0ustar www-datawww-data# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at stan001212@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] [homepage]: https://contributor-covenant.org [version]: https://contributor-covenant.org/version/1/4/ sentry-ruby-core-5.3.0/Rakefile0000644000004100000410000000041614232765714016461 0ustar www-datawww-datarequire "rake/clean" CLOBBER.include "pkg" require "bundler/gem_helper" Bundler::GemHelper.install_tasks(name: "sentry-ruby") require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec).tap do |task| task.rspec_opts = "--order rand" end task :default => :spec sentry-ruby-core-5.3.0/sentry-ruby-core.gemspec0000644000004100000410000000173414232765714021616 0ustar www-datawww-datarequire_relative "lib/sentry/version" Gem::Specification.new do |spec| spec.name = "sentry-ruby-core" spec.version = Sentry::VERSION spec.authors = ["Sentry Team"] 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.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.add_dependency "concurrent-ruby" end sentry-ruby-core-5.3.0/lib/0000755000004100000410000000000014232765714015561 5ustar www-datawww-datasentry-ruby-core-5.3.0/lib/sentry/0000755000004100000410000000000014232765714017105 5ustar www-datawww-datasentry-ruby-core-5.3.0/lib/sentry/core_ext/0000755000004100000410000000000014232765714020715 5ustar www-datawww-datasentry-ruby-core-5.3.0/lib/sentry/core_ext/object/0000755000004100000410000000000014232765714022163 5ustar www-datawww-datasentry-ruby-core-5.3.0/lib/sentry/core_ext/object/deep_dup.rb0000644000004100000410000000242414232765714024277 0ustar www-datawww-data# frozen_string_literal: true return if Object.method_defined?(:deep_dup) require 'sentry/core_ext/object/duplicable' ######################################### # This file was copied from Rails 5.2 # ######################################### class Object # Returns a deep copy of object if it's duplicable. If it's # not duplicable, returns +self+. # # object = Object.new # dup = object.deep_dup # dup.instance_variable_set(:@a, 1) # # object.instance_variable_defined?(:@a) # => false # dup.instance_variable_defined?(:@a) # => true def deep_dup duplicable? ? dup : self end end class Array # Returns a deep copy of array. # # array = [1, [2, 3]] # dup = array.deep_dup # dup[1][2] = 4 # # array[1][2] # => nil # dup[1][2] # => 4 def deep_dup map(&:deep_dup) end end class Hash # Returns a deep copy of hash. # # hash = { a: { b: 'b' } } # dup = hash.deep_dup # dup[:a][:c] = 'c' # # hash[:a][:c] # => nil # dup[:a][:c] # => "c" def deep_dup hash = dup each_pair do |key, value| if key.frozen? && ::String === key hash[key] = value.deep_dup else hash.delete(key) hash[key.deep_dup] = value.deep_dup end end hash end end sentry-ruby-core-5.3.0/lib/sentry/core_ext/object/duplicable.rb0000644000004100000410000000640614232765714024622 0ustar www-datawww-data# frozen_string_literal: true return if Object.method_defined?(:duplicable?) ######################################### # This file was copied from Rails 5.2 # ######################################### #-- # Most objects are cloneable, but not all. For example you can't dup methods: # # method(:puts).dup # => TypeError: allocator undefined for Method # # Classes may signal their instances are not duplicable removing +dup+/+clone+ # or raising exceptions from them. So, to dup an arbitrary object you normally # use an optimistic approach and are ready to catch an exception, say: # # arbitrary_object.dup rescue object # # Rails dups objects in a few critical spots where they are not that arbitrary. # That rescue is very expensive (like 40 times slower than a predicate), and it # is often triggered. # # That's why we hardcode the following cases and check duplicable? instead of # using that rescue idiom. #++ class Object # Can you safely dup this object? # # False for method objects; # true otherwise. def duplicable? true end end class NilClass begin nil.dup rescue TypeError # +nil+ is not duplicable: # # nil.duplicable? # => false # nil.dup # => TypeError: can't dup NilClass def duplicable? false end end end class FalseClass begin false.dup rescue TypeError # +false+ is not duplicable: # # false.duplicable? # => false # false.dup # => TypeError: can't dup FalseClass def duplicable? false end end end class TrueClass begin true.dup rescue TypeError # +true+ is not duplicable: # # true.duplicable? # => false # true.dup # => TypeError: can't dup TrueClass def duplicable? false end end end class Symbol begin :symbol.dup # Ruby 2.4.x. "symbol_from_string".to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0. rescue TypeError # Symbols are not duplicable: # # :my_symbol.duplicable? # => false # :my_symbol.dup # => TypeError: can't dup Symbol def duplicable? false end end end class Numeric begin 1.dup rescue TypeError # Numbers are not duplicable: # # 3.duplicable? # => false # 3.dup # => TypeError: can't dup Integer def duplicable? false end end end require "bigdecimal" class BigDecimal # BigDecimals are duplicable: # # BigDecimal("1.2").duplicable? # => true # BigDecimal("1.2").dup # => # def duplicable? true end end class Method # Methods are not duplicable: # # method(:puts).duplicable? # => false # method(:puts).dup # => TypeError: allocator undefined for Method def duplicable? false end end class Complex begin Complex(1).dup rescue TypeError # Complexes are not duplicable: # # Complex(1).duplicable? # => false # Complex(1).dup # => TypeError: can't copy Complex def duplicable? false end end end class Rational begin Rational(1).dup rescue TypeError # Rationals are not duplicable: # # Rational(1).duplicable? # => false # Rational(1).dup # => TypeError: can't copy Rational def duplicable? false end end end sentry-ruby-core-5.3.0/lib/sentry/session.rb0000644000004100000410000000144114232765714021115 0ustar www-datawww-data# frozen_string_literal: true module Sentry class Session attr_reader :started, :status # TODO-neel add :crashed after adding handled mechanism STATUSES = %i(ok errored exited) AGGREGATE_STATUSES = %i(errored exited) def initialize @started = Sentry.utc_now @status = :ok end # TODO-neel add :crashed after adding handled mechanism def update_from_exception(_exception = nil) @status = :errored end def close @status = :exited if @status == :ok end # truncate seconds from the timestamp since we only care about # minute level granularity for aggregation def aggregation_key Time.utc(started.year, started.month, started.day, started.hour, started.min) end def deep_dup dup end end end sentry-ruby-core-5.3.0/lib/sentry/interface.rb0000644000004100000410000000063114232765714021372 0ustar www-datawww-data# frozen_string_literal: true module Sentry class Interface # @return [Hash] def to_hash Hash[instance_variables.map { |name| [name[1..-1].to_sym, instance_variable_get(name)] }] end end end require "sentry/interfaces/exception" require "sentry/interfaces/request" require "sentry/interfaces/single_exception" require "sentry/interfaces/stacktrace" require "sentry/interfaces/threads" sentry-ruby-core-5.3.0/lib/sentry/version.rb0000644000004100000410000000010514232765714021113 0ustar www-datawww-data# frozen_string_literal: true module Sentry VERSION = "5.3.0" end sentry-ruby-core-5.3.0/lib/sentry/transport.rb0000644000004100000410000001360114232765714021467 0ustar www-datawww-data# frozen_string_literal: true require "json" require "base64" require "sentry/envelope" module Sentry class Transport PROTOCOL_VERSION = '7' USER_AGENT = "sentry-ruby/#{Sentry::VERSION}" CLIENT_REPORT_INTERVAL = 30 # https://develop.sentry.dev/sdk/client-reports/#envelope-item-payload CLIENT_REPORT_REASONS = [ :ratelimit_backoff, :queue_overflow, :cache_overflow, # NA :network_error, :sample_rate, :before_send, :event_processor ] include LoggingHelper attr_reader :rate_limits, :discarded_events, :last_client_report_sent # @deprecated Use Sentry.logger to retrieve the current logger instead. attr_reader :logger def initialize(configuration) @logger = configuration.logger @transport_configuration = configuration.transport @dsn = configuration.dsn @rate_limits = {} @send_client_reports = configuration.send_client_reports if @send_client_reports @discarded_events = Hash.new(0) @last_client_report_sent = Time.now end end def send_data(data, options = {}) raise NotImplementedError end def send_event(event) envelope = envelope_from_event(event) send_envelope(envelope) event end def send_envelope(envelope) reject_rate_limited_items(envelope) return if envelope.items.empty? data, serialized_items = serialize_envelope(envelope) if data log_info("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry") send_data(data) end end def serialize_envelope(envelope) serialized_items = [] serialized_results = [] envelope.items.each do |item| result = item.to_s if result.bytesize > Event::MAX_SERIALIZED_PAYLOAD_SIZE if item.payload.key?(:breadcrumbs) item.payload.delete(:breadcrumbs) elsif item.payload.key?("breadcrumbs") item.payload.delete("breadcrumbs") end result = item.to_s end if result.bytesize > Event::MAX_SERIALIZED_PAYLOAD_SIZE size_breakdown = item.payload.map do |key, value| "#{key}: #{JSON.generate(value).bytesize}" end.join(", ") log_debug("Envelope item [#{item.type}] is still oversized without breadcrumbs: {#{size_breakdown}}") next end serialized_results << result serialized_items << item end data = [JSON.generate(envelope.headers), *serialized_results].join("\n") unless serialized_results.empty? [data, serialized_items] end def is_rate_limited?(item_type) # check category-specific limit category_delay = case item_type when "transaction" @rate_limits["transaction"] when "sessions" @rate_limits["session"] else @rate_limits["error"] end # check universal limit if not category limit universal_delay = @rate_limits[nil] delay = if category_delay && universal_delay if category_delay > universal_delay category_delay else universal_delay end elsif category_delay category_delay else universal_delay end !!delay && delay > Time.now end 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 } fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key 'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ') end def envelope_from_event(event) # Convert to hash event_payload = event.to_hash event_id = event_payload[:event_id] || event_payload["event_id"] item_type = event_payload[:type] || event_payload["type"] envelope = Envelope.new( { event_id: event_id, dsn: @dsn.to_s, sdk: Sentry.sdk_meta, sent_at: Sentry.utc_now.iso8601 } ) envelope.add_item( { type: item_type, content_type: 'application/json' }, event_payload ) 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, item_type) return unless @send_client_reports return unless CLIENT_REPORT_REASONS.include?(reason) @discarded_events[[reason, item_type]] += 1 end private def fetch_pending_client_report return nil unless @send_client_reports return nil if @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL return nil if @discarded_events.empty? discarded_events_hash = @discarded_events.map do |key, val| reason, type = key # 'event' has to be mapped to 'error' category = type == 'transaction' ? 'transaction' : 'error' { reason: reason, category: category, quantity: val } end item_header = { type: 'client_report' } item_payload = { timestamp: Sentry.utc_now.iso8601, discarded_events: discarded_events_hash } @discarded_events = Hash.new(0) @last_client_report_sent = Time.now [item_header, item_payload] end def reject_rate_limited_items(envelope) envelope.items.reject! do |item| if is_rate_limited?(item.type) log_info("[Transport] Envelope item [#{item.type}] not sent: rate limiting") record_lost_event(:ratelimit_backoff, item.type) true else false end end end end end require "sentry/transport/dummy_transport" require "sentry/transport/http_transport" sentry-ruby-core-5.3.0/lib/sentry/rake.rb0000644000004100000410000000145614232765714020362 0ustar www-datawww-data# frozen_string_literal: true require "rake" require "rake/task" module Sentry module Rake module Application # @api private def display_error_message(ex) Sentry.capture_exception(ex) do |scope| task_name = top_level_tasks.join(' ') scope.set_transaction_name(task_name) scope.set_tag("rake_task", task_name) end if Sentry.initialized? && !Sentry.configuration.skip_rake_integration super end end module Task # @api private def execute(args=nil) return super unless Sentry.initialized? && Sentry.get_current_hub super end end end end # @api private module Rake class Application prepend(Sentry::Rake::Application) end class Task prepend(Sentry::Rake::Task) end end sentry-ruby-core-5.3.0/lib/sentry/utils/0000755000004100000410000000000014232765714020245 5ustar www-datawww-datasentry-ruby-core-5.3.0/lib/sentry/utils/custom_inspection.rb0000644000004100000410000000061114232765714024335 0ustar www-datawww-data# frozen_string_literal: true module Sentry module CustomInspection def inspect attr_strings = (instance_variables - self.class::SKIP_INSPECTION_ATTRIBUTES).each_with_object([]) do |attr, result| value = instance_variable_get(attr) result << "#{attr}=#{value.inspect}" if value end "#<#{self.class.name} #{attr_strings.join(", ")}>" end end end sentry-ruby-core-5.3.0/lib/sentry/utils/request_id.rb0000644000004100000410000000064014232765714022736 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Utils module RequestId REQUEST_ID_HEADERS = %w(action_dispatch.request_id HTTP_X_REQUEST_ID).freeze # Request ID based on ActionDispatch::RequestId def self.read_from(env) REQUEST_ID_HEADERS.each do |key| request_id = env[key] return request_id if request_id end nil end end end end sentry-ruby-core-5.3.0/lib/sentry/utils/exception_cause_chain.rb0000644000004100000410000000063314232765714025114 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Utils module ExceptionCauseChain def self.exception_to_array(exception) exceptions = [exception] while exception.cause exception = exception.cause break if exceptions.any? { |e| e.object_id == exception.object_id } exceptions << exception end exceptions end end end end sentry-ruby-core-5.3.0/lib/sentry/utils/argument_checking_helper.rb0000644000004100000410000000052014232765714025603 0ustar www-datawww-data# frozen_string_literal: true module Sentry module ArgumentCheckingHelper private def check_argument_type!(argument, expected_type) unless argument.is_a?(expected_type) raise ArgumentError, "expect the argument to be a #{expected_type}, got #{argument.class} (#{argument.inspect})" end end end end sentry-ruby-core-5.3.0/lib/sentry/utils/real_ip.rb0000644000004100000410000000533714232765714022215 0ustar www-datawww-data# frozen_string_literal: true 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, # and spoofing an IP here only makes data inaccurate, not insecure. Don't re-use # this module if you have to *trust* the IP address. module Sentry module Utils class RealIp LOCAL_ADDRESSES = [ "127.0.0.1", # localhost IPv4 "::1", # localhost IPv6 "fc00::/7", # private IPv6 range fc00::/7 "10.0.0.0/8", # private IPv4 range 10.x.x.x "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255 "192.168.0.0/16", # private IPv4 range 192.168.x.x ] attr_reader :ip def initialize( remote_addr: nil, client_ip: nil, real_ip: nil, forwarded_for: nil, trusted_proxies: [] ) @remote_addr = remote_addr @client_ip = client_ip @real_ip = real_ip @forwarded_for = forwarded_for @trusted_proxies = (LOCAL_ADDRESSES + Array(trusted_proxies)).map do |proxy| if proxy.is_a?(IPAddr) proxy else IPAddr.new(proxy.to_s) end end.uniq end def calculate_ip # CGI environment variable set by Rack remote_addr = ips_from(@remote_addr).last # Could be a CSV list and/or repeated headers that were concatenated. client_ips = ips_from(@client_ip) real_ips = ips_from(@real_ip) # The first address in this list is the original client, followed by # the IPs of successive proxies. We want to search starting from the end # until we find the first proxy that we do not trust. forwarded_ips = ips_from(@forwarded_for).reverse ips = [client_ips, real_ips, forwarded_ips, remote_addr].flatten.compact # If every single IP option is in the trusted list, just return REMOTE_ADDR @ip = filter_trusted_proxy_addresses(ips).first || remote_addr end protected def ips_from(header) # Split the comma-separated list into an array of strings ips = header ? header.strip.split(/[,\s]+/) : [] ips.select do |ip| begin # Only return IPs that are valid according to the IPAddr#new method range = IPAddr.new(ip).to_range # we want to make sure nobody is sneaking a netmask in range.begin == range.end rescue ArgumentError nil end end end def filter_trusted_proxy_addresses(ips) ips.reject { |ip| @trusted_proxies.any? { |proxy| proxy === ip } } end end end end sentry-ruby-core-5.3.0/lib/sentry/utils/logging_helper.rb0000644000004100000410000000107314232765714023560 0ustar www-datawww-data# frozen_string_literal: true module Sentry module LoggingHelper def log_error(message, exception, debug: false) message = "#{message}: #{exception.message}" message += "\n#{exception.backtrace.join("\n")}" if debug @logger.error(LOGGER_PROGNAME) do message end end def log_info(message) @logger.info(LOGGER_PROGNAME) { message } end def log_debug(message) @logger.debug(LOGGER_PROGNAME) { message } end def log_warn(message) @logger.warn(LOGGER_PROGNAME) { message } end end end sentry-ruby-core-5.3.0/lib/sentry/exceptions.rb0000644000004100000410000000017214232765714021613 0ustar www-datawww-data# frozen_string_literal: true module Sentry class Error < StandardError end class ExternalError < Error end end sentry-ruby-core-5.3.0/lib/sentry/breadcrumb.rb0000644000004100000410000000326314232765714021544 0ustar www-datawww-data# frozen_string_literal: true module Sentry class Breadcrumb DATA_SERIALIZATION_ERROR_MESSAGE = "[data were removed due to serialization issues]" # @return [String, nil] attr_accessor :category # @return [Hash, nil] attr_accessor :data # @return [String, nil] attr_accessor :level # @return [Time, Integer, nil] attr_accessor :timestamp # @return [String, nil] attr_accessor :type # @return [String, nil] attr_reader :message # @param category [String, nil] # @param data [Hash, nil] # @param message [String, nil] # @param timestamp [Time, Integer, nil] # @param level [String, nil] # @param type [String, nil] def initialize(category: nil, data: nil, message: nil, timestamp: nil, level: nil, type: nil) @category = category @data = data || {} @level = level @timestamp = timestamp || Sentry.utc_now.to_i @type = type self.message = message end # @return [Hash] def to_hash { category: @category, data: serialized_data, level: @level, message: @message, timestamp: @timestamp, type: @type } end # @param message [String] # @return [void] def message=(message) @message = (message || "").byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES) end private def serialized_data begin ::JSON.parse(::JSON.generate(@data)) rescue Exception => e Sentry.logger.debug(LOGGER_PROGNAME) do <<~MSG can't serialize breadcrumb data because of error: #{e} data: #{@data} MSG end DATA_SERIALIZATION_ERROR_MESSAGE end end end end sentry-ruby-core-5.3.0/lib/sentry/interfaces/0000755000004100000410000000000014232765714021230 5ustar www-datawww-datasentry-ruby-core-5.3.0/lib/sentry/interfaces/request.rb0000644000004100000410000001065214232765714023251 0ustar www-datawww-data# frozen_string_literal: true module Sentry class RequestInterface < Interface REQUEST_ID_HEADERS = %w(action_dispatch.request_id HTTP_X_REQUEST_ID).freeze CONTENT_HEADERS = %w(CONTENT_TYPE CONTENT_LENGTH).freeze IP_HEADERS = [ "REMOTE_ADDR", "HTTP_CLIENT_IP", "HTTP_X_REAL_IP", "HTTP_X_FORWARDED_FOR" ].freeze # See Sentry server default limits at # https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py MAX_BODY_LIMIT = 4096 * 4 # @return [String] attr_accessor :url # @return [String] attr_accessor :method # @return [Hash] attr_accessor :data # @return [String] attr_accessor :query_string # @return [String] attr_accessor :cookies # @return [Hash] attr_accessor :headers # @return [Hash] attr_accessor :env # @param env [Hash] # @param send_default_pii [Boolean] # @param rack_env_whitelist [Array] # @see Configuration#send_default_pii # @see Configuration#rack_env_whitelist def initialize(env:, send_default_pii:, rack_env_whitelist:) env = env.dup unless send_default_pii # need to completely wipe out ip addresses RequestInterface::IP_HEADERS.each do |header| env.delete(header) end end request = ::Rack::Request.new(env) if send_default_pii self.data = read_data_from(request) self.cookies = request.cookies self.query_string = request.query_string end self.url = request.scheme && request.url.split('?').first self.method = request.request_method self.headers = filter_and_format_headers(env, send_default_pii) self.env = filter_and_format_env(env, rack_env_whitelist) end private def read_data_from(request) if request.form_data? request.POST elsif request.body # JSON requests, etc data = request.body.read(MAX_BODY_LIMIT) data = encode_to_utf_8(data.to_s) request.body.rewind data end rescue IOError => e e.message end 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 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('-') memo[key] = encode_to_utf_8(value.to_s) rescue StandardError => e # Rails adds objects to the Rack env that can sometimes raise exceptions # when `to_s` is called. # See: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L134 Sentry.logger.warn(LOGGER_PROGNAME) { "Error raised while formatting headers: #{e.message}" } next end end end def encode_to_utf_8(value) if value.encoding != Encoding::UTF_8 && value.respond_to?(:force_encoding) value = value.dup.force_encoding(Encoding::UTF_8) end if !value.valid_encoding? value = value.scrub end value end 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)) end # Rack adds in an incorrect HTTP_VERSION key, which causes downstream # to think this is a Version header. Instead, this is mapped to # env['SERVER_PROTOCOL']. But we don't want to ignore a valid header # if the request has legitimately sent a Version header themselves. # See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29 # NOTE: This will be removed in version 3.0+ def is_server_protocol?(key, value, protocol_version) key == 'HTTP_VERSION' && value == protocol_version end def filter_and_format_env(env, rack_env_whitelist) return env if rack_env_whitelist.empty? env.select do |k, _v| rack_env_whitelist.include? k.to_s end end end end sentry-ruby-core-5.3.0/lib/sentry/interfaces/stacktrace_builder.rb0000644000004100000410000000443614232765714025416 0ustar www-datawww-data# frozen_string_literal: true module Sentry class StacktraceBuilder # @return [String] attr_reader :project_root # @return [Regexp, nil] attr_reader :app_dirs_pattern # @return [LineCache] attr_reader :linecache # @return [Integer, nil] attr_reader :context_lines # @return [Proc, nil] attr_reader :backtrace_cleanup_callback # @param project_root [String] # @param app_dirs_pattern [Regexp, nil] # @param linecache [LineCache] # @param context_lines [Integer, nil] # @param backtrace_cleanup_callback [Proc, nil] # @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) @project_root = project_root @app_dirs_pattern = app_dirs_pattern @linecache = linecache @context_lines = context_lines @backtrace_cleanup_callback = backtrace_cleanup_callback end # Generates a StacktraceInterface with the given backtrace. # You can pass a block to customize/exclude frames: # # @example # builder.build(backtrace) do |frame| # if frame.module.match?(/a_gem/) # nil # else # frame # end # end # @param backtrace [Array] # @param frame_callback [Proc] # @yieldparam frame [StacktraceInterface::Frame] # @return [StacktraceInterface] def build(backtrace:, &frame_callback) parsed_lines = parse_backtrace_lines(backtrace).select(&:file) frames = parsed_lines.reverse.map do |line| frame = convert_parsed_line_into_frame(line) frame = frame_callback.call(frame) if frame_callback frame end.compact StacktraceInterface.new(frames: frames) end private def convert_parsed_line_into_frame(line) frame = StacktraceInterface::Frame.new(project_root, line) frame.set_context(linecache, context_lines) if context_lines frame end def parse_backtrace_lines(backtrace) Backtrace.parse( backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback ).lines end end end sentry-ruby-core-5.3.0/lib/sentry/interfaces/threads.rb0000644000004100000410000000221014232765714023202 0ustar www-datawww-data# frozen_string_literal: true module Sentry class ThreadsInterface # @param crashed [Boolean] # @param stacktrace [Array] def initialize(crashed: false, stacktrace: nil) @id = Thread.current.object_id @name = Thread.current.name @current = true @crashed = crashed @stacktrace = stacktrace end # @return [Hash] def to_hash { values: [ { id: @id, name: @name, crashed: @crashed, current: @current, stacktrace: @stacktrace&.to_hash } ] } end # Builds the ThreadsInterface with given backtrace and stacktrace_builder. # Patch this method if you want to change a threads interface's stacktrace frames. # @see StacktraceBuilder.build # @param backtrace [Array] # @param stacktrace_builder [StacktraceBuilder] # @param crashed [Hash] # @return [ThreadsInterface] def self.build(backtrace:, stacktrace_builder:, **options) stacktrace = stacktrace_builder.build(backtrace: backtrace) if backtrace new(**options, stacktrace: stacktrace) end end end sentry-ruby-core-5.3.0/lib/sentry/interfaces/exception.rb0000644000004100000410000000242414232765714023555 0ustar www-datawww-data# frozen_string_literal: true require "set" module Sentry class ExceptionInterface < Interface # @param exceptions [Array] def initialize(exceptions:) @values = exceptions end # @return [Hash] def to_hash data = super data[:values] = data[:values].map(&:to_hash) if data[:values] data end # Builds ExceptionInterface with given exception and stacktrace_builder. # @param exception [Exception] # @param stacktrace_builder [StacktraceBuilder] # @see SingleExceptionInterface#build_with_stacktrace # @see SingleExceptionInterface#initialize # @return [ExceptionInterface] def self.build(exception:, stacktrace_builder:) exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exception).reverse processed_backtrace_ids = Set.new exceptions = exceptions.map do |e| if e.backtrace && !processed_backtrace_ids.include?(e.backtrace.object_id) processed_backtrace_ids << e.backtrace.object_id SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder) else SingleExceptionInterface.new(exception: exception) end end new(exceptions: exceptions) end end end sentry-ruby-core-5.3.0/lib/sentry/interfaces/single_exception.rb0000644000004100000410000000326214232765714025117 0ustar www-datawww-data# frozen_string_literal: true require "sentry/utils/exception_cause_chain" module Sentry class SingleExceptionInterface < Interface include CustomInspection SKIP_INSPECTION_ATTRIBUTES = [:@stacktrace] PROBLEMATIC_LOCAL_VALUE_REPLACEMENT = "[ignored due to error]".freeze OMISSION_MARK = "...".freeze MAX_LOCAL_BYTES = 1024 attr_reader :type, :value, :module, :thread_id, :stacktrace def initialize(exception:, stacktrace: nil) @type = exception.class.to_s @value = (exception.message || "").byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES) @module = exception.class.to_s.split('::')[0...-1].join('::') @thread_id = Thread.current.object_id @stacktrace = stacktrace end def to_hash data = super data[:stacktrace] = data[:stacktrace].to_hash if data[:stacktrace] data end # patch this method if you want to change an exception's stacktrace frames # also see `StacktraceBuilder.build`. def self.build_with_stacktrace(exception:, stacktrace_builder:) stacktrace = stacktrace_builder.build(backtrace: exception.backtrace) if locals = exception.instance_variable_get(:@sentry_locals) locals.each do |k, v| locals[k] = begin v = v.inspect unless v.is_a?(String) if v.length >= MAX_LOCAL_BYTES v = v.byteslice(0..MAX_LOCAL_BYTES - 1) + OMISSION_MARK end v rescue StandardError PROBLEMATIC_LOCAL_VALUE_REPLACEMENT end end stacktrace.frames.last.vars = locals end new(exception: exception, stacktrace: stacktrace) end end end sentry-ruby-core-5.3.0/lib/sentry/interfaces/stacktrace.rb0000644000004100000410000000431314232765714023702 0ustar www-datawww-data# frozen_string_literal: true module Sentry class StacktraceInterface # @return [] attr_reader :frames # @param frames [] def initialize(frames:) @frames = frames end # @return [Hash] def to_hash { frames: @frames.map(&:to_hash) } end # @return [String] def inspect @frames.map(&:to_s) end private # Not actually an interface, but I want to use the same style 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) @project_root = project_root @abs_path = line.file @function = line.method if line.method @lineno = line.number @in_app = line.in_app @module = line.module_name if line.module_name @filename = compute_filename end def to_s "#{@filename}:#{@lineno}" end def compute_filename return if abs_path.nil? prefix = if under_project_root? && in_app @project_root elsif under_project_root? longest_load_path || @project_root else longest_load_path end prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path end def set_context(linecache, context_lines) return unless abs_path @pre_context, @context_line, @post_context = \ linecache.get_file_context(abs_path, lineno, context_lines) end def to_hash(*args) data = super(*args) data.delete(:vars) unless vars && !vars.empty? data.delete(:pre_context) unless pre_context && !pre_context.empty? data.delete(:post_context) unless post_context && !post_context.empty? data.delete(:context_line) unless context_line && !context_line.empty? data end private def under_project_root? @project_root && abs_path.start_with?(@project_root) end def longest_load_path $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size) end end end end sentry-ruby-core-5.3.0/lib/sentry/error_event.rb0000644000004100000410000000172714232765714021773 0ustar www-datawww-data# frozen_string_literal: true module Sentry # ErrorEvent represents error or normal message events. class ErrorEvent < Event # @return [ExceptionInterface] attr_reader :exception # @return [ThreadsInterface] attr_reader :threads # @return [Hash] def to_hash data = super data[:threads] = threads.to_hash if threads data[:exception] = exception.to_hash if exception data end # @!visibility private def add_threads_interface(backtrace: nil, **options) @threads = ThreadsInterface.build( backtrace: backtrace, stacktrace_builder: @stacktrace_builder, **options ) end # @!visibility private def add_exception_interface(exception) if exception.respond_to?(:sentry_context) @extra.merge!(exception.sentry_context) end @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder) end end end sentry-ruby-core-5.3.0/lib/sentry/net/0000755000004100000410000000000014232765714017673 5ustar www-datawww-datasentry-ruby-core-5.3.0/lib/sentry/net/http.rb0000644000004100000410000000647414232765714021212 0ustar www-datawww-data# frozen_string_literal: true require "net/http" module Sentry # @api private module Net module HTTP OP_NAME = "http.client" BREADCRUMB_CATEGORY = "net.http" # To explain how the entire thing works, we need to know how the original Net::HTTP#request works # Here's part of its definition. As you can see, it usually calls itself inside a #start block # # ``` # def request(req, body = nil, &block) # unless started? # start { # req['connection'] ||= 'close' # return request(req, body, &block) # <- request will be called for the second time from the first call # } # end # # ..... # end # ``` # # So we're only instrumenting request when `Net::HTTP` is already started def request(req, body = nil, &block) return super unless started? sentry_span = start_sentry_span set_sentry_trace_header(req, sentry_span) super.tap do |res| record_sentry_breadcrumb(req, res) record_sentry_span(req, res, sentry_span) end end private def set_sentry_trace_header(req, sentry_span) return unless sentry_span trace = Sentry.get_current_client.generate_sentry_trace(sentry_span) req[SENTRY_TRACE_HEADER_NAME] = trace if trace end def record_sentry_breadcrumb(req, res) return unless Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger) return if from_sentry_sdk? request_info = extract_request_info(req) crumb = Sentry::Breadcrumb.new( level: :info, category: BREADCRUMB_CATEGORY, type: :info, data: { status: res.code.to_i, **request_info } ) Sentry.add_breadcrumb(crumb) end def record_sentry_span(req, res, sentry_span) return unless Sentry.initialized? && sentry_span request_info = extract_request_info(req) sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}") sentry_span.set_data(:status, res.code.to_i) finish_sentry_span(sentry_span) end def start_sentry_span return unless Sentry.initialized? && span = Sentry.get_current_scope.get_span return if from_sentry_sdk? return if span.sampled == false span.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) end def finish_sentry_span(sentry_span) return unless Sentry.initialized? && sentry_span sentry_span.set_timestamp(Sentry.utc_now.to_f) end def from_sentry_sdk? dsn = Sentry.configuration.dsn dsn && dsn.host == self.address end def extract_request_info(req) uri = req.uri || URI.parse("#{use_ssl? ? 'https' : 'http'}://#{address}#{req.path}") url = "#{uri.scheme}://#{uri.host}#{uri.path}" rescue uri.to_s result = { method: req.method, url: url } if Sentry.configuration.send_default_pii result[:url] = result[:url] + "?#{uri.query}" result[:body] = req.body end result end end end end Sentry.register_patch do patch = Sentry::Net::HTTP Net::HTTP.send(:prepend, patch) unless Net::HTTP.ancestors.include?(patch) end sentry-ruby-core-5.3.0/lib/sentry/dsn.rb0000644000004100000410000000222714232765714020221 0ustar www-datawww-data# frozen_string_literal: true require "uri" module Sentry class DSN 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 def initialize(dsn_string) @raw_value = dsn_string uri = URI.parse(dsn_string) uri_path = uri.path.split('/') if uri.user # DSN-style string @project_id = uri_path.pop @public_key = uri.user @secret_key = !(uri.password.nil? || uri.password.empty?) ? uri.password : nil end @scheme = uri.scheme @host = uri.host @port = uri.port if uri.port @path = uri_path.join('/') end def valid? REQUIRED_ATTRIBUTES.all? { |k| public_send(k) } end def to_s @raw_value end def server server = "#{scheme}://#{host}" server += ":#{port}" unless port == PORT_MAP[scheme] server end def csp_report_uri "#{server}/api/#{project_id}/security/?sentry_key=#{public_key}" end def envelope_endpoint "#{path}/api/#{project_id}/envelope/" end end end sentry-ruby-core-5.3.0/lib/sentry/rack/0000755000004100000410000000000014232765714020025 5ustar www-datawww-datasentry-ruby-core-5.3.0/lib/sentry/rack/capture_exceptions.rb0000644000004100000410000000403414232765714024257 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Rack class CaptureExceptions def initialize(app) @app = app end def call(env) return @app.call(env) unless Sentry.initialized? # 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 scope.clear_breadcrumbs scope.set_transaction_name(env["PATH_INFO"]) if env["PATH_INFO"] scope.set_rack_env(env) transaction = start_transaction(env, scope) scope.set_span(transaction) if transaction begin response = @app.call(env) rescue Sentry::Error finish_transaction(transaction, 500) raise # Don't capture Sentry errors rescue Exception => e capture_exception(e) finish_transaction(transaction, 500) raise end exception = collect_exception(env) capture_exception(exception) if exception finish_transaction(transaction, response[0]) response end end end private def collect_exception(env) env['rack.exception'] || env['sinatra.error'] end def transaction_op "rack.request".freeze end def capture_exception(exception) Sentry.capture_exception(exception) end def start_transaction(env, scope) sentry_trace = env["HTTP_SENTRY_TRACE"] options = { name: scope.transaction_name, op: transaction_op } transaction = Sentry::Transaction.from_sentry_trace(sentry_trace, **options) if sentry_trace Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options) end def finish_transaction(transaction, status_code) return unless transaction transaction.set_http_status(status_code) transaction.finish end end end end sentry-ruby-core-5.3.0/lib/sentry/client.rb0000644000004100000410000001475514232765714020724 0ustar www-datawww-data# frozen_string_literal: true require "sentry/transport" module Sentry class Client include LoggingHelper # The Transport object that'll send events for the client. # @return [Transport] attr_reader :transport # @!macro configuration attr_reader :configuration # @deprecated Use Sentry.logger to retrieve the current logger instead. attr_reader :logger # @param configuration [Configuration] def initialize(configuration) @configuration = configuration @logger = configuration.logger if transport_class = configuration.transport.transport_class @transport = transport_class.new(configuration) else @transport = case configuration.dsn&.scheme when 'http', 'https' HTTPTransport.new(configuration) else DummyTransport.new(configuration) end end end # Applies the given scope's data to the event and sends it to Sentry. # @param event [Event] the event to be sent. # @param scope [Scope] the scope with contextual data that'll be applied to the event before it's sent. # @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors. # @return [Event, nil] def capture_event(event, scope, hint = {}) return unless configuration.sending_allowed? unless event.is_a?(TransactionEvent) || configuration.sample_allowed? transport.record_lost_event(:sample_rate, 'event') return end event_type = event.is_a?(Event) ? event.type : event["type"] event = scope.apply_to_event(event, hint) if event.nil? log_info("Discarded event because one of the event processors returned nil") transport.record_lost_event(:event_processor, event_type) return 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, event_type) unless queued else send_event(event, hint) end event rescue => e log_error("Event capturing failed", e, debug: configuration.debug) nil end # Initializes an Event object with the given exception. Returns `nil` if the exception's class is excluded from reporting. # @param exception [Exception] the exception to be reported. # @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors. # @return [Event, nil] def event_from_exception(exception, hint = {}) return unless @configuration.sending_allowed? && @configuration.exception_class_allowed?(exception) integration_meta = Sentry.integrations[hint[:integration]] ErrorEvent.new(configuration: configuration, integration_meta: integration_meta).tap do |event| event.add_exception_interface(exception) event.add_threads_interface(crashed: true) event.level = :error end end # Initializes an Event object with the given message. # @param message [String] the message to be reported. # @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors. # @return [Event] def event_from_message(message, hint = {}, backtrace: nil) return unless @configuration.sending_allowed? integration_meta = Sentry.integrations[hint[:integration]] event = ErrorEvent.new(configuration: configuration, integration_meta: integration_meta, message: message) event.add_threads_interface(backtrace: backtrace || caller) event.level = :error event end # Initializes an Event object with the given Transaction object. # @param transaction [Transaction] the transaction to be recorded. # @return [TransactionEvent] def event_from_transaction(transaction) TransactionEvent.new(configuration: configuration).tap do |event| event.transaction = transaction.name event.contexts.merge!(trace: transaction.get_trace_context) event.timestamp = transaction.timestamp event.start_timestamp = transaction.start_timestamp event.tags = transaction.tags finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction } event.spans = finished_spans.map(&:to_hash) end end # @!macro send_event def send_event(event, hint = nil) event_type = event.is_a?(Event) ? event.type : event["type"] if event_type != TransactionEvent::TYPE && configuration.before_send event = configuration.before_send.call(event, hint) if event.nil? log_info("Discarded event because before_send returned nil") transport.record_lost_event(:before_send, 'event') return end end transport.send_event(event) event rescue => e loggable_event_type = event_type.capitalize log_error("#{loggable_event_type} sending failed", e, debug: configuration.debug) event_info = Event.get_log_message(event.to_hash) log_info("Unreported #{loggable_event_type}: #{event_info}") transport.record_lost_event(:network_error, event_type) raise end # Generates a Sentry trace for distribted tracing from the given Span. # Returns `nil` if `config.propagate_traces` is `false`. # @param span [Span] the span to generate trace from. # @return [String, nil] def generate_sentry_trace(span) return unless configuration.propagate_traces trace = span.to_sentry_trace log_debug("[Tracing] Adding #{SENTRY_TRACE_HEADER_NAME} header to outgoing request: #{trace}") trace end private def dispatch_background_event(event, hint) Sentry.background_worker.perform do send_event(event, hint) end end def dispatch_async_event(async_block, event, hint) # We have to convert to a JSON-like hash, because background job # processors (esp ActiveJob) may not like weird types in the event hash event_hash = event.to_json_compatible if async_block.arity == 2 hint = JSON.parse(JSON.generate(hint)) async_block.call(event_hash, hint) else async_block.call(event_hash) end rescue => e log_error("Async #{event_hash["type"]} sending failed", e, debug: configuration.debug) send_event(event, hint) end end end sentry-ruby-core-5.3.0/lib/sentry/breadcrumb/0000755000004100000410000000000014232765714021213 5ustar www-datawww-datasentry-ruby-core-5.3.0/lib/sentry/breadcrumb/sentry_logger.rb0000644000004100000410000000506414232765714024430 0ustar www-datawww-data# frozen_string_literal: true require 'logger' module Sentry class Breadcrumb module SentryLogger LEVELS = { ::Logger::DEBUG => 'debug', ::Logger::INFO => 'info', ::Logger::WARN => 'warn', ::Logger::ERROR => 'error', ::Logger::FATAL => 'fatal' }.freeze def add(*args, &block) super add_breadcrumb(*args, &block) nil end def add_breadcrumb(severity, message = nil, progname = nil) # because the breadcrumbs now belongs to different Hub's Scope in different threads # we need to make sure the current thread's Hub has been set before adding breadcrumbs return unless Sentry.get_current_hub category = "logger" # this is because the nature of Ruby Logger class: # # when given 1 argument, the argument will become both message and progname # # ``` # logger.info("foo") # # message == progname == "foo" # ``` # # and to specify progname with a different message, # we need to pass the progname as the argument and pass the message as a proc # # ``` # logger.info("progname") { "the message" } # ``` # # so the condition below is to replicate the similar behavior if message.nil? if block_given? message = yield category = progname else message = progname end end return if ignored_logger?(progname) || message == "" # some loggers will add leading/trailing space as they (incorrectly, mind you) # think of logging as a shortcut to std{out,err} message = message.to_s.strip last_crumb = current_breadcrumbs.peek # try to avoid dupes from logger broadcasts if last_crumb.nil? || last_crumb.message != message level = Sentry::Breadcrumb::SentryLogger::LEVELS.fetch(severity, nil) crumb = Sentry::Breadcrumb.new( level: level, category: category, message: message, type: severity >= 3 ? "error" : level ) Sentry.add_breadcrumb(crumb, hint: { severity: severity }) end end private def ignored_logger?(progname) progname == LOGGER_PROGNAME || Sentry.configuration.exclude_loggers.include?(progname) end def current_breadcrumbs Sentry.get_current_scope.breadcrumbs end end end end ::Logger.send(:prepend, Sentry::Breadcrumb::SentryLogger) sentry-ruby-core-5.3.0/lib/sentry/background_worker.rb0000644000004100000410000000375514232765714023154 0ustar www-datawww-data# frozen_string_literal: true require "concurrent/executor/thread_pool_executor" require "concurrent/executor/immediate_executor" require "concurrent/configuration" module Sentry class BackgroundWorker include LoggingHelper attr_reader :max_queue, :number_of_threads # @deprecated Use Sentry.logger to retrieve the current logger instead. attr_reader :logger attr_accessor :shutdown_timeout def initialize(configuration) @max_queue = 30 @shutdown_timeout = 1 @number_of_threads = configuration.background_worker_threads @logger = configuration.logger @debug = configuration.debug @shutdown_callback = nil @executor = if configuration.async log_debug("config.async is set, BackgroundWorker is disabled") Concurrent::ImmediateExecutor.new elsif @number_of_threads == 0 log_debug("config.background_worker_threads is set to 0, all events will be sent synchronously") Concurrent::ImmediateExecutor.new else log_debug("Initializing the background worker with #{@number_of_threads} threads") executor = Concurrent::ThreadPoolExecutor.new( min_threads: 0, max_threads: @number_of_threads, max_queue: @max_queue, fallback_policy: :discard ) @shutdown_callback = proc do executor.shutdown executor.wait_for_termination(@shutdown_timeout) end executor end end # if you want to monkey-patch this method, please override `_perform` instead def perform(&block) @executor.post do begin _perform(&block) rescue Exception => e log_error("exception happened in background worker", e, debug: @debug) end end end def shutdown log_debug("Shutting down background worker") @shutdown_callback&.call end private def _perform(&block) block.call end end end sentry-ruby-core-5.3.0/lib/sentry/release_detector.rb0000644000004100000410000000215414232765714022745 0ustar www-datawww-data# frozen_string_literal: true module Sentry # @api private class ReleaseDetector class << self def detect_release(project_root:, running_on_heroku:) detect_release_from_env || detect_release_from_git || detect_release_from_capistrano(project_root) || detect_release_from_heroku(running_on_heroku) end def detect_release_from_heroku(running_on_heroku) return unless running_on_heroku 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') if File.exist?(revision_file) File.read(revision_file).strip elsif File.exist?(revision_log) File.open(revision_log).to_a.last.strip.sub(/.*as release ([0-9]+).*/, '\1') end end def detect_release_from_git Sentry.sys_command("git rev-parse --short HEAD") if File.directory?(".git") end def detect_release_from_env ENV['SENTRY_RELEASE'] end end end end sentry-ruby-core-5.3.0/lib/sentry/configuration.rb0000644000004100000410000003621614232765714022311 0ustar www-datawww-data# frozen_string_literal: true require "concurrent/utility/processor_counter" require "sentry/utils/exception_cause_chain" require 'sentry/utils/custom_inspection' require "sentry/dsn" require "sentry/release_detector" require "sentry/transport/configuration" require "sentry/linecache" require "sentry/interfaces/stacktrace_builder" module Sentry class Configuration include CustomInspection include LoggingHelper # Directories to be recognized as part of your app. e.g. if you # have an `engines` dir at the root of your project, you may want # to set this to something like /(app|config|engines|lib)/ # # @return [Regexp, nil] attr_accessor :app_dirs_pattern # Provide an object that responds to `call` to send events asynchronously. # E.g.: lambda { |event| Thread.new { Sentry.send_event(event) } } # # @deprecated It will be removed in the next major release. Please read https://github.com/getsentry/sentry-ruby/issues/1522 for more information # @return [Proc, nil] attr_reader :async # to send events in a non-blocking way, sentry-ruby has its own background worker # by default, the worker holds a thread pool that has [the number of processors] threads # but you can configure it with this configuration option # E.g.: config.background_worker_threads = 5 # # if you want to send events synchronously, set the value to 0 # E.g.: config.background_worker_threads = 0 # @return [Integer] attr_accessor :background_worker_threads # a proc/lambda that takes an array of stack traces # it'll be used to silence (reduce) backtrace of the exception # # @example # config.backtrace_cleanup_callback = lambda do |backtrace| # Rails.backtrace_cleaner.clean(backtrace) # end # # @return [Proc, nil] attr_accessor :backtrace_cleanup_callback # Optional Proc, called before adding the breadcrumb to the current scope # @example # config.before = lambda do |breadcrumb, hint| # breadcrumb.message = 'a' # breadcrumb # end # @return [Proc] attr_reader :before_breadcrumb # Optional Proc, called before sending an event to the server # @example # config.before_send = lambda do |event, hint| # # skip ZeroDivisionError exceptions # # note: hint[:exception] would be a String if you use async callback # if hint[:exception].is_a?(ZeroDivisionError) # nil # else # event # end # end # @return [Proc] attr_reader :before_send # An array of breadcrumbs loggers to be used. Available options are: # - :sentry_logger # - :http_logger # - :redis_logger # # And if you also use sentry-rails: # - :active_support_logger # - :monotonic_active_support_logger # # @return [Array] attr_reader :breadcrumbs_logger # Whether to capture local variables from the raised exception's frame. Default is false. # @return [Boolean] attr_accessor :capture_exception_frame_locals # Max number of breadcrumbs a breadcrumb buffer can hold # @return [Integer] attr_accessor :max_breadcrumbs # Number of lines of code context to capture, or nil for none # @return [Integer, nil] attr_accessor :context_lines # RACK_ENV by default. # @return [String] attr_reader :environment # Whether the SDK should run in the debugging mode. Default is false. # If set to true, SDK errors will be logged with backtrace # @return [Boolean] attr_accessor :debug # the dsn value, whether it's set via `config.dsn=` or `ENV["SENTRY_DSN"]` # @return [String] attr_reader :dsn # Whitelist of enabled_environments that will send notifications to Sentry. Array of Strings. # @return [Array] attr_accessor :enabled_environments # Logger 'progname's to exclude from breadcrumbs # @return [Array] attr_accessor :exclude_loggers # Array of exception classes that should never be sent. See IGNORE_DEFAULT. # You should probably append to this rather than overwrite it. # @return [Array] attr_accessor :excluded_exceptions # Boolean to check nested exceptions when deciding if to exclude. Defaults to true # @return [Boolean] attr_accessor :inspect_exception_causes_for_exclusion alias inspect_exception_causes_for_exclusion? inspect_exception_causes_for_exclusion # You may provide your own LineCache for matching paths with source files. # This may be useful if you need to get source code from places other than the disk. # @see LineCache # @return [LineCache] attr_accessor :linecache # Logger used by Sentry. In Rails, this is the Rails logger, otherwise # Sentry provides its own Sentry::Logger. # @return [Logger] attr_accessor :logger # Project directory root for in_app detection. Could be Rails root, etc. # Set automatically for Rails. # @return [String] attr_accessor :project_root # Insert sentry-trace to outgoing requests' headers # @return [Boolean] attr_accessor :propagate_traces # Array of rack env parameters to be included in the event sent to sentry. # @return [Array] attr_accessor :rack_env_whitelist # Release tag to be passed with every event sent to Sentry. # We automatically try to set this to a git SHA or Capistrano release. # @return [String] attr_accessor :release # The sampling factor to apply to events. A value of 0.0 will not send # any events, and a value of 1.0 will send 100% of events. # @return [Float] attr_accessor :sample_rate # Include module versions in reports - boolean. # @return [Boolean] attr_accessor :send_modules # When send_default_pii's value is false (default), sensitive information like # - user ip # - user cookie # - request body # - query string # will not be sent to Sentry. # @return [Boolean] attr_accessor :send_default_pii # Allow to skip Sentry emails within rake tasks # @return [Boolean] attr_accessor :skip_rake_integration # IP ranges for trusted proxies that will be skipped when calculating IP address. attr_accessor :trusted_proxies # @return [String] attr_accessor :server_name # Return a Transport::Configuration object for transport-related configurations. # @return [Transport] attr_reader :transport # Take a float between 0.0 and 1.0 as the sample rate for tracing events (transactions). # @return [Float] attr_accessor :traces_sample_rate # Take a Proc that controls the sample rate for every tracing event, e.g. # @example # config.traces_sampler = lambda do |tracing_context| # # tracing_context[:transaction_context] contains the information about the transaction # # tracing_context[:parent_sampled] contains the transaction's parent's sample decision # true # return value can be a boolean or a float between 0.0 and 1.0 # end # @return [Proc] attr_accessor :traces_sampler # Send diagnostic client reports about dropped events, true by default # tries to attach to an existing envelope max once every 30s # @return [Boolean] attr_accessor :send_client_reports # Track sessions in request/response cycles automatically # @return [Boolean] attr_accessor :auto_session_tracking # these are not config options # @!visibility private attr_reader :errors, :gem_specs # 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' ].freeze RACK_ENV_WHITELIST_DEFAULT = %w( REMOTE_ADDR SERVER_NAME SERVER_PORT ).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 LOG_PREFIX = "** [Sentry] ".freeze MODULE_SEPARATOR = "::".freeze SKIP_INSPECTION_ATTRIBUTES = [:@linecache, :@stacktrace_builder] # Post initialization callbacks are called at the end of initialization process # allowing extending the configuration of sentry-ruby by multiple extensions @@post_initialization_callbacks = [] def initialize self.app_dirs_pattern = nil self.debug = false self.background_worker_threads = Concurrent.processor_count self.backtrace_cleanup_callback = nil self.max_breadcrumbs = BreadcrumbBuffer::DEFAULT_SIZE self.breadcrumbs_logger = [] self.context_lines = 3 self.capture_exception_frame_locals = false self.environment = environment_from_env self.enabled_environments = [] self.exclude_loggers = [] self.excluded_exceptions = IGNORE_DEFAULT.dup self.inspect_exception_causes_for_exclusion = true self.linecache = ::Sentry::LineCache.new self.logger = ::Sentry::Logger.new(STDOUT) self.project_root = Dir.pwd self.propagate_traces = true self.sample_rate = 1.0 self.send_modules = true self.send_default_pii = false self.skip_rake_integration = false self.send_client_reports = true self.auto_session_tracking = true self.trusted_proxies = [] self.dsn = ENV['SENTRY_DSN'] self.server_name = server_name_from_env self.before_send = nil self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT self.traces_sample_rate = nil self.traces_sampler = nil @transport = Transport::Configuration.new @gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map) run_post_initialization_callbacks end def dsn=(value) @dsn = init_dsn(value) end alias server= dsn= def async=(value) check_callable!("async", value) @async = value end def breadcrumbs_logger=(logger) loggers = if logger.is_a?(Array) logger else Array(logger) end require "sentry/breadcrumb/sentry_logger" if loggers.include?(:sentry_logger) @breadcrumbs_logger = logger end def before_send=(value) check_callable!("before_send", value) @before_send = value end def before_breadcrumb=(value) check_callable!("before_breadcrumb", value) @before_breadcrumb = value end def environment=(environment) @environment = environment.to_s end def sending_allowed? @errors = [] valid? && capture_in_environment? end def sample_allowed? return true if sample_rate == 1.0 Random.rand < sample_rate end def exception_class_allowed?(exc) if exc.is_a?(Sentry::Error) # Try to prevent error reporting loops log_debug("Refusing to capture Sentry error: #{exc.inspect}") false elsif excluded_exception?(exc) log_debug("User excluded error: #{exc.inspect}") false else true end end def enabled_in_current_env? enabled_environments.empty? || enabled_environments.include?(environment) end def tracing_enabled? !!((@traces_sample_rate && @traces_sample_rate >= 0.0 && @traces_sample_rate <= 1.0) || @traces_sampler) && sending_allowed? end # @return [String, nil] def csp_report_uri if dsn && dsn.valid? uri = dsn.csp_report_uri uri += "&sentry_release=#{CGI.escape(release)}" if release && !release.empty? uri += "&sentry_environment=#{CGI.escape(environment)}" if environment && !environment.empty? uri end end # @api private def stacktrace_builder @stacktrace_builder ||= StacktraceBuilder.new( project_root: @project_root.to_s, app_dirs_pattern: @app_dirs_pattern, linecache: @linecache, context_lines: @context_lines, backtrace_cleanup_callback: @backtrace_cleanup_callback ) end # @api private def detect_release return unless sending_allowed? self.release ||= ReleaseDetector.detect_release(project_root: project_root, running_on_heroku: running_on_heroku?) if running_on_heroku? && release.nil? log_warn(HEROKU_DYNO_METADATA_MESSAGE) end rescue => e log_error("Error detecting release", e, debug: debug) end # @api private def error_messages @errors = [@errors[0]] + @errors[1..-1].map(&:downcase) # fix case of all but first @errors.join(", ") end private def check_callable!(name, value) unless value == nil || value.respond_to?(:call) raise ArgumentError, "#{name} must be callable (or nil to disable)" end end def init_dsn(dsn_string) return if dsn_string.nil? || dsn_string.empty? DSN.new(dsn_string) end def excluded_exception?(incoming_exception) excluded_exception_classes.any? do |excluded_exception| matches_exception?(excluded_exception, incoming_exception) end end def excluded_exception_classes @excluded_exception_classes ||= excluded_exceptions.map { |e| get_exception_class(e) } end def get_exception_class(x) x.is_a?(Module) ? x : safe_const_get(x) end def matches_exception?(excluded_exception_class, incoming_exception) if inspect_exception_causes_for_exclusion? Sentry::Utils::ExceptionCauseChain.exception_to_array(incoming_exception).any? { |cause| excluded_exception_class === cause } else excluded_exception_class === incoming_exception end end def safe_const_get(x) x = x.to_s unless x.is_a?(String) Object.const_get(x) rescue NameError # There's no way to safely ask if a constant exist for an unknown string nil end def capture_in_environment? return true if enabled_in_current_env? @errors << "Not configured to send/capture in environment '#{environment}'" false end def valid? if @dsn&.valid? true else @errors << "DSN not set or not valid" false end end def environment_from_env 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'] else # Try to resolve the hostname to an FQDN, but fall back to whatever # the load name is. Socket.gethostname || Socket.gethostbyname(hostname).first rescue server_name end end def running_on_heroku? File.directory?("/etc/heroku") && !ENV["CI"] end def run_post_initialization_callbacks self.class.post_initialization_callbacks.each do |hook| instance_eval(&hook) end end # allow extensions to add their hooks to the Configuration class def self.add_post_initialization_callback(&block) self.post_initialization_callbacks << block end protected def self.post_initialization_callbacks @@post_initialization_callbacks end end end sentry-ruby-core-5.3.0/lib/sentry/transport/0000755000004100000410000000000014232765714021141 5ustar www-datawww-datasentry-ruby-core-5.3.0/lib/sentry/transport/configuration.rb0000644000004100000410000000117014232765714024334 0ustar www-datawww-data# frozen_string_literal: true module Sentry class Transport class Configuration attr_accessor :timeout, :open_timeout, :proxy, :ssl, :ssl_ca_file, :ssl_verification, :encoding attr_reader :transport_class def initialize @ssl_verification = true @open_timeout = 1 @timeout = 2 @encoding = HTTPTransport::GZIP_ENCODING end def transport_class=(klass) unless klass.is_a?(Class) raise Sentry::Error.new("config.transport.transport_class must a class. got: #{klass.class}") end @transport_class = klass end end end end sentry-ruby-core-5.3.0/lib/sentry/transport/http_transport.rb0000644000004100000410000001151414232765714024563 0ustar www-datawww-data# frozen_string_literal: true require "net/http" require "zlib" module Sentry class HTTPTransport < Transport GZIP_ENCODING = "gzip" GZIP_THRESHOLD = 1024 * 30 CONTENT_TYPE = 'application/x-sentry-envelope' DEFAULT_DELAY = 60 RETRY_AFTER_HEADER = "retry-after" RATE_LIMIT_HEADER = "x-sentry-rate-limits" USER_AGENT = "sentry-ruby/#{Sentry::VERSION}" def initialize(*args) super @endpoint = @dsn.envelope_endpoint log_debug("Sentry HTTP Transport will connect to #{@dsn.server}") end def send_data(data) encoding = "" if should_compress?(data) data = Zlib.gzip(data) encoding = GZIP_ENCODING end headers = { 'Content-Type' => CONTENT_TYPE, 'Content-Encoding' => encoding, 'X-Sentry-Auth' => generate_auth_header, 'User-Agent' => USER_AGENT } response = conn.start do |http| request = ::Net::HTTP::Post.new(@endpoint, headers) request.body = data http.request(request) end if response.code.match?(/\A2\d{2}/) if has_rate_limited_header?(response) handle_rate_limited_response(response) end else error_info = "the server responded with status #{response.code}" if response.code == "429" handle_rate_limited_response(response) else error_info += "\nbody: #{response.body}" error_info += " Error in headers is: #{response['x-sentry-error']}" if response['x-sentry-error'] end raise Sentry::ExternalError, error_info end rescue SocketError => e raise Sentry::ExternalError.new(e.message) end private def has_rate_limited_header?(headers) headers[RETRY_AFTER_HEADER] || headers[RATE_LIMIT_HEADER] end def handle_rate_limited_response(headers) rate_limits = if rate_limits = headers[RATE_LIMIT_HEADER] parse_rate_limit_header(rate_limits) elsif retry_after = headers[RETRY_AFTER_HEADER] # although Sentry doesn't send a date string back # based on HTTP specification, this could be a date string (instead of an integer) retry_after = retry_after.to_i retry_after = DEFAULT_DELAY if retry_after == 0 { nil => Time.now + retry_after } else { nil => Time.now + DEFAULT_DELAY } end rate_limits.each do |category, limit| if current_limit = @rate_limits[category] if current_limit < limit @rate_limits[category] = limit end else @rate_limits[category] = limit end end end def parse_rate_limit_header(rate_limit_header) time = Time.now result = {} limits = rate_limit_header.split(",") limits.each do |limit| next if limit.nil? || limit.empty? begin retry_after, categories = limit.strip.split(":").first(2) retry_after = time + retry_after.to_i categories = categories.split(";") if categories.empty? result[nil] = retry_after else categories.each do |category| result[category] = retry_after end end rescue StandardError end end result end def should_compress?(data) @transport_configuration.encoding == GZIP_ENCODING && data.bytesize >= GZIP_THRESHOLD end def conn server = URI(@dsn.server) connection = if proxy = normalize_proxy(@transport_configuration.proxy) ::Net::HTTP.new(server.hostname, server.port, proxy[:uri].hostname, proxy[:uri].port, proxy[:user], proxy[:password]) else ::Net::HTTP.new(server.hostname, server.port, nil) end connection.use_ssl = server.scheme == "https" connection.read_timeout = @transport_configuration.timeout connection.write_timeout = @transport_configuration.timeout if connection.respond_to?(:write_timeout) connection.open_timeout = @transport_configuration.open_timeout ssl_configuration.each do |key, value| connection.send("#{key}=", value) end connection end def normalize_proxy(proxy) return proxy unless proxy case proxy when String uri = URI(proxy) { uri: uri, user: uri.user, password: uri.password } when URI { uri: proxy, user: proxy.user, password: proxy.password } when Hash proxy end end def ssl_configuration configuration = { verify: @transport_configuration.ssl_verification, ca_file: @transport_configuration.ssl_ca_file }.merge(@transport_configuration.ssl || {}) configuration[:verify_mode] = configuration.delete(:verify) ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE configuration end end end sentry-ruby-core-5.3.0/lib/sentry/transport/dummy_transport.rb0000644000004100000410000000052414232765714024736 0ustar www-datawww-data# frozen_string_literal: true module Sentry class DummyTransport < Transport attr_accessor :events, :envelopes def initialize(*) super @events = [] @envelopes = [] end def send_event(event) @events << event end def send_envelope(envelope) @envelopes << envelope end end end sentry-ruby-core-5.3.0/lib/sentry/event.rb0000644000004100000410000001257614232765714020566 0ustar www-datawww-data# 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' module Sentry # This is an abstract class that defines the shared attributes of an event. # Please don't use it directly. The user-facing classes are its child classes. class Event TYPE = "event" # These are readable attributes. SERIALIZEABLE_ATTRIBUTES = %i( event_id level timestamp release environment server_name modules message user tags contexts extra fingerprint breadcrumbs transaction platform sdk type ) # These are writable attributes. WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i(type timestamp level) MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8 MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 200 SKIP_INSPECTION_ATTRIBUTES = [:@modules, :@stacktrace_builder, :@send_default_pii, :@trusted_proxies, :@rack_env_whitelist] include CustomInspection attr_writer(*WRITER_ATTRIBUTES) attr_reader(*SERIALIZEABLE_ATTRIBUTES) # @return [RequestInterface] attr_reader :request # @param configuration [Configuration] # @param integration_meta [Hash, nil] # @param message [String, nil] def initialize(configuration:, integration_meta: nil, message: nil) # Set some simple default values @event_id = SecureRandom.uuid.delete("-") @timestamp = Sentry.utc_now.iso8601 @platform = :ruby @type = self.class::TYPE @sdk = integration_meta || Sentry.sdk_meta @user = {} @extra = {} @contexts = {} @tags = {} @fingerprint = [] # configuration data that's directly used by events @server_name = configuration.server_name @environment = configuration.environment @release = configuration.release @modules = configuration.gem_specs if configuration.send_modules # configuration options to help events process data @send_default_pii = configuration.send_default_pii @trusted_proxies = configuration.trusted_proxies @stacktrace_builder = configuration.stacktrace_builder @rack_env_whitelist = configuration.rack_env_whitelist @message = (message || "").byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES) end class << self # @!visibility private def get_log_message(event_hash) message = event_hash[:message] || event_hash['message'] return message unless message.nil? || message.empty? message = get_message_from_exception(event_hash) return message unless message.nil? || message.empty? message = event_hash[:transaction] || event_hash["transaction"] return message unless message.nil? || message.empty? '' end # @!visibility private def get_message_from_exception(event_hash) if exception = event_hash.dig(:exception, :values, 0) "#{exception[:type]}: #{exception[:value]}" elsif exception = event_hash.dig("exception", "values", 0) "#{exception["type"]}: #{exception["value"]}" end end end # @deprecated This method will be removed in v5.0.0. Please just use Sentry.configuration # @return [Configuration] def configuration Sentry.configuration end # Sets the event's timestamp. # @param time [Time, Float] # @return [void] def timestamp=(time) @timestamp = time.is_a?(Time) ? time.to_f : time end # Sets the event's level. # @param level [String, Symbol] # @return [void] def level=(level) # needed to meet the Sentry spec @level = level.to_s == "warn" ? :warning : level end # Sets the event's request environment data with RequestInterface. # @see RequestInterface # @param env [Hash] # @return [void] 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 if request_id = Utils::RequestId.read_from(env) tags[:request_id] = request_id end end end # @return [Hash] def to_hash data = serialize_attributes data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs data[:request] = request.to_hash if request data end # @return [Hash] def to_json_compatible JSON.parse(JSON.generate(to_hash)) end private def add_request_interface(env) @request = Sentry::RequestInterface.new(env: env, send_default_pii: @send_default_pii, rack_env_whitelist: @rack_env_whitelist) end def serialize_attributes self.class::SERIALIZEABLE_ATTRIBUTES.each_with_object({}) do |att, memo| if value = public_send(att) memo[att] = value end end end # When behind a proxy (or if the user is using a proxy), we can't use # REMOTE_ADDR to determine the Event IP, and must use other headers instead. def calculate_real_ip_from_rack(env) Utils::RealIp.new( :remote_addr => env["REMOTE_ADDR"], :client_ip => env["HTTP_CLIENT_IP"], :real_ip => env["HTTP_X_REAL_IP"], :forwarded_for => env["HTTP_X_FORWARDED_FOR"], :trusted_proxies => @trusted_proxies ).calculate_ip end end end sentry-ruby-core-5.3.0/lib/sentry/scope.rb0000644000004100000410000001677414232765714020562 0ustar www-datawww-data# frozen_string_literal: true require "sentry/breadcrumb_buffer" require "etc" module Sentry class Scope include ArgumentCheckingHelper ATTRIBUTES = [:transaction_names, :contexts, :extra, :tags, :user, :level, :breadcrumbs, :fingerprint, :event_processors, :rack_env, :span, :session] attr_reader(*ATTRIBUTES) # @param max_breadcrumbs [Integer] the maximum number of breadcrumbs to be stored in the scope. def initialize(max_breadcrumbs: nil) @max_breadcrumbs = max_breadcrumbs set_default_value end # Resets the scope's attributes to defaults. # @return [void] def clear set_default_value end # Applies stored attributes and event processors to the given event. # @param event [Event] # @param hint [Hash] the hint data that'll be passed to event processors. # @return [Event] def apply_to_event(event, hint = nil) event.tags = tags.merge(event.tags) event.user = user.merge(event.user) event.extra = extra.merge(event.extra) event.contexts = contexts.merge(event.contexts) event.transaction = transaction_name if transaction_name if span event.contexts[:trace] = span.get_trace_context end event.fingerprint = fingerprint event.level = level event.breadcrumbs = breadcrumbs event.rack_env = rack_env if rack_env unless @event_processors.empty? @event_processors.each do |processor_block| event = processor_block.call(event, hint) end end event end # Adds the breadcrumb to the scope's breadcrumbs buffer. # @param breadcrumb [Breadcrumb] # @return [void] def add_breadcrumb(breadcrumb) breadcrumbs.record(breadcrumb) end # Clears the scope's breadcrumbs buffer # @return [void] def clear_breadcrumbs set_new_breadcrumb_buffer end # @return [Scope] def dup copy = super copy.breadcrumbs = breadcrumbs.dup copy.contexts = contexts.deep_dup copy.extra = extra.deep_dup copy.tags = tags.deep_dup copy.user = user.deep_dup copy.transaction_names = transaction_names.deep_dup copy.fingerprint = fingerprint.deep_dup copy.span = span.deep_dup copy.session = session.deep_dup copy end # Updates the scope's data from a given scope. # @param scope [Scope] # @return [void] def update_from_scope(scope) self.breadcrumbs = scope.breadcrumbs self.contexts = scope.contexts self.extra = scope.extra self.tags = scope.tags self.user = scope.user self.transaction_names = scope.transaction_names self.fingerprint = scope.fingerprint self.span = scope.span end # Updates the scope's data from the given options. # @param contexts [Hash] # @param extras [Hash] # @param tags [Hash] # @param user [Hash] # @param level [String, Symbol] # @param fingerprint [Array] # @return [void] def update_from_options( contexts: nil, extra: nil, tags: nil, user: nil, level: nil, fingerprint: nil ) self.contexts.merge!(contexts) if contexts self.extra.merge!(extra) if extra self.tags.merge!(tags) if tags self.user = user if user self.level = level if level self.fingerprint = fingerprint if fingerprint end # Sets the scope's rack_env attribute. # @param env [Hash] # @return [Hash] def set_rack_env(env) env = env || {} @rack_env = env end # Sets the scope's span attribute. # @param span [Span] # @return [Span] def set_span(span) check_argument_type!(span, Span) @span = span end # @!macro set_user def set_user(user_hash) check_argument_type!(user_hash, Hash) @user = user_hash end # @!macro set_extras def set_extras(extras_hash) check_argument_type!(extras_hash, Hash) @extra.merge!(extras_hash) end # Adds a new key-value pair to current extras. # @param key [String, Symbol] # @param value [Object] # @return [Hash] def set_extra(key, value) set_extras(key => value) end # @!macro set_tags def set_tags(tags_hash) check_argument_type!(tags_hash, Hash) @tags.merge!(tags_hash) end # Adds a new key-value pair to current tags. # @param key [String, Symbol] # @param value [Object] # @return [Hash] def set_tag(key, value) set_tags(key => value) end # Updates the scope's contexts attribute by merging with the old value. # @param contexts [Hash] # @return [Hash] def set_contexts(contexts_hash) check_argument_type!(contexts_hash, Hash) @contexts.merge!(contexts_hash) do |key, old, new| old.merge(new) end end # @!macro set_context def set_context(key, value) check_argument_type!(value, Hash) set_contexts(key => value) end # Sets the scope's level attribute. # @param level [String, Symbol] # @return [void] def set_level(level) @level = level end # Appends a new transaction name to the scope. # The "transaction" here does not refer to `Transaction` objects. # @param transaction_name [String] # @return [void] def set_transaction_name(transaction_name) @transaction_names << transaction_name end # Sets the currently active session on the scope. # @param session [Session, nil] # @return [void] def set_session(session) @session = session end # Returns current transaction name. # The "transaction" here does not refer to `Transaction` objects. # @return [String, nil] def transaction_name @transaction_names.last end # Returns the associated Transaction object. # @return [Transaction, nil] def get_transaction span.transaction if span end # Returns the associated Span object. # @return [Span, nil] def get_span span end # Sets the scope's fingerprint attribute. # @param fingerprint [Array] # @return [Array] def set_fingerprint(fingerprint) check_argument_type!(fingerprint, Array) @fingerprint = fingerprint end # Adds a new event processor [Proc] to the scope. # @param block [Proc] # @return [void] def add_event_processor(&block) @event_processors << block end protected # for duplicating scopes internally attr_writer(*ATTRIBUTES) private def set_default_value @contexts = { :os => self.class.os_context, :runtime => self.class.runtime_context } @extra = {} @tags = {} @user = {} @level = :error @fingerprint = [] @transaction_names = [] @event_processors = [] @rack_env = {} @span = nil @session = nil set_new_breadcrumb_buffer end def set_new_breadcrumb_buffer @breadcrumbs = BreadcrumbBuffer.new(@max_breadcrumbs) end class << self # @return [Hash] def os_context @os_context ||= begin uname = Etc.uname { name: uname[:sysname] || RbConfig::CONFIG["host_os"], version: uname[:version], build: uname[:release], kernel_version: uname[:version] } end end # @return [Hash] def runtime_context @runtime_context ||= { name: RbConfig::CONFIG["ruby_install_name"], version: RUBY_DESCRIPTION || Sentry.sys_command("ruby -v") } end end end end sentry-ruby-core-5.3.0/lib/sentry/span.rb0000644000004100000410000001345214232765714020400 0ustar www-datawww-data# frozen_string_literal: true require "securerandom" module Sentry class Span STATUS_MAP = { 400 => "invalid_argument", 401 => "unauthenticated", 403 => "permission_denied", 404 => "not_found", 409 => "already_exists", 429 => "resource_exhausted", 499 => "cancelled", 500 => "internal_error", 501 => "unimplemented", 503 => "unavailable", 504 => "deadline_exceeded" } # An uuid that can be used to identify a trace. # @return [String] attr_reader :trace_id # An uuid that can be used to identify the span. # @return [String] attr_reader :span_id # Span parent's span_id. # @return [String] attr_reader :parent_span_id # Sampling result of the span. # @return [Boolean, nil] attr_reader :sampled # Starting timestamp of the span. # @return [Float] attr_reader :start_timestamp # Finishing timestamp of the span. # @return [Float] attr_reader :timestamp # Span description # @return [String] attr_reader :description # Span operation # @return [String] attr_reader :op # Span status # @return [String] attr_reader :status # Span tags # @return [Hash] attr_reader :tags # Span data # @return [Hash] attr_reader :data # The SpanRecorder the current span belongs to. # SpanRecorder holds all spans under the same Transaction object (including the Transaction itself). # @return [SpanRecorder] attr_accessor :span_recorder # The Transaction object the Span belongs to. # Every span needs to be attached to a Transaction and their child spans will also inherit the same transaction. # @return [Transaction] attr_accessor :transaction def initialize( description: nil, op: nil, status: nil, trace_id: nil, parent_span_id: nil, sampled: nil, start_timestamp: nil, timestamp: nil ) @trace_id = trace_id || SecureRandom.uuid.delete("-") @span_id = SecureRandom.hex(8) @parent_span_id = parent_span_id @sampled = sampled @start_timestamp = start_timestamp || Sentry.utc_now.to_f @timestamp = timestamp @description = description @op = op @status = status @data = {} @tags = {} end # Finishes the span by adding a timestamp. # @return [self] def finish # already finished return if @timestamp @timestamp = Sentry.utc_now.to_f self end # Generates a trace string that can be used to connect other transactions. # @return [String] def to_sentry_trace sampled_flag = "" sampled_flag = @sampled ? 1 : 0 unless @sampled.nil? "#{@trace_id}-#{@span_id}-#{sampled_flag}" end # @return [Hash] def to_hash { trace_id: @trace_id, span_id: @span_id, parent_span_id: @parent_span_id, start_timestamp: @start_timestamp, timestamp: @timestamp, description: @description, op: @op, status: @status, tags: @tags, data: @data } end # Returns the span's context that can be used to embed in an Event. # @return [Hash] def get_trace_context { trace_id: @trace_id, span_id: @span_id, parent_span_id: @parent_span_id, description: @description, op: @op, status: @status } end # Starts a child span with given attributes. # @param attributes [Hash] the attributes for the child span. def start_child(**attributes) attributes = attributes.dup.merge(trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled) new_span = Span.new(**attributes) new_span.transaction = transaction new_span.span_recorder = span_recorder if span_recorder span_recorder.add(new_span) end new_span end # Starts a child span, yield it to the given block, and then finish the span after the block is executed. # @example # span.with_child_span do |child_span| # # things happen here will be recorded in a child span # end # # @param attributes [Hash] the attributes for the child span. # @param block [Proc] the action to be recorded in the child span. # @yieldparam child_span [Span] def with_child_span(**attributes, &block) child_span = start_child(**attributes) yield(child_span) child_span.finish end def deep_dup dup end # Sets the span's operation. # @param op [String] operation of the span. def set_op(op) @op = op end # Sets the span's description. # @param description [String] description of the span. def set_description(description) @description = description end # Sets the span's status. # @param satus [String] status of the span. def set_status(status) @status = status end # Sets the span's finish timestamp. # @param timestamp [Float] finished time in float format (most precise). def set_timestamp(timestamp) @timestamp = timestamp end # Sets the span's status with given http status code. # @param status_code [String] example: "500". def set_http_status(status_code) status_code = status_code.to_i set_data("status_code", status_code) status = if status_code >= 200 && status_code < 299 "ok" else STATUS_MAP[status_code] end set_status(status) end # Inserts a key-value pair to the span's data payload. # @param key [String, Symbol] # @param value [Object] def set_data(key, value) @data[key] = value end # Sets a tag to the span. # @param key [String, Symbol] # @param value [String] def set_tag(key, value) @tags[key] = value end end end sentry-ruby-core-5.3.0/lib/sentry/linecache.rb0000644000004100000410000000214514232765714021347 0ustar www-datawww-data# frozen_string_literal: true module Sentry # @api private class LineCache def initialize @cache = {} end # Any linecache you provide to Sentry must implement this method. # Returns an Array of Strings representing the lines in the source # file. The number of lines retrieved is (2 * context) + 1, the middle # line should be the line requested by lineno. See specs for more information. def get_file_context(filename, lineno, context) return nil, nil, nil unless valid_path?(filename) lines = Array.new(2 * context + 1) do |i| getline(filename, lineno - context + i) end [lines[0..(context - 1)], lines[context], lines[(context + 1)..-1]] end private def valid_path?(path) lines = getlines(path) !lines.nil? end def getlines(path) @cache[path] ||= begin IO.readlines(path) rescue nil end end def getline(path, n) return nil if n < 1 lines = getlines(path) return nil if lines.nil? lines[n - 1] end end end sentry-ruby-core-5.3.0/lib/sentry/rack.rb0000644000004100000410000000013014232765714020344 0ustar www-datawww-data# frozen_string_literal: true require 'rack' require 'sentry/rack/capture_exceptions' sentry-ruby-core-5.3.0/lib/sentry/redis.rb0000644000004100000410000000416514232765714020546 0ustar www-datawww-data# frozen_string_literal: true module Sentry # @api private class Redis OP_NAME = "db.redis.command" LOGGER_NAME = :redis_logger def initialize(commands, host, port, db) @commands, @host, @port, @db = commands, host, port, db end def instrument return yield unless Sentry.initialized? record_span do yield.tap do record_breadcrumb end end end private attr_reader :commands, :host, :port, :db def record_span return yield unless (transaction = Sentry.get_current_scope.get_transaction) && transaction.sampled sentry_span = transaction.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) yield.tap do sentry_span.set_description(commands_description) sentry_span.set_data(:server, server_description) sentry_span.set_timestamp(Sentry.utc_now.to_f) end end def record_breadcrumb return unless Sentry.configuration.breadcrumbs_logger.include?(LOGGER_NAME) Sentry.add_breadcrumb( Sentry::Breadcrumb.new( level: :info, category: OP_NAME, type: :info, data: { commands: parsed_commands, server: server_description } ) ) end def commands_description parsed_commands.map do |statement| statement.values.join(" ").strip end.join(", ") end def parsed_commands commands.map do |statement| command, key, *arguments = statement { command: command.to_s.upcase, key: key }.tap do |command_set| command_set[:arguments] = arguments.join(" ") if Sentry.configuration.send_default_pii end end end def server_description "#{host}:#{port}/#{db}" end module Client def logging(commands, &block) Sentry::Redis.new(commands, host, port, db).instrument do super end end end end end if defined?(::Redis::Client) Sentry.register_patch do patch = Sentry::Redis::Client Redis::Client.prepend(patch) unless Redis::Client.ancestors.include?(patch) end end sentry-ruby-core-5.3.0/lib/sentry/integrable.rb0000644000004100000410000000124314232765714021546 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Integrable def register_integration(name:, version:) Sentry.register_integration(name, version) @integration_name = name end def integration_name @integration_name end def capture_exception(exception, **options, &block) options[:hint] ||= {} options[:hint][:integration] = integration_name Sentry.capture_exception(exception, **options, &block) end def capture_message(message, **options, &block) options[:hint] ||= {} options[:hint][:integration] = integration_name Sentry.capture_message(message, **options, &block) end end end sentry-ruby-core-5.3.0/lib/sentry/transaction.rb0000644000004100000410000001414114232765714021760 0ustar www-datawww-data# frozen_string_literal: true module Sentry class Transaction < Span SENTRY_TRACE_REGEXP = Regexp.new( "^[ \t]*" + # whitespace "([0-9a-f]{32})?" + # trace_id "-?([0-9a-f]{16})?" + # span_id "-?([01])?" + # sampled "[ \t]*$" # whitespace ) UNLABELD_NAME = "".freeze MESSAGE_PREFIX = "[Tracing]" include LoggingHelper # The name of the transaction. # @return [String] attr_reader :name # The sampling decision of the parent transaction, which will be considered when making the current transaction's sampling decision. # @return [String] attr_reader :parent_sampled # @deprecated Use Sentry.get_current_hub instead. attr_reader :hub # @deprecated Use Sentry.configuration instead. attr_reader :configuration # @deprecated Use Sentry.logger instead. attr_reader :logger def initialize(name: nil, parent_sampled: nil, hub:, **options) super(**options) @name = name @parent_sampled = parent_sampled @transaction = self @hub = hub @configuration = hub.configuration # to be removed @tracing_enabled = hub.configuration.tracing_enabled? @traces_sampler = hub.configuration.traces_sampler @traces_sample_rate = hub.configuration.traces_sample_rate @logger = hub.configuration.logger init_span_recorder end # Initalizes a Transaction instance with a Sentry trace string from another transaction (usually from an external request). # # The original transaction will become the parent of the new Transaction instance. And they will share the same `trace_id`. # # The child transaction will also store the parent's sampling decision in its `parent_sampled` attribute. # @param sentry_trace [String] the trace string from the previous transaction. # @param hub [Hub] the hub that'll be responsible for sending this transaction when it's finished. # @param options [Hash] the options you want to use to initialize a Transaction instance. # @return [Transaction, nil] def self.from_sentry_trace(sentry_trace, hub: Sentry.get_current_hub, **options) return unless hub.configuration.tracing_enabled? return unless sentry_trace match = SENTRY_TRACE_REGEXP.match(sentry_trace) return if match.nil? trace_id, parent_span_id, sampled_flag = match[1..3] parent_sampled = if sampled_flag.nil? nil else sampled_flag != "0" end new(trace_id: trace_id, parent_span_id: parent_span_id, parent_sampled: parent_sampled, hub: hub, **options) end # @return [Hash] def to_hash hash = super hash.merge!(name: @name, sampled: @sampled, parent_sampled: @parent_sampled) hash end # @return [Transaction] def deep_dup copy = super copy.init_span_recorder(@span_recorder.max_length) @span_recorder.spans.each do |span| # span_recorder's first span is the current span, which should not be added to the copy's spans next if span == self copy.span_recorder.add(span.dup) end copy end # Sets initial sampling decision of the transaction. # @param sampling_context [Hash] a context Hash that'll be passed to `traces_sampler` (if provided). # @return [void] def set_initial_sample_decision(sampling_context:) unless @tracing_enabled @sampled = false return end return unless @sampled.nil? sample_rate = if @traces_sampler.is_a?(Proc) @traces_sampler.call(sampling_context) elsif !sampling_context[:parent_sampled].nil? sampling_context[:parent_sampled] else @traces_sample_rate end transaction_description = generate_transaction_description unless [true, false].include?(sample_rate) || (sample_rate.is_a?(Numeric) && sample_rate >= 0.0 && sample_rate <= 1.0) @sampled = false log_warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}") return end if sample_rate == 0.0 || sample_rate == false @sampled = false log_debug("#{MESSAGE_PREFIX} Discarding #{transaction_description} because traces_sampler returned 0 or false") return end if sample_rate == true @sampled = true else @sampled = Random.rand < sample_rate end if @sampled log_debug("#{MESSAGE_PREFIX} Starting #{transaction_description}") else log_debug( "#{MESSAGE_PREFIX} Discarding #{transaction_description} because it's not included in the random sample (sampling rate = #{sample_rate})" ) end end # Finishes the transaction's recording and send it to Sentry. # @param hub [Hub] the hub that'll send this transaction. (Deprecated) # @return [TransactionEvent] def finish(hub: nil) if hub log_warn( <<~MSG Specifying a different hub in `Transaction#finish` will be deprecated in version 5.0. Please use `Hub#start_transaction` with the designated hub. MSG ) end hub ||= @hub super() # Span#finish doesn't take arguments if @name.nil? @name = UNLABELD_NAME end if @sampled event = hub.current_client.event_from_transaction(self) hub.capture_event(event) else hub.current_client.transport.record_lost_event(:sample_rate, 'transaction') end end protected def init_span_recorder(limit = 1000) @span_recorder = SpanRecorder.new(limit) @span_recorder.add(self) end private def generate_transaction_description result = op.nil? ? "" : "<#{@op}> " result += "transaction" result += " <#{@name}>" if @name result end class SpanRecorder attr_reader :max_length, :spans def initialize(max_length) @max_length = max_length @spans = [] end def add(span) if @spans.count < @max_length @spans << span end end end end end sentry-ruby-core-5.3.0/lib/sentry/backtrace.rb0000644000004100000410000000623314232765714021355 0ustar www-datawww-data# frozen_string_literal: true module Sentry # @api private class Backtrace # Handles backtrace parsing line by line class Line RB_EXTENSION = ".rb" # regexp (optional leading X: on windows, or JRuby9000 class-prefix) RUBY_INPUT_FORMAT = / ^ \s* (?: [a-zA-Z]: | uri:classloader: )? ([^:]+ | <.*>): (\d+) (?: :in \s `([^']+)')?$ /x.freeze # org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:170) JAVA_INPUT_FORMAT = /^(.+)\.([^\.]+)\(([^\:]+)\:(\d+)\)$/.freeze # The file portion of the line (such as app/models/user.rb) attr_reader :file # The line number portion of the line attr_reader :number # The method of the line (such as index) attr_reader :method # The module name (JRuby) attr_reader :module_name attr_reader :in_app_pattern # Parses a single line of a given backtrace # @param [String] unparsed_line The raw line from +caller+ or some backtrace # @return [Line] The parsed backtrace line def self.parse(unparsed_line, in_app_pattern) ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT) if ruby_match _, file, number, method = ruby_match.to_a file.sub!(/\.class$/, RB_EXTENSION) module_name = nil else java_match = unparsed_line.match(JAVA_INPUT_FORMAT) _, module_name, method, file, number = java_match.to_a end new(file, number, method, module_name, in_app_pattern) end def initialize(file, number, method, module_name, in_app_pattern) @file = file @module_name = module_name @number = number.to_i @method = method @in_app_pattern = in_app_pattern end def in_app if file =~ in_app_pattern true else false end end # Reconstructs the line in a readable fashion def to_s "#{file}:#{number}:in `#{method}'" end def ==(other) to_s == other.to_s end def inspect "" end end APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test)/.freeze # holder for an Array of Backtrace::Line instances attr_reader :lines def self.parse(backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback) ruby_lines = backtrace.is_a?(Array) ? backtrace : backtrace.split(/\n\s*/) 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}") end lines = ruby_lines.to_a.map do |unparsed_line| Line.parse(unparsed_line, in_app_pattern) end new(lines) end def initialize(lines) @lines = lines end def inspect "" end def to_s content = [] lines.each do |line| content << line end content.join("\n") end def ==(other) if other.respond_to?(:lines) lines == other.lines else false end end end end sentry-ruby-core-5.3.0/lib/sentry/logger.rb0000644000004100000410000000072214232765714020712 0ustar www-datawww-data# frozen_string_literal: true require 'logger' module Sentry class Logger < ::Logger LOG_PREFIX = "** [Sentry] " PROGNAME = "sentry" def initialize(*) super @level = ::Logger::INFO original_formatter = ::Logger::Formatter.new @default_formatter = proc do |severity, datetime, _progname, msg| msg = "#{LOG_PREFIX}#{msg}" original_formatter.call(severity, datetime, PROGNAME, msg) end end end end sentry-ruby-core-5.3.0/lib/sentry/session_flusher.rb0000644000004100000410000000343014232765714022645 0ustar www-datawww-data# frozen_string_literal: true module Sentry class SessionFlusher include LoggingHelper FLUSH_INTERVAL = 60 def initialize(configuration, client) @thread = nil @client = client @pending_aggregates = {} @release = configuration.release @environment = configuration.environment @logger = configuration.logger log_debug("[Sessions] Sessions won't be captured without a valid release") unless @release end def flush return if @pending_aggregates.empty? envelope = pending_envelope Sentry.background_worker.perform do @client.transport.send_envelope(envelope) end @pending_aggregates = {} end def add_session(session) return unless @release ensure_thread return unless Session::AGGREGATE_STATUSES.include?(session.status) @pending_aggregates[session.aggregation_key] ||= init_aggregates(session.aggregation_key) @pending_aggregates[session.aggregation_key][session.status] += 1 end def kill log_debug("Killing session flusher") @thread&.kill end private def init_aggregates(aggregation_key) aggregates = { started: aggregation_key.iso8601 } Session::AGGREGATE_STATUSES.each { |k| aggregates[k] = 0 } aggregates end def pending_envelope envelope = Envelope.new header = { type: 'sessions' } payload = { attrs: attrs, aggregates: @pending_aggregates.values } envelope.add_item(header, payload) envelope end def attrs { release: @release, environment: @environment } end def ensure_thread return if @thread&.alive? @thread = Thread.new do loop do sleep(FLUSH_INTERVAL) flush end end end end end sentry-ruby-core-5.3.0/lib/sentry/hub.rb0000644000004100000410000001204414232765714020211 0ustar www-datawww-data# frozen_string_literal: true require "sentry/scope" require "sentry/client" require "sentry/session" module Sentry class Hub include ArgumentCheckingHelper attr_reader :last_event_id def initialize(client, scope) first_layer = Layer.new(client, scope) @stack = [first_layer] @last_event_id = nil end def new_from_top Hub.new(current_client, current_scope) end def current_client current_layer&.client end def configuration current_client.configuration end def current_scope current_layer&.scope end def clone layer = current_layer if layer scope = layer.scope&.dup Hub.new(layer.client, scope) end end def bind_client(client) layer = current_layer if layer layer.client = client end end def configure_scope(&block) block.call(current_scope) end def with_scope(&block) push_scope yield(current_scope) ensure pop_scope end def push_scope new_scope = if current_scope current_scope.dup else Scope.new end @stack << Layer.new(current_client, new_scope) end def pop_scope @stack.pop end def start_transaction(transaction: nil, custom_sampling_context: {}, **options) return unless configuration.tracing_enabled? transaction ||= Transaction.new(**options.merge(hub: self)) sampling_context = { transaction_context: transaction.to_hash, parent_sampled: transaction.parent_sampled } sampling_context.merge!(custom_sampling_context) transaction.set_initial_sample_decision(sampling_context: sampling_context) transaction end def capture_exception(exception, **options, &block) check_argument_type!(exception, ::Exception) return if Sentry.exception_captured?(exception) return unless current_client options[:hint] ||= {} options[:hint][:exception] = exception event = current_client.event_from_exception(exception, options[:hint]) return unless event current_scope.session&.update_from_exception(event.exception) capture_event(event, **options, &block).tap do # mark the exception as captured so we can use this information to avoid duplicated capturing exception.instance_variable_set(Sentry::CAPTURED_SIGNATURE, true) end end def capture_message(message, **options, &block) check_argument_type!(message, ::String) return unless current_client options[:hint] ||= {} options[:hint][:message] = message backtrace = options.delete(:backtrace) event = current_client.event_from_message(message, options[:hint], backtrace: backtrace) return unless event capture_event(event, **options, &block) end def capture_event(event, **options, &block) check_argument_type!(event, Sentry::Event) return unless current_client hint = options.delete(:hint) || {} scope = current_scope.dup if block block.call(scope) elsif custom_scope = options[:scope] scope.update_from_scope(custom_scope) elsif !options.empty? scope.update_from_options(**options) end event = current_client.capture_event(event, scope, hint) if event && configuration.debug configuration.log_debug(event.to_json_compatible) end @last_event_id = event&.event_id unless event.is_a?(Sentry::TransactionEvent) event end def add_breadcrumb(breadcrumb, hint: {}) return unless configuration.enabled_in_current_env? if before_breadcrumb = current_client.configuration.before_breadcrumb breadcrumb = before_breadcrumb.call(breadcrumb, hint) end return unless breadcrumb current_scope.add_breadcrumb(breadcrumb) end # this doesn't do anything to the already initialized background worker # but it temporarily disables dispatching events to it def with_background_worker_disabled(&block) original_background_worker_threads = configuration.background_worker_threads configuration.background_worker_threads = 0 block.call ensure configuration.background_worker_threads = original_background_worker_threads end def start_session return unless current_scope current_scope.set_session(Session.new) end def end_session return unless current_scope session = current_scope.session current_scope.set_session(nil) return unless session session.close Sentry.session_flusher.add_session(session) end def with_session_tracking(&block) return yield unless configuration.auto_session_tracking start_session yield ensure end_session end private def current_layer @stack.last end class Layer attr_accessor :client attr_reader :scope def initialize(client, scope) @client = client @scope = scope end end end end sentry-ruby-core-5.3.0/lib/sentry/envelope.rb0000644000004100000410000000136614232765714021255 0ustar www-datawww-data# frozen_string_literal: true module Sentry # @api private class Envelope class Item attr_accessor :headers, :payload def initialize(headers, payload) @headers = headers @payload = payload end def type @headers[:type] || 'event' end def to_s <<~ITEM #{JSON.generate(@headers)} #{JSON.generate(@payload)} ITEM end end attr_accessor :headers, :items def initialize(headers = {}) @headers = headers @items = [] end def add_item(headers, payload) @items << Item.new(headers, payload) end def item_types @items.map(&:type) end def event_id @headers[:event_id] end end end sentry-ruby-core-5.3.0/lib/sentry/breadcrumb_buffer.rb0000644000004100000410000000234714232765714023077 0ustar www-datawww-data# frozen_string_literal: true require "sentry/breadcrumb" module Sentry class BreadcrumbBuffer DEFAULT_SIZE = 100 include Enumerable # @return [Array] attr_accessor :buffer # @param size [Integer, nil] If it's not provided, it'll fallback to DEFAULT_SIZE def initialize(size = nil) @buffer = Array.new(size || DEFAULT_SIZE) end # @param crumb [Breadcrumb] # @return [void] def record(crumb) yield(crumb) if block_given? @buffer.slice!(0) @buffer << crumb end # @return [Array] def members @buffer.compact end # Returns the last breadcrumb stored in the buffer. If the buffer it's empty, it returns nil. # @return [Breadcrumb, nil] def peek members.last end # Iterates through all breadcrumbs. # @param block [Proc] # @yieldparam crumb [Breadcrumb] # @return [Array] def each(&block) members.each(&block) end # @return [Boolean] def empty? members.none? end # @return [Hash] def to_hash { values: members.map(&:to_hash) } end # @return [BreadcrumbBuffer] def dup copy = super copy.buffer = buffer.deep_dup copy end end end sentry-ruby-core-5.3.0/lib/sentry/transaction_event.rb0000644000004100000410000000126314232765714023162 0ustar www-datawww-data# frozen_string_literal: true module Sentry # TransactionEvent represents events that carry transaction data (type: "transaction"). class TransactionEvent < Event TYPE = "transaction" # @return [] attr_accessor :spans # @return [Float, nil] attr_reader :start_timestamp # Sets the event's start_timestamp. # @param time [Time, Float] # @return [void] def start_timestamp=(time) @start_timestamp = time.is_a?(Time) ? time.to_f : time end # @return [Hash] def to_hash data = super data[:spans] = @spans.map(&:to_hash) if @spans data[:start_timestamp] = @start_timestamp data end end end sentry-ruby-core-5.3.0/lib/sentry-ruby.rb0000644000004100000410000003164314232765714020420 0ustar www-datawww-data# frozen_string_literal: true require "English" require "forwardable" require "time" require "sentry/version" require "sentry/exceptions" require "sentry/core_ext/object/deep_dup" require "sentry/utils/argument_checking_helper" require "sentry/utils/logging_helper" require "sentry/configuration" require "sentry/logger" require "sentry/event" require "sentry/error_event" require "sentry/transaction_event" require "sentry/span" require "sentry/transaction" require "sentry/hub" require "sentry/background_worker" require "sentry/session_flusher" [ "sentry/rake", "sentry/rack", ].each do |lib| begin require lib rescue LoadError end end module Sentry META = { "name" => "sentry.ruby", "version" => Sentry::VERSION }.freeze CAPTURED_SIGNATURE = :@__sentry_captured LOGGER_PROGNAME = "sentry".freeze SENTRY_TRACE_HEADER_NAME = "sentry-trace".freeze THREAD_LOCAL = :sentry_hub class << self # @!visibility private def exception_locals_tp @exception_locals_tp ||= TracePoint.new(:raise) do |tp| exception = tp.raised_exception # don't collect locals again if the exception is re-raised next if exception.instance_variable_get(:@sentry_locals) next unless tp.binding locals = tp.binding.local_variables.each_with_object({}) do |local, result| result[local] = tp.binding.local_variable_get(local) end exception.instance_variable_set(:@sentry_locals, locals) end end # @!attribute [rw] background_worker # @return [BackgroundWorker] attr_accessor :background_worker # @!attribute [r] session_flusher # @return [SessionFlusher] attr_reader :session_flusher ##### Patch Registration ##### # @!visibility private def register_patch(&block) registered_patches << block end # @!visibility private def apply_patches(config) registered_patches.each do |patch| patch.call(config) end end # @!visibility private def registered_patches @registered_patches ||= [] end ##### Integrations ##### # Returns a hash that contains all the integrations that have been registered to the main SDK. # # @return [Hash{String=>Hash}] def integrations @integrations ||= {} end # Registers the SDK integration with its name and version. # # @param name [String] name of the integration # @param version [String] version of the integration def register_integration(name, version) if initialized? logger.warn(LOGGER_PROGNAME) do <<~MSG Integration '#{name}' is loaded after the SDK is initialized, which can cause unexpected behavior. Please make sure all integrations are loaded before SDK initialization. MSG end end meta = { name: "sentry.ruby.#{name}", version: version }.freeze integrations[name.to_s] = meta end ##### Method Delegation ##### extend Forwardable # @!macro [new] configuration # The Configuration object that's used for configuring the client and its transport. # @return [Configuration] # @!macro [new] send_event # Sends the event to Sentry. # @param event [Event] the event to be sent. # @param hint [Hash] the hint data that'll be passed to `before_send` callback. # @return [Event] # @!method configuration # @!macro configuration def configuration return unless initialized? get_current_client.configuration end # @!method send_event # @!macro send_event def send_event(*args) return unless initialized? get_current_client.send_event(*args) end # @!macro [new] set_extras # Updates the scope's extras attribute by merging with the old value. # @param extras [Hash] # @return [Hash] # @!macro [new] set_user # Sets the scope's user attribute. # @param user [Hash] # @return [Hash] # @!macro [new] set_context # Adds a new key-value pair to current contexts. # @param key [String, Symbol] # @param value [Object] # @return [Hash] # @!macro [new] set_tags # Updates the scope's tags attribute by merging with the old value. # @param tags [Hash] # @return [Hash] # @!method set_tags # @!macro set_tags def set_tags(*args) return unless initialized? get_current_scope.set_tags(*args) end # @!method set_extras # @!macro set_extras def set_extras(*args) return unless initialized? get_current_scope.set_extras(*args) end # @!method set_user # @!macro set_user def set_user(*args) return unless initialized? get_current_scope.set_user(*args) end # @!method set_context # @!macro set_context def set_context(*args) return unless initialized? get_current_scope.set_context(*args) end ##### Main APIs ##### # Initializes the SDK with given configuration. # # @yieldparam config [Configuration] # @return [void] def init(&block) config = Configuration.new yield(config) if block_given? config.detect_release apply_patches(config) client = Client.new(config) scope = Scope.new(max_breadcrumbs: config.max_breadcrumbs) hub = Hub.new(client, scope) Thread.current.thread_variable_set(THREAD_LOCAL, hub) @main_hub = hub @background_worker = Sentry::BackgroundWorker.new(config) @session_flusher = if config.auto_session_tracking Sentry::SessionFlusher.new(config, client) else nil end if config.capture_exception_frame_locals exception_locals_tp.enable end at_exit do @session_flusher&.kill @background_worker.shutdown end end # Returns true if the SDK is initialized. # # @return [Boolean] def initialized? !!get_main_hub end # Returns an uri for security policy reporting that's generated from the given DSN # (To learn more about security policy reporting: https://docs.sentry.io/product/security-policy-reporting/) # # It returns nil if # - The SDK is not initialized yet. # - The DSN is not provided or is invalid. # # @return [String, nil] def csp_report_uri return unless initialized? configuration.csp_report_uri end # Returns the main thread's active hub. # # @return [Hub] def get_main_hub @main_hub end # Takes an instance of Sentry::Breadcrumb and stores it to the current active scope. # # @return [Breadcrumb, nil] def add_breadcrumb(breadcrumb, **options) return unless initialized? get_current_hub.add_breadcrumb(breadcrumb, **options) end # Returns the current active hub. # If the current thread doesn't have an active hub, it will clone the main thread's active hub, # stores it in the current thread, and then returns it. # # @return [Hub] def get_current_hub # we need to assign a hub to the current thread if it doesn't have one yet # # ideally, we should do this proactively whenever a new thread is created # but it's impossible for the SDK to keep track every new thread # so we need to use this rather passive way to make sure the app doesn't crash Thread.current.thread_variable_get(THREAD_LOCAL) || clone_hub_to_current_thread end # Returns the current active client. # @return [Client, nil] def get_current_client return unless initialized? get_current_hub.current_client end # Returns the current active scope. # # @return [Scope, nil] def get_current_scope return unless initialized? get_current_hub.current_scope end # Clones the main thread's active hub and stores it to the current thread. # # @return [void] def clone_hub_to_current_thread Thread.current.thread_variable_set(THREAD_LOCAL, get_main_hub.clone) end # Takes a block and yields the current active scope. # # @example # Sentry.configure_scope do |scope| # scope.set_tags(foo: "bar") # end # # Sentry.capture_message("test message") # this event will have tags { foo: "bar" } # # @yieldparam scope [Scope] # @return [void] def configure_scope(&block) return unless initialized? get_current_hub.configure_scope(&block) end # Takes a block and yields a temporary scope. # The temporary scope will inherit all the attributes from the current active scope and replace it to be the active # scope inside the block. # # @example # Sentry.configure_scope do |scope| # scope.set_tags(foo: "bar") # end # # Sentry.capture_message("test message") # this event will have tags { foo: "bar" } # # Sentry.with_scope do |temp_scope| # temp_scope.set_tags(foo: "baz") # Sentry.capture_message("test message 2") # this event will have tags { foo: "baz" } # end # # Sentry.capture_message("test message 3") # this event will have tags { foo: "bar" } # # @yieldparam scope [Scope] # @return [void] def with_scope(&block) return unless initialized? get_current_hub.with_scope(&block) end # Wrap a given block with session tracking. # Aggregate sessions in minutely buckets will be recorded # around this block and flushed every minute. # # @example # Sentry.with_session_tracking do # a = 1 + 1 # new session recorded with :exited status # end # # Sentry.with_session_tracking do # 1 / 0 # rescue => e # Sentry.capture_exception(e) # new session recorded with :errored status # end # @return [void] def with_session_tracking(&block) return yield unless initialized? get_current_hub.with_session_tracking(&block) end # Takes an exception and reports it to Sentry via the currently active hub. # # @yieldparam scope [Scope] # @return [Event, nil] def capture_exception(exception, **options, &block) return unless initialized? get_current_hub.capture_exception(exception, **options, &block) end # Takes a message string and reports it to Sentry via the currently active hub. # # @yieldparam scope [Scope] # @return [Event, nil] def capture_message(message, **options, &block) return unless initialized? get_current_hub.capture_message(message, **options, &block) end # Takes an instance of Sentry::Event and dispatches it to the currently active hub. # # @return [Event, nil] def capture_event(event) return unless initialized? get_current_hub.capture_event(event) end # Takes or initializes a new Sentry::Transaction and makes a sampling decision for it. # # @return [Transaction, nil] def start_transaction(**options) return unless initialized? get_current_hub.start_transaction(**options) end # Records the block's execution as a child of the current span. # If the current scope doesn't have a span, the block would still be executed but the yield param will be nil. # @param attributes [Hash] attributes for the child span. # @yieldparam child_span [Span, nil] # @return yield result # # @example # Sentry.with_child_span(op: "my operation") do |child_span| # child_span.set_data(operation_data) # child_span.set_description(operation_detail) # # result will be returned # end # def with_child_span(**attributes, &block) current_span = get_current_scope.get_span if current_span result = nil begin current_span.with_child_span(**attributes) do |child_span| get_current_scope.set_span(child_span) result = yield(child_span) end ensure get_current_scope.set_span(current_span) end result else yield(nil) end end # Returns the id of the lastly reported Sentry::Event. # # @return [String, nil] def last_event_id return unless initialized? get_current_hub.last_event_id end # Checks if the exception object has been captured by the SDK. # # @return [Boolean] def exception_captured?(exc) return false unless initialized? !!exc.instance_variable_get(CAPTURED_SIGNATURE) end ##### Helpers ##### # @!visibility private def sys_command(command) result = `#{command} 2>&1` rescue nil return if result.nil? || result.empty? || ($CHILD_STATUS && $CHILD_STATUS.exitstatus != 0) result.strip end # @!visibility private def logger configuration.logger end # @!visibility private def sdk_meta META end # @!visibility private def utc_now Time.now.utc end end end # patches require "sentry/net/http" require "sentry/redis" sentry-ruby-core-5.3.0/.yardopts0000644000004100000410000000007214232765714016660 0ustar www-datawww-data--exclude lib/sentry/utils/ --exclude lib/sentry/core_ext sentry-ruby-core-5.3.0/Gemfile0000644000004100000410000000113014232765714016301 0ustar www-datawww-datasource "https://rubygems.org" git_source(:github) { |name| "https://github.com/#{name}.git" } gem "sentry-ruby-core", path: "./" gem "sentry-ruby", path: "./" gem "rack" unless ENV["WITHOUT_RACK"] == "1" gem "rake", "~> 12.0" gem "rspec", "~> 3.0" gem "rspec-retry" gem "webmock" gem "fakeredis" gem "timecop" gem 'simplecov' gem "simplecov-cobertura", "~> 1.4" gem "rexml" gem "object_tracer" gem "debug", github: "ruby/debug", platform: :ruby if RUBY_VERSION.to_f >= 2.6 gem "pry" gem "benchmark-ips" gem "benchmark_driver" gem "benchmark-ipsa" gem "benchmark-memory" gem "yard", "~> 0.9.27" sentry-ruby-core-5.3.0/LICENSE.txt0000644000004100000410000000206114232765714016635 0ustar www-datawww-dataThe MIT License (MIT) Copyright (c) 2020 Sentry Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sentry-ruby-core-5.3.0/sentry-ruby.gemspec0000644000004100000410000000151414232765714020664 0ustar www-datawww-datarequire_relative "lib/sentry/version" Gem::Specification.new do |spec| spec.name = "sentry-ruby" spec.version = Sentry::VERSION spec.authors = ["Sentry Team"] 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.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" spec.add_dependency "sentry-ruby-core", Sentry::VERSION spec.add_dependency "concurrent-ruby", '~> 1.0', '>= 1.0.2' end