Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 5 additions & 14 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
### Bug Fixes

- Handle empty frames case gracefully with local vars ([#2807](https://github.com/getsentry/sentry-ruby/pull/2807))
- Handle more extra attribute types when using `extra` attributes for structured logging ([#2815](https://github.com/getsentry/sentry-ruby/pull/2815))
```ruby
# This now works too and the nested hash is dumped to JSON string
Sentry.logger.info("Hello World", extra: { today: Date.today, user_id: user.id })
```

## 6.2.0

Expand Down Expand Up @@ -529,7 +534,6 @@
- Use `Concurrent.available_processor_count` instead of `Concurrent.usable_processor_count` ([#2358](https://github.com/getsentry/sentry-ruby/pull/2358))

- Support for tracing Faraday requests ([#2345](https://github.com/getsentry/sentry-ruby/pull/2345))

- Closes [#1795](https://github.com/getsentry/sentry-ruby/issues/1795)
- Please note that the Faraday instrumentation has some limitations in case of async requests: <https://github.com/lostisland/faraday/issues/1381>

Expand All @@ -552,7 +556,6 @@
```

- Transaction data are now included in the context ([#2365](https://github.com/getsentry/sentry-ruby/pull/2365))

- Closes [#2363](https://github.com/getsentry/sentry-ruby/issues/2363)

- Inject Sentry meta tags in the Rails application layout automatically in the generator ([#2369](https://github.com/getsentry/sentry-ruby/pull/2369))
Expand Down Expand Up @@ -666,7 +669,6 @@
- Fix warning about default gems on Ruby 3.3.0 ([#2225](https://github.com/getsentry/sentry-ruby/pull/2225))
- Add `hint:` support to `Sentry::Rails::ErrorSubscriber` [#2235](https://github.com/getsentry/sentry-ruby/pull/2235)
- Add [Metrics](https://docs.sentry.io/product/metrics/) support

- Add main APIs and `Aggregator` thread [#2247](https://github.com/getsentry/sentry-ruby/pull/2247)
- Add `Sentry::Metrics.timing` API for measuring block duration [#2254](https://github.com/getsentry/sentry-ruby/pull/2254)
- Add metric summaries on spans [#2255](https://github.com/getsentry/sentry-ruby/pull/2255)
Expand Down Expand Up @@ -826,7 +828,6 @@
- Improve default slug generation for Crons [#2168](https://github.com/getsentry/sentry-ruby/pull/2168)
- Change release name generator to use full SHA commit hash and align with `sentry-cli` and other Sentry SDKs [#2174](https://github.com/getsentry/sentry-ruby/pull/2174)
- Automatic Crons support for scheduling gems

- Add support for [`sidekiq-cron`](https://github.com/sidekiq-cron/sidekiq-cron) [#2170](https://github.com/getsentry/sentry-ruby/pull/2170)

You can opt in to the `sidekiq-cron` patch and we will automatically monitor check-ins for all jobs listed in your `config/schedule.yml` file.
Expand Down Expand Up @@ -882,7 +883,6 @@
- Adopt Rails 7.1's new BroadcastLogger [#2120](https://github.com/getsentry/sentry-ruby/pull/2120)
- Support sending events after all retries were performed (sentry-resque) [#2087](https://github.com/getsentry/sentry-ruby/pull/2087)
- Add [Cron Monitoring](https://docs.sentry.io/product/crons/) support

- Add `Sentry.capture_check_in` API for Cron Monitoring [#2117](https://github.com/getsentry/sentry-ruby/pull/2117)

You can now track progress of long running scheduled jobs.
Expand Down Expand Up @@ -965,7 +965,6 @@
```

- Tracing without Performance

- Implement `PropagationContext` on `Scope` and add `Sentry.get_trace_propagation_headers` API [#2084](https://github.com/getsentry/sentry-ruby/pull/2084)
- Implement `Sentry.continue_trace` API [#2089](https://github.com/getsentry/sentry-ruby/pull/2089)

Expand Down Expand Up @@ -1052,7 +1051,6 @@
```

Some implementation caveats:

- Profiles are sampled **relative** to traces, so if both rates are 0.5, we will capture 0.25 of all requests.
- Profiles are only captured for code running within a transaction.
- Profiles for multi-threaded servers like `puma` might not capture frames correctly when async I/O is happening. This is a `stackprof` limitation.
Expand Down Expand Up @@ -1137,7 +1135,6 @@
- Expose `end_timestamp` in `Span#finish` and `Transaction#finish` [#1946](https://github.com/getsentry/sentry-ruby/pull/1946)
- Add `Transaction#set_context` api [#1947](https://github.com/getsentry/sentry-ruby/pull/1947)
- Add OpenTelemetry support with new `sentry-opentelemetry` gem

- Add `config.instrumenter` to switch between `:sentry` and `:otel` instrumentation [#1944](https://github.com/getsentry/sentry-ruby/pull/1944)

The new `sentry-opentelemetry` gem adds support to automatically integrate OpenTelemetry performance tracing with Sentry. [Give it a try](https://github.com/getsentry/sentry-ruby/tree/master/sentry-opentelemetry#getting-started) and let us know if you have any feedback or problems with using it.
Expand All @@ -1157,7 +1154,6 @@
```

- Use `Sentry.with_child_span` in redis and net/http instead of `span.start_child` [#1920](https://github.com/getsentry/sentry-ruby/pull/1920)

- This might change the nesting of some spans and make it more accurate
- Followup fix to set the sentry-trace header in the correct place [#1922](https://github.com/getsentry/sentry-ruby/pull/1922)

Expand Down Expand Up @@ -1186,14 +1182,12 @@
### Features

- Support rack 3 [#1884](https://github.com/getsentry/sentry-ruby/pull/1884)

- We no longer need the `HTTP_VERSION` check for ignoring the header

- Add [Dynamic Sampling](https://docs.sentry.io/product/sentry-basics/sampling/) support
The SDK now supports Sentry's Dynamic Sampling product.

Note that this is not supported for users still using the `config.async` option.

- Parse incoming [W3C Baggage Headers](https://www.w3.org/TR/baggage/) and propagate them to continue traces [#1869](https://github.com/getsentry/sentry-ruby/pull/1869)
- in all outgoing requests in our net/http patch
- in Sentry transactions as [Dynamic Sampling Context](https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/)
Expand Down Expand Up @@ -1229,7 +1223,6 @@
- Expose `:values` in `ExceptionInterface`, so that it can be accessed in `before_send` under `event.exception.values` [#1843](https://github.com/getsentry/sentry-ruby/pull/1843)

- Add top level `Sentry.close` API [#1844](https://github.com/getsentry/sentry-ruby/pull/1844)

- Cleans up SDK state and sets it to uninitialized
- No-ops all SDK APIs and also disables the transport layer, so nothing will be sent to Sentry after closing the SDK

Expand Down Expand Up @@ -1391,7 +1384,6 @@ end
- Check envelope size before sending it [#1747](https://github.com/getsentry/sentry-ruby/pull/1747)

The SDK will now check if the envelope's event items are oversized before sending the envelope. It goes like this:

1. If an event is oversized (200kb), the SDK will remove its breadcrumbs (which in our experience is the most common cause).
2. If the event size now falls within the limit, it'll be sent.
3. Otherwise, the event will be thrown away. The SDK will also log a debug message about the event's attributes size (in bytes) breakdown. For example,
Expand Down Expand Up @@ -1710,7 +1702,6 @@ When `config.send_default_pii` is set as `true`, `:http_logger` will include que
### Features

- Support exception frame's local variable capturing

- [#1580](https://github.com/getsentry/sentry-ruby/pull/1580)
- [#1589](https://github.com/getsentry/sentry-ruby/pull/1589)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

expect(log_event[:attributes][:method][:value]).to eq("GET")
expect(log_event[:attributes][:path][:value]).to eq("/world")
expect(log_event[:attributes][:format][:value]).to eq(:html)
expect(log_event[:attributes][:format][:value]).to eq("\"html\"")
expect(log_event[:attributes]["sentry.origin"][:value]).to eq("auto.log.rails.log_subscriber")
end

Expand Down Expand Up @@ -275,8 +275,10 @@
log_event = sentry_logs.find { |log| log[:body] == "HelloController#world" }
expect(log_event).not_to be_nil
expect(log_event[:attributes][:params]).to be_present
expect(log_event[:attributes][:params][:value]).to include("safe_param" => "value")
expect(log_event[:attributes][:params][:value]).to include("password" => "[FILTERED]")

params = JSON.parse(log_event[:attributes][:params][:value])
expect(params).to include("safe_param" => "value")
expect(params).to include("password" => "[FILTERED]")
end

it "filters sensitive parameter names" do
Expand All @@ -295,7 +297,7 @@
log_event = sentry_logs.find { |log| log[:body] == "HelloController#world" }
expect(log_event).not_to be_nil

params = log_event[:attributes][:params][:value]
params = JSON.parse(log_event[:attributes][:params][:value])
expect(params).to include("normal_param" => "value")
expect(params).to include("password" => "[FILTERED]")
expect(params).to include("api_key" => "[FILTERED]")
Expand Down Expand Up @@ -323,7 +325,7 @@
log_event = sentry_logs.find { |log| log[:body] == "HelloController#world" }
expect(log_event).not_to be_nil

params = log_event[:attributes][:params][:value]
params = JSON.parse(log_event[:attributes][:params][:value])
expect(params).to include("normal_param" => "value")
expect(params["user"]).to include("name" => "John")
expect(params["user"]).to include("password" => "[FILTERED]")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
expect(log_event[:attributes][:mailer][:value]).to eq("UserMailer")
expect(log_event[:attributes][:duration_ms][:value]).to be > 0
expect(log_event[:attributes][:perform_deliveries][:value]).to be true
expect(log_event[:attributes][:delivery_method][:value]).to eq(:test)
expect(log_event[:attributes][:delivery_method][:value]).to eq("\"test\"")
expect(log_event[:attributes]["sentry.origin"][:value]).to eq("auto.log.rails.log_subscriber")
expect(log_event[:attributes][:date]).to be_present
end
Expand Down Expand Up @@ -76,7 +76,7 @@

log_event = sentry_logs.find { |log| log[:body] == "Email delivered via NotificationMailer" }
expect(log_event).not_to be_nil
expect(log_event[:attributes][:delivery_method][:value]).to eq(:smtp)
expect(log_event[:attributes][:delivery_method][:value]).to eq("\"smtp\"")
end

it "includes date when available" do
Expand Down Expand Up @@ -179,12 +179,12 @@
expect(log_event).not_to be_nil
expect(log_event[:attributes][:params]).to be_present

params = log_event[:attributes][:params][:value]
params = JSON.parse(log_event[:attributes][:params][:value])

expect(params).to include(user_id: 123, safe_param: "value")
expect(params[:password]).to eq("[FILTERED]")
expect(params[:api_key]).to eq("[FILTERED]")
expect(params).to include(email_address: "[email protected]", subject: "Welcome!")
expect(params).to include("user_id" => 123, "safe_param" => "value")
expect(params["password"]).to eq("[FILTERED]")
expect(params["api_key"]).to eq("[FILTERED]")
expect(params).to include("email_address" => "[email protected]", "subject" => "Welcome!")
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
expect(attributes[:job_id][:value]).to be_a(String)
expect(attributes[:queue_name][:value]).to eq("default")
expect(attributes[:executions][:value]).to eq(1)
expect(attributes[:priority][:value]).to be_a(Integer).or be_nil
expect(attributes[:priority][:value]).to be_a(Integer).or(eq("null")).or be_nil
end

it "includes adapter information when available" do
Expand Down Expand Up @@ -122,8 +122,9 @@ def perform(*args, **kwargs)
expect(log_event).not_to be_nil

attributes = log_event[:attributes]
expect(attributes[:arguments][:value]).to be_a(Array)
expect(attributes[:arguments][:value]).to include("safe_arg")
arguments = JSON.parse(attributes[:arguments][:value])
expect(arguments).to be_a(Array)
expect(arguments).to include("safe_arg")
end

it "filters sensitive arguments" do
Expand All @@ -147,11 +148,11 @@ def perform(password:, token:, safe_data:)
expect(log_event).not_to be_nil

attributes = log_event[:attributes]
arguments = attributes[:arguments][:value]
arguments = JSON.parse(attributes[:arguments][:value])

expect(arguments.first).to include(safe_data: "public")
expect(arguments.first).to include(password: "[FILTERED]")
expect(arguments.first).to include(token: "[FILTERED]")
expect(arguments.first).to include("safe_data" => "public")
expect(arguments.first).to include("password" => "[FILTERED]")
expect(arguments.first).to include("token" => "[FILTERED]")

Rails.application.config.filter_parameters = original_filter_params
end
Expand All @@ -175,7 +176,7 @@ def perform(short_string, long_string)
expect(log_event).not_to be_nil

attributes = log_event[:attributes]
arguments = attributes[:arguments][:value]
arguments = JSON.parse(attributes[:arguments][:value])

expect(arguments).to include("short")
expect(arguments).to include("[FILTERED: 150 chars]")
Expand Down Expand Up @@ -204,11 +205,11 @@ def perform(string_arg, hash_arg, number_arg, array_arg)
expect(log_event).not_to be_nil

attributes = log_event[:attributes]
arguments = attributes[:arguments][:value]
arguments = JSON.parse(attributes[:arguments][:value])

expect(arguments[0]).to eq("string_value")
expect(arguments[1]).to include(safe_key: "value")
expect(arguments[1]).to include(password: "[FILTERED]")
expect(arguments[1]).to include("safe_key" => "value")
expect(arguments[1]).to include("password" => "[FILTERED]")
expect(arguments[2]).to eq(42)
expect(arguments[3]).to eq([1, 2, 3])
end
Expand Down
30 changes: 18 additions & 12 deletions sentry-ruby/lib/sentry/log_event.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "json"

module Sentry
# Event type that represents a log entry with its attributes
#
Expand Down Expand Up @@ -62,13 +64,6 @@ class LogEvent
user_email
].map { |name| [name, :"serialize_#{name}"] }.to_h

VALUE_TYPES = Hash.new("string").merge!({
TrueClass => "boolean",
FalseClass => "boolean",
Integer => "integer",
Float => "double"
}).freeze

TOKEN_REGEXP = /%\{(\w+)\}/

def initialize(configuration: Sentry.configuration, **options)
Expand Down Expand Up @@ -178,11 +173,22 @@ def serialize_attributes
end

def attribute_hash(value)
{ value: value, type: value_type(value) }
end

def value_type(value)
VALUE_TYPES[value.class]
case value
when String
{ value: value, type: "string" }
when TrueClass, FalseClass
{ value: value, type: "boolean" }
when Integer
{ value: value, type: "integer" }
when Float
{ value: value, type: "double" }
else
begin
{ value: JSON.generate(value), type: "string" }
rescue
{ value: value, type: "string" }
end
Comment on lines +187 to +190
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The rescue block in attribute_hash incorrectly returns the original non-serializable object, causing a crash later during final payload serialization.
Severity: CRITICAL | Confidence: High

🔍 Detailed Analysis

When a non-serializable object is passed as an attribute to a LogEvent, the attribute_hash method attempts to serialize it with JSON.generate. This fails and is caught by a rescue block. However, the rescue block then returns the original, non-serializable object within the attribute hash. This hash is embedded in the event payload. When the transport layer later attempts to serialize the entire event envelope, JSON.generate is called again on the full payload, which now contains the non-serializable object. This second serialization attempt fails with an unhandled exception, preventing the event from being sent.

💡 Suggested Fix

In the rescue block of the attribute_hash method, instead of returning the raw non-serializable value, convert it to a string representation using a method like value.to_s or value.inspect. This ensures the value is always serializable before being added to the event payload.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: sentry-ruby/lib/sentry/log_event.rb#L187-L190

Potential issue: When a non-serializable object is passed as an attribute to a
`LogEvent`, the `attribute_hash` method attempts to serialize it with `JSON.generate`.
This fails and is caught by a `rescue` block. However, the `rescue` block then returns
the original, non-serializable object within the attribute hash. This hash is embedded
in the event payload. When the transport layer later attempts to serialize the entire
event envelope, `JSON.generate` is called again on the full payload, which now contains
the non-serializable object. This second serialization attempt fails with an unhandled
exception, preventing the event from being sent.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 8030622

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine, previously it would crash too but at the transport level which now works when we can convert to JSON.

end
end

def parameters
Expand Down
39 changes: 39 additions & 0 deletions sentry-ruby/spec/sentry/structured_logger_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,45 @@
expect(log_event[:attributes]["sentry.message.parameter.1"]).to eql({ value: "Monday", type: "string" })
end

it "logs with nested hash attributes" do
attributes = { number: 312, string: "hello" }

Sentry.logger.public_send(level, "Hello world", extra: attributes)

expect(sentry_logs).to_not be_empty

log_event = sentry_logs.last

expect(log_event[:level]).to eql(level)
expect(log_event[:attributes][:extra]).to eql({ type: "string", value: attributes.to_json })
end

it "logs with array attributes" do
attributes = [1, 2, 3, "hello"]

Sentry.logger.public_send(level, "Hello world", extra: attributes)

expect(sentry_logs).to_not be_empty

log_event = sentry_logs.last

expect(log_event[:level]).to eql(level)
expect(log_event[:attributes][:extra]).to eql({ type: "string", value: attributes.to_json })
end

it "logs with date in attributes" do
attributes = Date.today

Sentry.logger.public_send(level, "Hello world", extra: attributes)

expect(sentry_logs).to_not be_empty

log_event = sentry_logs.last

expect(log_event[:level]).to eql(level)
expect(log_event[:attributes][:extra]).to eql({ type: "string", value: attributes.to_json })
end

it "logs with hash-based template parameters" do
Sentry.logger.public_send(level, "Hello %{name}, it is %{day}", name: "Jane", day: "Monday")

Expand Down
Loading