Skip to content
Open
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
4 changes: 3 additions & 1 deletion app/controllers/rails_observatory/mailers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ def recent
MailDelivery.ensure_index
@page = [params[:page].to_i, 1].max
@per_page = PER_PAGE
query = MailDelivery.all
@direction = params[:direction] || "outbound"

query = MailDelivery.where(direction: @direction)
@total_count = query.count
@total_pages = (@total_count.to_f / @per_page).ceil
@total_pages = 1 if @total_pages < 1
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<turbo-frame id="recent-deliveries">
<turbo-frame id="recent-<%= @direction %>-deliveries">
<%= render 'query_table',
attributes: { id: 'recent-mail-deliveries' },
attributes: { id: "recent-mail-deliveries-#{@direction}" },
events: @deliveries,
fields: [:time, :mailer, :to, :subject],
fields: [:time, :mailer, :from, :to, :subject],
formatters: {
time: ->(time, delivery) {
link_to Time.at(time).strftime('%H:%M:%S'),
preview_mail_path(delivery.message_id),
data: { turbo_frame: '_top' }
}
},
pagination: { page: @page, total_pages: @total_pages, per_page: @per_page }
pagination: { page: @page, total_pages: @total_pages, per_page: @per_page, params: { direction: @direction } }
%>
</turbo-frame>
13 changes: 11 additions & 2 deletions app/views/rails_observatory/mailers/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@
<dl>
<dt>Emails Sent</dt>
<dd><%= metric_value(name: 'mailer.delivery_count_sum') || 0 %></dd>
<dt>Emails Received</dt>
<dd><%= metric_value(name: 'mailer.inbound_count_sum') || 0 %></dd>
</dl>
</div>

<section class="recent-requests">
<header>
<h2>Recent Deliveries</h2>
<h2>Outbound Deliveries</h2>
</header>
<turbo-frame id="recent-deliveries" src="<%= recent_mailers_path %>" loading="lazy"></turbo-frame>
<turbo-frame id="recent-outbound-deliveries" src="<%= recent_mailers_path(direction: 'outbound') %>" loading="lazy"></turbo-frame>
</section>

<section class="recent-requests">
<header>
<h2>Inbound Deliveries</h2>
</header>
<turbo-frame id="recent-inbound-deliveries" src="<%= recent_mailers_path(direction: 'inbound') %>" loading="lazy"></turbo-frame>
</section>
</main>
34 changes: 34 additions & 0 deletions lib/rails_observatory/action_mailbox_subscriber.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module RailsObservatory
class ActionMailboxSubscriber < ActiveSupport::Subscriber
attach_to :action_mailbox

def process(event)
inbound_email = event.payload[:inbound_email]
mail = inbound_email.mail

# Extract mailbox name from the inbound_email
mailer = inbound_email.class.name

MailDelivery.new(
mail: mail.to_s,
mailer: mailer,
to: format_addresses(mail.to),
from: format_addresses(mail.from),
subject: mail.subject,
message_id: mail.message_id,
time: Time.now.to_f,
duration: event.duration,
direction: "inbound"
).save

RedisTimeSeries.record_occurrence("mailer.inbound_count", labels: {mailer: mailer})
end

private

def format_addresses(addresses)
return nil if addresses.nil?
Array(addresses).join(", ")
end
end
end
2 changes: 1 addition & 1 deletion lib/rails_observatory/action_mailer_subscriber.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class ActionMailerSubscriber < ActiveSupport::Subscriber

def deliver(event)
event.payload => {mail:, mailer:, to:, from:, subject:, message_id:}
MailDelivery.new(mail:, mailer:, to:, from:, subject:, message_id:, time: Time.now.to_f, duration: event.duration).save
MailDelivery.new(mail:, mailer:, to:, from:, subject:, message_id:, time: Time.now.to_f, duration: event.duration, direction: "outbound").save

RedisTimeSeries.record_occurrence("mailer.delivery_count", labels: {mailer:})
end
Expand Down
1 change: 1 addition & 0 deletions lib/rails_observatory/models/mail_delivery.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class MailDelivery < RedisModel
attribute :to, :string
attribute :from, :string
attribute :subject, :string
attribute :direction, :string # 'outbound' or 'inbound'
attribute :mail, compressed: true, indexed: false

alias_attribute :id, :message_id
Expand Down
39 changes: 30 additions & 9 deletions test/controllers/rails_observatory/mailers_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,58 @@ def teardown
assert_response :success
end

test "index renders turbo frame for recent deliveries" do
test "index renders turbo frames for outbound and inbound deliveries" do
get mailers_path
assert_response :success

# Verify the turbo-frame element is present with lazy loading
assert_select "turbo-frame#recent-deliveries[src='#{recent_mailers_path}'][loading='lazy']"
# Verify the outbound turbo-frame element is present with lazy loading
assert_select "turbo-frame#recent-outbound-deliveries[src='#{recent_mailers_path(direction: 'outbound')}'][loading='lazy']"

# Verify the inbound turbo-frame element is present with lazy loading
assert_select "turbo-frame#recent-inbound-deliveries[src='#{recent_mailers_path(direction: 'inbound')}'][loading='lazy']"
end

test "recent action renders deliveries" do
get recent_mailers_path
test "recent action renders outbound deliveries" do
get recent_mailers_path(direction: "outbound")
assert_response :success

# Verify the turbo-frame wrapper is present
assert_select "turbo-frame#recent-deliveries"
assert_select "turbo-frame#recent-outbound-deliveries"
end

test "index renders summary panel" do
test "recent action renders inbound deliveries" do
get recent_mailers_path(direction: "inbound")
assert_response :success

# Verify the turbo-frame wrapper is present
assert_select "turbo-frame#recent-inbound-deliveries"
end

test "index renders summary panel with sent and received counts" do
get mailers_path
assert_response :success

assert_select "div.summary-panel" do
assert_select "dt", text: "Emails Sent"
assert_select "dt", text: "Emails Received"
end
end

test "index renders outbound deliveries section" do
get mailers_path
assert_response :success

assert_select "section.recent-requests" do
assert_select "h2", text: "Outbound Deliveries"
end
end

test "index renders recent deliveries section" do
test "index renders inbound deliveries section" do
get mailers_path
assert_response :success

assert_select "section.recent-requests" do
assert_select "h2", text: "Recent Deliveries"
assert_select "h2", text: "Inbound Deliveries"
end
end
end
Expand Down
6 changes: 6 additions & 0 deletions test/dummy/app/mailboxes/application_mailbox.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

class ApplicationMailbox < ActionMailbox::Base
# Route all inbound emails to CatchAll mailbox
routing :all => :catch_all
end
9 changes: 9 additions & 0 deletions test/dummy/app/mailboxes/catch_all_mailbox.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

# CatchAllMailbox processes all inbound emails
# The actual tracking is done by RailsObservatory::ActionMailboxSubscriber
class CatchAllMailbox < ApplicationMailbox
def process
# Just process the email - the subscriber will capture it
end
end
3 changes: 3 additions & 0 deletions test/dummy/config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,7 @@

# Rails observatory configuration
config.rails_observatory.redis = {host: "localhost", port: 6379, db: 0, middlewares: [], pool_size: ENV["RAILS_MAX_THREADS"] || 5}

# ActionMailbox configuration
config.action_mailbox.ingress = :relay
end
3 changes: 3 additions & 0 deletions test/dummy/config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,7 @@
# config.action_view.annotate_rendered_view_with_filenames = true

config.rails_observatory.redis = {host: "localhost", port: 6399, db: 0, middlewares: [], pool_size: 1}

# ActionMailbox configuration
config.action_mailbox.ingress = :test
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

class CreateActionMailboxTables < ActiveRecord::Migration[8.1]
def change
create_table :action_mailbox_inbound_emails do |t|
t.integer :status, default: 0, null: false
t.string :message_id, null: false
t.string :message_checksum, null: false

t.timestamps

t.index [:message_id, :message_checksum], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true
end

create_active_storage_tables if !ActiveRecord::Base.connection.table_exists?(:active_storage_blobs)
end

private

def create_active_storage_tables
create_table :active_storage_blobs do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum

if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end

t.index [:key], unique: true
end

create_table :active_storage_attachments do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false
t.references :blob, null: false

if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end

t.index [:record_type, :record_id, :name, :blob_id], name: :index_active_storage_attachments_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end

create_table :active_storage_variant_records do |t|
t.belongs_to :blob, null: false, index: false
t.string :variation_digest, null: false

t.index [:blob_id, :variation_digest], name: :index_active_storage_variant_records_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
end
32 changes: 32 additions & 0 deletions test/dummy/db/seeds.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

puts "Seeding test data..."

# Generate some outbound emails
puts "Generating outbound emails..."
5.times do |i|
NewUserMailer.welcome_email("user#{i}@example.com").deliver_now
sleep 0.1 # Small delay to ensure distinct timestamps
end

# Generate some inbound emails
puts "Generating inbound emails..."
5.times do |i|
inbound_email = ActionMailbox::InboundEmail.create_and_extract_message_id!(<<~EMAIL)
From: sender#{i}@example.com
To: support@example.com
Subject: Test Inbound Email #{i + 1}
Date: #{Time.now.rfc2822}
Message-ID: <inbound-#{i}-#{SecureRandom.uuid}@example.com>

This is a test inbound email message number #{i + 1}.

It can contain multiple lines of text.
EMAIL

# Process the inbound email
inbound_email.deliver
sleep 0.1 # Small delay to ensure distinct timestamps
end

puts "Done! Created 5 outbound and 5 inbound emails."
49 changes: 40 additions & 9 deletions test/dummy/lib/tasks/observatory.rake
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,23 @@ namespace :observatory do
{type: :retry, desc: "RetryJob"}
]

# Mailer scenarios
mailers = [
# Outbound mailer scenarios
outbound_mailers = [
{action: :welcome, args: -> { {user_email: "user#{rand(1000)}@example.com"} }, desc: "welcome"},
{action: :notification, args: -> { {user_email: "user#{rand(1000)}@example.com", message: "Notification #{rand(1000)}"} }, desc: "notification"},
{action: :newsletter, args: -> { {recipients: ["list#{rand(100)}@example.com"]} }, desc: "newsletter"}
]

# Inbound email scenarios
inbound_email_subjects = [
"Customer Support Request",
"Product Inquiry",
"Feedback on Recent Order",
"Question About Pricing",
"Bug Report",
"Feature Request"
]

# Helper to make HTTP requests
make_request = lambda do |method, path, params = {}|
uri = URI("#{base_url}#{path}")
Expand Down Expand Up @@ -83,26 +93,47 @@ namespace :observatory do
while running
count += 1

# Pick event type: 60% request, 30% job, 10% mailer
# Pick event type: 50% request, 30% job, 10% outbound mail, 10% inbound mail
roll = rand(100)
if roll < 60
if roll < 50
# Request
req = requests.sample
print "[#{count}] REQUEST #{req[:desc]}..."
response = make_request.call(req[:method], req[:path], req[:params] || {})
puts response ? " #{response.code}" : " (unreachable)"
elsif roll < 90
elsif roll < 80
# Job
job = jobs.sample
print "[#{count}] JOB #{job[:desc]}..."
run_job.call(job[:type])
puts " done"
else
# Mailer
mailer = mailers.sample
print "[#{count}] MAIL #{mailer[:desc]}..."
elsif roll < 90
# Outbound Mailer
mailer = outbound_mailers.sample
print "[#{count}] MAIL OUTBOUND #{mailer[:desc]}..."
ScenariosMailer.send(mailer[:action], **mailer[:args].call).deliver_now
puts " done"
else
# Inbound Email
subject = inbound_email_subjects.sample
from = "customer#{rand(1000)}@example.com"
print "[#{count}] MAIL INBOUND #{subject}..."
inbound_email = ActionMailbox::InboundEmail.create_and_extract_message_id!(<<~EMAIL)
From: #{from}
To: support@example.com
Subject: #{subject}
Date: #{Time.now.rfc2822}
Message-ID: <auto-#{SecureRandom.uuid}@example.com>

This is an automated test inbound email.

#{["Please help with this issue.", "I have a question about your service.", "Great product!", "Looking forward to hearing from you."].sample}

Thanks,
Customer
EMAIL
inbound_email.deliver
puts " delivered"
end

sleep(rand(0.1..0.5)) if running
Expand Down
Loading