Skip to content

Commit 606e6e2

Browse files
committed
WIP - add ActionMailerSubscriber
1 parent d7e83cf commit 606e6e2

File tree

2 files changed

+343
-0
lines changed

2 files changed

+343
-0
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# frozen_string_literal: true
2+
3+
require "sentry/rails/log_subscriber"
4+
5+
module Sentry
6+
module Rails
7+
module LogSubscribers
8+
# LogSubscriber for ActionMailer events that captures email delivery
9+
# and processing events using Sentry's structured logging system.
10+
#
11+
# This subscriber captures deliver.action_mailer and process.action_mailer events
12+
# and formats them with relevant email information while respecting PII settings.
13+
#
14+
# @example Usage
15+
# # Enable structured logging for ActionMailer
16+
# Sentry.init do |config|
17+
# config.enable_logs = true
18+
# config.rails.structured_logging = true
19+
# config.rails.structured_logging.attach_to = [:action_mailer]
20+
# end
21+
class ActionMailerSubscriber < Sentry::Rails::LogSubscriber
22+
# Handle deliver.action_mailer events
23+
#
24+
# @param event [ActiveSupport::Notifications::Event] The email delivery event
25+
def deliver(event)
26+
return if excluded_event?(event)
27+
28+
payload = event.payload
29+
mailer = payload[:mailer]
30+
duration = duration_ms(event)
31+
32+
# Prepare structured attributes
33+
attributes = {
34+
mailer: mailer,
35+
duration_ms: duration,
36+
perform_deliveries: payload[:perform_deliveries]
37+
}
38+
39+
# Add delivery method if available
40+
attributes[:delivery_method] = payload[:delivery_method] if payload[:delivery_method]
41+
42+
# Add date if available
43+
attributes[:date] = payload[:date].to_s if payload[:date]
44+
45+
# Only include email details if PII is allowed
46+
if Sentry.configuration.send_default_pii
47+
# Note: We're being very conservative here and not including
48+
# to, from, subject, or body to avoid PII leakage
49+
# Users can customize this behavior by extending the subscriber
50+
attributes[:message_id] = payload[:message_id] if payload[:message_id]
51+
end
52+
53+
message = "Email delivered via #{mailer}"
54+
55+
# Log the structured event
56+
log_structured_event(
57+
message: message,
58+
level: :info,
59+
attributes: attributes
60+
)
61+
end
62+
63+
# Handle process.action_mailer events
64+
#
65+
# @param event [ActiveSupport::Notifications::Event] The email processing event
66+
def process(event)
67+
return if excluded_event?(event)
68+
69+
payload = event.payload
70+
mailer = payload[:mailer]
71+
action = payload[:action]
72+
duration = duration_ms(event)
73+
74+
# Prepare structured attributes
75+
attributes = {
76+
mailer: mailer,
77+
action: action,
78+
duration_ms: duration
79+
}
80+
81+
# Add parameters if PII is allowed and they exist
82+
if Sentry.configuration.send_default_pii && payload[:params]
83+
# Filter sensitive parameters
84+
filtered_params = filter_sensitive_params(payload[:params])
85+
attributes[:params] = filtered_params unless filtered_params.empty?
86+
end
87+
88+
message = "#{mailer}##{action}"
89+
90+
# Log the structured event
91+
log_structured_event(
92+
message: message,
93+
level: :info,
94+
attributes: attributes
95+
)
96+
end
97+
98+
private
99+
100+
# Filter sensitive parameters from mailer params
101+
#
102+
# @param params [Hash] Mailer parameters
103+
# @return [Hash] Filtered parameters
104+
def filter_sensitive_params(params)
105+
return {} unless params.is_a?(Hash)
106+
107+
# Email-specific sensitive parameter names to exclude
108+
sensitive_keys = %w[
109+
password token secret api_key
110+
email_address to from cc bcc
111+
subject body content message
112+
personal_data user_data
113+
]
114+
115+
params.reject do |key, _value|
116+
key_str = key.to_s.downcase
117+
sensitive_keys.any? { |sensitive| key_str.include?(sensitive) }
118+
end
119+
end
120+
end
121+
end
122+
end
123+
end
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
require "sentry/rails/log_subscribers/action_mailer_subscriber"
5+
6+
RSpec.describe Sentry::Rails::LogSubscribers::ActionMailerSubscriber do
7+
before do
8+
make_basic_app do |config|
9+
config.enable_logs = true
10+
config.rails.structured_logging.enabled = true
11+
config.rails.structured_logging.attach_to = [:action_mailer]
12+
end
13+
end
14+
15+
describe "integration with ActiveSupport::Notifications" do
16+
it "logs deliver events when emails are sent" do
17+
sentry_transport.events.clear
18+
sentry_transport.envelopes.clear
19+
20+
ActiveSupport::Notifications.instrument("deliver.action_mailer",
21+
mailer: "UserMailer",
22+
perform_deliveries: true,
23+
delivery_method: :test,
24+
date: Time.current,
25+
message_id: "[email protected]"
26+
) do
27+
sleep(0.01)
28+
end
29+
30+
Sentry.get_current_client.log_event_buffer.flush
31+
32+
expect(sentry_logs).not_to be_empty
33+
34+
log_event = sentry_logs.find { |log| log[:body] == "Email delivered via UserMailer" }
35+
expect(log_event).not_to be_nil
36+
expect(log_event[:level]).to eq("info")
37+
expect(log_event[:attributes][:mailer][:value]).to eq("UserMailer")
38+
expect(log_event[:attributes][:duration_ms][:value]).to be > 0
39+
expect(log_event[:attributes][:perform_deliveries][:value]).to be true
40+
expect(log_event[:attributes][:delivery_method][:value]).to eq(:test)
41+
expect(log_event[:attributes][:date]).to be_present
42+
end
43+
44+
it "logs process events when mailer actions are processed" do
45+
sentry_transport.events.clear
46+
sentry_transport.envelopes.clear
47+
48+
ActiveSupport::Notifications.instrument("process.action_mailer",
49+
mailer: "UserMailer",
50+
action: "welcome_email",
51+
params: { user_id: 123, name: "John Doe" }
52+
) do
53+
sleep(0.01)
54+
end
55+
56+
Sentry.get_current_client.log_event_buffer.flush
57+
58+
expect(sentry_logs).not_to be_empty
59+
60+
log_event = sentry_logs.find { |log| log[:body] == "UserMailer#welcome_email" }
61+
expect(log_event).not_to be_nil
62+
expect(log_event[:level]).to eq("info")
63+
expect(log_event[:attributes][:mailer][:value]).to eq("UserMailer")
64+
expect(log_event[:attributes][:action][:value]).to eq("welcome_email")
65+
expect(log_event[:attributes][:duration_ms][:value]).to be > 0
66+
end
67+
68+
it "includes delivery method when available" do
69+
sentry_transport.events.clear
70+
sentry_transport.envelopes.clear
71+
72+
ActiveSupport::Notifications.instrument("deliver.action_mailer",
73+
mailer: "NotificationMailer",
74+
perform_deliveries: true,
75+
delivery_method: :smtp
76+
)
77+
78+
Sentry.get_current_client.log_event_buffer.flush
79+
80+
expect(sentry_logs).not_to be_empty
81+
82+
log_event = sentry_logs.find { |log| log[:body] == "Email delivered via NotificationMailer" }
83+
expect(log_event).not_to be_nil
84+
expect(log_event[:attributes][:delivery_method][:value]).to eq(:smtp)
85+
end
86+
87+
context "when send_default_pii is enabled" do
88+
before do
89+
Sentry.configuration.send_default_pii = true
90+
end
91+
92+
after do
93+
Sentry.configuration.send_default_pii = false
94+
end
95+
96+
it "includes message_id for deliver events" do
97+
sentry_transport.events.clear
98+
sentry_transport.envelopes.clear
99+
100+
ActiveSupport::Notifications.instrument("deliver.action_mailer",
101+
mailer: "UserMailer",
102+
perform_deliveries: true,
103+
message_id: "[email protected]"
104+
)
105+
106+
Sentry.get_current_client.log_event_buffer.flush
107+
108+
expect(sentry_logs).not_to be_empty
109+
110+
log_event = sentry_logs.find { |log| log[:body] == "Email delivered via UserMailer" }
111+
expect(log_event).not_to be_nil
112+
expect(log_event[:attributes][:message_id][:value]).to eq("[email protected]")
113+
end
114+
115+
it "includes filtered parameters for process events" do
116+
sentry_transport.events.clear
117+
sentry_transport.envelopes.clear
118+
119+
ActiveSupport::Notifications.instrument("process.action_mailer",
120+
mailer: "UserMailer",
121+
action: "welcome_email",
122+
params: {
123+
user_id: 123,
124+
safe_param: "value",
125+
password: "secret",
126+
email_address: "[email protected]",
127+
subject: "Welcome!",
128+
api_key: "secret-key"
129+
}
130+
)
131+
132+
Sentry.get_current_client.log_event_buffer.flush
133+
134+
expect(sentry_logs).not_to be_empty
135+
136+
log_event = sentry_logs.find { |log| log[:body] == "UserMailer#welcome_email" }
137+
expect(log_event).not_to be_nil
138+
expect(log_event[:attributes][:params]).to be_present
139+
140+
params = log_event[:attributes][:params][:value]
141+
expect(params).to include(user_id: 123, safe_param: "value")
142+
expect(params).not_to have_key(:password)
143+
expect(params).not_to have_key(:email_address)
144+
expect(params).not_to have_key(:subject)
145+
expect(params).not_to have_key(:api_key)
146+
end
147+
end
148+
149+
context "when send_default_pii is disabled" do
150+
it "does not include message_id for deliver events" do
151+
sentry_transport.events.clear
152+
sentry_transport.envelopes.clear
153+
154+
ActiveSupport::Notifications.instrument("deliver.action_mailer",
155+
mailer: "UserMailer",
156+
perform_deliveries: true,
157+
message_id: "[email protected]"
158+
)
159+
160+
Sentry.get_current_client.log_event_buffer.flush
161+
162+
expect(sentry_logs).not_to be_empty
163+
164+
log_event = sentry_logs.find { |log| log[:body] == "Email delivered via UserMailer" }
165+
expect(log_event).not_to be_nil
166+
expect(log_event[:attributes]).not_to have_key(:message_id)
167+
end
168+
169+
it "does not include parameters for process events" do
170+
sentry_transport.events.clear
171+
sentry_transport.envelopes.clear
172+
173+
ActiveSupport::Notifications.instrument("process.action_mailer",
174+
mailer: "UserMailer",
175+
action: "welcome_email",
176+
params: { user_id: 123, name: "John Doe" }
177+
)
178+
179+
Sentry.get_current_client.log_event_buffer.flush
180+
181+
expect(sentry_logs).not_to be_empty
182+
183+
log_event = sentry_logs.find { |log| log[:body] == "UserMailer#welcome_email" }
184+
expect(log_event).not_to be_nil
185+
expect(log_event[:attributes]).not_to have_key(:params)
186+
end
187+
end
188+
189+
it "excludes events starting with !" do
190+
subscriber = described_class.new
191+
event = double("event", name: "!connection.action_mailer", payload: {})
192+
expect(subscriber.send(:excluded_event?, event)).to be true
193+
end
194+
end
195+
196+
describe "when logging is disabled" do
197+
before do
198+
make_basic_app do |config|
199+
config.enable_logs = false
200+
config.rails.structured_logging.enabled = true
201+
config.rails.structured_logging.attach_to = [:action_mailer]
202+
end
203+
end
204+
205+
it "does not log events when logging is disabled" do
206+
initial_log_count = sentry_logs.count
207+
208+
ActiveSupport::Notifications.instrument("deliver.action_mailer",
209+
mailer: "UserMailer",
210+
perform_deliveries: true
211+
)
212+
213+
if Sentry.get_current_client&.log_event_buffer
214+
Sentry.get_current_client.log_event_buffer.flush
215+
end
216+
217+
expect(sentry_logs.count).to eq(initial_log_count)
218+
end
219+
end
220+
end

0 commit comments

Comments
 (0)