From 28a0630d6102a27f9edc625823cc5d417187018e Mon Sep 17 00:00:00 2001 From: Tom de Bruijn Date: Tue, 4 Apr 2017 15:04:22 +0200 Subject: [PATCH] Add ActionCable support Support ActionCable by listening for `perform_action.action_cable` events. This allows us to create performance and exception samples for ActionCable events. --- lib/appsignal/hooks.rb | 1 + lib/appsignal/hooks/action_cable.rb | 45 ++++++++ lib/appsignal/transaction.rb | 1 + spec/lib/appsignal/hooks/action_cable_spec.rb | 106 ++++++++++++++++++ spec/support/helpers/dependency_helper.rb | 4 + 5 files changed, 157 insertions(+) create mode 100644 lib/appsignal/hooks/action_cable.rb create mode 100644 spec/lib/appsignal/hooks/action_cable_spec.rb diff --git a/lib/appsignal/hooks.rb b/lib/appsignal/hooks.rb index a5888ac00..62bb48dbc 100644 --- a/lib/appsignal/hooks.rb +++ b/lib/appsignal/hooks.rb @@ -92,6 +92,7 @@ def format_args(args) end end +require "appsignal/hooks/action_cable" require "appsignal/hooks/active_support_notifications" require "appsignal/hooks/celluloid" require "appsignal/hooks/delayed_job" diff --git a/lib/appsignal/hooks/action_cable.rb b/lib/appsignal/hooks/action_cable.rb new file mode 100644 index 000000000..2e745e361 --- /dev/null +++ b/lib/appsignal/hooks/action_cable.rb @@ -0,0 +1,45 @@ +module Appsignal + class Hooks + # @api private + class ActionCableHook < Appsignal::Hooks::Hook + register :action_cable + + def dependencies_present? + defined?(::ActiveSupport::Notifications::Instrumenter) && + defined?(::ActionCable) + end + + def install + ActiveSupport::Notifications.subscribe("perform_action.action_cable", Subscriber.new) + end + + class Subscriber + def start(_name, id, payload) + # TODO: Set params and filter them, perferably with Rails param + # filtering if present. The params given are not filtered. + + # request = ActionDispatch::Request.new(payload) + Appsignal::Transaction.create( + id, + Appsignal::Transaction::ACTION_CABLE, + {}#, + # :params_method => :filtered_parameters + ) + end + + def finish(_name, _id, payload) + transaction = Appsignal::Transaction.current + + exception = payload[:exception_object] + transaction.set_error(exception) if exception + + transaction.set_action_if_nil( + "#{payload[:channel_class]}##{payload[:action]}" + ) + transaction.set_metadata("method", "websocket") + transaction.complete + end + end + end + end +end diff --git a/lib/appsignal/transaction.rb b/lib/appsignal/transaction.rb index f88db74b0..152c62bdd 100644 --- a/lib/appsignal/transaction.rb +++ b/lib/appsignal/transaction.rb @@ -4,6 +4,7 @@ module Appsignal class Transaction HTTP_REQUEST = "http_request".freeze BACKGROUND_JOB = "background_job".freeze + ACTION_CABLE = "action_cable".freeze FRONTEND = "frontend".freeze BLANK = "".freeze diff --git a/spec/lib/appsignal/hooks/action_cable_spec.rb b/spec/lib/appsignal/hooks/action_cable_spec.rb new file mode 100644 index 000000000..4aea17d49 --- /dev/null +++ b/spec/lib/appsignal/hooks/action_cable_spec.rb @@ -0,0 +1,106 @@ +describe Appsignal::Hooks::ActionCableHook do + if DependencyHelper.action_cable_present? + context "with ActionCable" do + require "action_cable/engine" + + before do + Appsignal.config = project_fixture_config + expect(Appsignal.active?).to be_truthy + Appsignal::Hooks.load_hooks + end + + describe ".dependencies_present?" do + subject { described_class.new.dependencies_present? } + + it "returns true" do + is_expected.to be_truthy + end + end + + describe ".install" do + it "installs the ActionCable subscriber" do + listeners = + ActiveSupport::Notifications.notifier.listeners_for("perform_action.action_cable") + expect(listeners).to_not be_empty + end + end + + describe Appsignal::Hooks::ActionCableHook::Subscriber do + context "without action_cable events" do + it "does not register the event" do + expect(Appsignal::Transaction).to_not receive(:create) + + ActiveSupport::Notifications.instrument("perform_action.foo") do + # nothing + end + end + end + + context "with action_cable events" do + let(:transaction) do + instance_double "Appsignal::Transaction", + :set_http_or_background_action => nil, + :set_http_or_background_queue_start => nil, + :set_metadata => nil, + :set_action => nil, + :set_action_if_nil => nil, + :set_error => nil, + :start_event => nil, + :finish_event => nil, + :complete => nil + end + let(:payload) do + { + :channel_class => "ChannelClass", + :action => "channel_action", + :data => { :foo => :bar } + }.tap do |hash| + hash[:exception_object] = exception if defined?(exception) + end + end + before do + expect(Appsignal::Transaction).to receive(:create) + .with(kind_of(String), Appsignal::Transaction::ACTION_CABLE, kind_of(Hash)) + .and_return(transaction) + allow(Appsignal::Transaction).to receive(:current).and_return(transaction) + end + after do + ActiveSupport::Notifications.instrument("perform_action.action_cable", payload) do + # nothing + end + end + + shared_examples "a ActionCable transaction" do + it "starts and completes a transaction for perform_action.action_cable events" do + expect(transaction).to receive(:set_action_if_nil).with("ChannelClass#channel_action") + expect(transaction).to receive(:set_metadata).with("method", "websocket") + expect(transaction).to receive(:complete) + end + end + + it_behaves_like "a ActionCable transaction" + + context "with an exception" do + let(:exception) { VerySpecificError } + + it_behaves_like "a ActionCable transaction" + + it "registers the exception" do + expect(transaction).to receive(:set_error).with(exception) + end + end + end + end + end + else + context "without ActionCable" do + describe ".dependencies_present?" do + subject { described_class.new.dependencies_present? } + + it "returns false" do + is_expected.to be_falsy + end + end + end + end +end diff --git a/spec/support/helpers/dependency_helper.rb b/spec/support/helpers/dependency_helper.rb index a9d2f0c33..7c08e0dba 100644 --- a/spec/support/helpers/dependency_helper.rb +++ b/spec/support/helpers/dependency_helper.rb @@ -21,6 +21,10 @@ def redis_present? dependency_present? "redis" end + def action_cable_present? + dependency_present? "actioncable" + end + def active_job_present? dependency_present? "activejob" end