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
5 changes: 5 additions & 0 deletions lib/honeybadger/config/defaults.rb
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,11 @@ class Boolean; end
default: false,
type: Boolean
},
"active_agent.insights.enabled": {
description: "Enable automatic data collection for Active Agent.",
default: true,
type: Boolean
},
"active_job.attempt_threshold": {
description: "The number of attempts before notifications will be sent.",
default: 0,
Expand Down
59 changes: 34 additions & 25 deletions lib/honeybadger/notification_subscriber.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,37 @@ def finish(name, id, payload)
}.merge(format_payload(payload).compact)

record(name, payload)
record_metrics(name, payload)
end

def record(name, payload)
if Honeybadger.config.load_plugin_insights?(:rails, feature: :active_support_events)
Honeybadger.event(name, payload)
end
Honeybadger.event(name, payload)
end

if Honeybadger.config.load_plugin_insights?(:rails, feature: :metrics)
metric_source "rails"
record_metrics(name, payload)
end
def record_metrics(name, payload)
# noop
end

def process?(name, payload)
true
end

def format_payload(payload)
payload
end
end

class RailsSubscriber < NotificationSubscriber
def record(name, payload)
return unless Honeybadger.config.load_plugin_insights?(:rails, feature: :active_support_events)
Honeybadger.event(name, payload)
end

def record_metrics(name, payload)
return unless Honeybadger.config.load_plugin_insights?(:rails, feature: :metrics)

metric_source "rails"

case name
when "sql.active_record"
gauge("duration.sql.active_record", value: payload[:duration], **payload.slice(:query))
Expand All @@ -46,37 +63,29 @@ def record_metrics(name, payload)
gauge("duration.#{name}", value: payload[:duration], **payload.slice(:store, :key))
end
end

def process?(event, payload)
true
end

def format_payload(payload)
payload
end
end

class ActionControllerSubscriber < NotificationSubscriber
class ActionControllerSubscriber < RailsSubscriber
def format_payload(payload)
payload.except(:headers, :request, :response)
end
end

class ActionControllerCacheSubscriber < NotificationSubscriber
class ActionControllerCacheSubscriber < RailsSubscriber
def format_payload(payload)
payload[:key] = ::ActiveSupport::Cache.expand_cache_key(payload[:key]) if payload[:key]
payload
end
end

class ActiveSupportCacheSubscriber < NotificationSubscriber
class ActiveSupportCacheSubscriber < RailsSubscriber
def format_payload(payload)
payload[:key] = ::ActiveSupport::Cache.expand_cache_key(payload[:key]) if payload[:key]
payload
end
end

class ActiveSupportCacheMultiSubscriber < NotificationSubscriber
class ActiveSupportCacheMultiSubscriber < RailsSubscriber
def format_payload(payload)
payload[:key] = expand_cache_keys_from_payload(payload[:key])
payload[:hits] = expand_cache_keys_from_payload(payload[:hits])
Expand All @@ -94,7 +103,7 @@ def expand_cache_keys_from_payload(data)
end
end

class ActionViewSubscriber < NotificationSubscriber
class ActionViewSubscriber < RailsSubscriber
PROJECT_ROOT = defined?(::Rails) ? ::Rails.root.to_s : ""

def format_payload(payload)
Expand All @@ -105,7 +114,7 @@ def format_payload(payload)
end
end

class ActiveRecordSubscriber < NotificationSubscriber
class ActiveRecordSubscriber < RailsSubscriber
def format_payload(payload)
{
query: Util::SQL.obfuscate(payload[:sql], payload[:connection]&.adapter_name),
Expand All @@ -114,13 +123,13 @@ def format_payload(payload)
}
end

def process?(event, payload)
def process?(name, payload)
return false if payload[:name] == "SCHEMA"
true
end
end

class ActiveJobSubscriber < NotificationSubscriber
class ActiveJobSubscriber < RailsSubscriber
def format_payload(payload)
job = payload[:job]
jobs = payload[:jobs]
Expand All @@ -146,7 +155,7 @@ def format_payload(payload)
end
end

class ActionMailerSubscriber < NotificationSubscriber
class ActionMailerSubscriber < RailsSubscriber
def format_payload(payload)
# Don't include the mail object in the payload...
mail = payload.delete(:mail)
Expand All @@ -162,7 +171,7 @@ def format_payload(payload)
end
end

class ActiveStorageSubscriber < NotificationSubscriber
class ActiveStorageSubscriber < RailsSubscriber
end

class RailsEventSubscriber
Expand Down
28 changes: 28 additions & 0 deletions lib/honeybadger/plugins/active_agent.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require "honeybadger/plugin"
require "honeybadger/notification_subscriber"

module Honeybadger
module Plugins
module ActiveAgent

Plugin.register :active_agent do
requirement { defined?(::ActiveAgent) }
requirement { defined?(::ActiveSupport::Notifications) }

execution do
if config.load_plugin_insights?(:active_agent)
::ActiveSupport::Notifications.subscribe(
/(prompt|embed|stream_open|stream_close|stream_chunk|tool_call|process)\.active_agent/,
Honeybadger::ActiveAgentSubscriber.new
)
end
end
end
end
end
end

module Honeybadger
class ActiveAgentSubscriber < NotificationSubscriber
end
end
8 changes: 8 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@
config.exclude_pattern = "spec/unit/honeybadger/rack/*_spec.rb"
end

begin
# ActiveSupport::Notifications is also a soft dependency.
require "active_support/notifications"
rescue LoadError
puts "Excluding specs which depend on ActiveSupport::Notifications."
# noop
end

config.before(:each, framework: :rails) do
FileUtils.cp_r(FIXTURES_PATH.join("rails"), current_dir)
cd("rails")
Expand Down
80 changes: 80 additions & 0 deletions spec/unit/honeybadger/plugins/active_agent_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
require "honeybadger/plugins/active_agent"
require "honeybadger/config"

describe "Active Agent Dependency" do
let(:config) { Honeybadger::Config.new(logger: NULL_LOGGER, debug: true) }

before do
Honeybadger::Plugin.instances[:active_agent].reset!
end

context "when Active Agent is not installed" do
it "fails quietly" do
expect { Honeybadger::Plugin.instances[:active_agent].load!(config) }.not_to raise_error
end
end

context "when Active Agent is installed", if: defined?(::ActiveSupport::Notifications) do
let(:active_agent_shim) do
Module.new
end

before do
Object.const_set(:ActiveAgent, active_agent_shim)
end

after { Object.send(:remove_const, :ActiveAgent) }

context "when insights are enabled" do
let(:config) { Honeybadger::Config.new(logger: NULL_LOGGER, debug: true, "insights.enabled": true, "active_agent.insights.enabled": true) }
let(:subscriber) { instance_double(Honeybadger::ActiveAgentSubscriber) }

before do
allow(Honeybadger::ActiveAgentSubscriber).to receive(:new).and_return(subscriber)
allow(ActiveSupport::Notifications).to receive(:subscribe)
end

it "subscribes to Active Agent notifications" do
expect(ActiveSupport::Notifications).to receive(:subscribe).with(
match("prompt.active_agent"),
subscriber
)
Honeybadger::Plugin.instances[:active_agent].load!(config)
end
end

context "when insights are disabled" do
let(:config) { Honeybadger::Config.new(logger: NULL_LOGGER, debug: true, "insights.enabled": false) }

before do
allow(ActiveSupport::Notifications).to receive(:subscribe)
end

it "does not subscribe to notifications" do
expect(ActiveSupport::Notifications).not_to receive(:subscribe)
Honeybadger::Plugin.instances[:active_agent].load!(config)
end
end

context "when Active Agent insights are disabled" do
let(:config) { Honeybadger::Config.new(logger: NULL_LOGGER, debug: true, "insights.enabled": true, "active_agent.insights.enabled": false) }

before do
allow(ActiveSupport::Notifications).to receive(:subscribe)
end

it "does not subscribe to notifications" do
expect(ActiveSupport::Notifications).not_to receive(:subscribe)
Honeybadger::Plugin.instances[:active_agent].load!(config)
end
end
end
end

describe Honeybadger::ActiveAgentSubscriber do
let(:subscriber) { described_class.new }

it "is a NotificationSubscriber" do
expect(subscriber).to be_a(Honeybadger::NotificationSubscriber)
end
end
9 changes: 1 addition & 8 deletions spec/unit/honeybadger/plugins/flipper_spec.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
require "honeybadger/plugins/flipper"
require "honeybadger/config"

module ActiveSupport
module Notifications
def self.subscribe(*args)
end
end
end

describe "Flipper Dependency" do
let(:config) { Honeybadger::Config.new(logger: NULL_LOGGER, debug: true) }

Expand All @@ -21,7 +14,7 @@ def self.subscribe(*args)
end
end

context "when flipper is installed" do
context "when flipper is installed", if: defined?(::ActiveSupport::Notifications) do
let(:flipper_shim) do
Module.new
end
Expand Down