From b461be36f4db03060b82c9cf27cdc5c69c109889 Mon Sep 17 00:00:00 2001 From: Tom de Bruijn Date: Tue, 4 Apr 2017 15:04:22 +0200 Subject: [PATCH 1/5] Add ActionCable support Support ActionCable by overriding the `#perform_action` method and wrapping it in an AppSignal transaction. The `#perform_action` is called by ActionCable once a message is send to the channel. We also track subscribe and unsubscribe events as they can also contain complex logic added by the user, which is something we want to track. To track this two "around" callbacks are added to ActionCable on load. --- lib/appsignal/hooks.rb | 1 + lib/appsignal/hooks/action_cable.rb | 95 +++++++ lib/appsignal/transaction.rb | 1 + spec/lib/appsignal/hooks/action_cable_spec.rb | 258 ++++++++++++++++++ spec/support/helpers/dependency_helper.rb | 4 + spec/support/helpers/env_helpers.rb | 2 +- 6 files changed, 360 insertions(+), 1 deletion(-) 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 914ebc7ba..ce842c8d8 100644 --- a/lib/appsignal/hooks.rb +++ b/lib/appsignal/hooks.rb @@ -86,6 +86,7 @@ def extract_value(object_or_hash, field, default_value = nil, convert_to_s = fal 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..b9361b6a2 --- /dev/null +++ b/lib/appsignal/hooks/action_cable.rb @@ -0,0 +1,95 @@ +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 + patch_perform_action + install_callbacks + end + + private + + def patch_perform_action + ActionCable::Channel::Base.class_eval do + alias_method :original_perform_action, :perform_action + + def perform_action(*args, &block) + # The request is only the original websocket request + request = ActionDispatch::Request.new(connection.env) + transaction = Appsignal::Transaction.create( + request.request_id, + Appsignal::Transaction::ACTION_CABLE, + request + ) + + begin + original_perform_action(*args, &block) + rescue => exception + transaction.set_error(exception) + raise exception + ensure + transaction.params = args.first + transaction.set_action_if_nil("#{self.class}##{args.first["action"]}") + transaction.set_metadata("path", request.path) + transaction.set_metadata("method", "websocket") + Appsignal::Transaction.complete_current! + end + end + end + end + + def install_callbacks + ActionCable::Channel::Base.set_callback :subscribe, :around, :prepend => true do |channel, inner| + # The request is only the original websocket request + request = ActionDispatch::Request.new(channel.connection.env) + transaction = Appsignal::Transaction.create( + request.request_id, + Appsignal::Transaction::ACTION_CABLE, + request + ) + + begin + inner.call + rescue => exception + transaction.set_error(exception) + raise exception + ensure + transaction.set_action_if_nil("#{channel.class}#subscribed") + transaction.set_metadata("path", request.path) + transaction.set_metadata("method", "websocket") + Appsignal::Transaction.complete_current! + end + end + + ActionCable::Channel::Base.set_callback :unsubscribe, :around, :prepend => true do |channel, inner| + # The request is only the original websocket request + request = ActionDispatch::Request.new(channel.connection.env) + transaction = Appsignal::Transaction.create( + request.request_id, + Appsignal::Transaction::ACTION_CABLE, + request + ) + + begin + inner.call + rescue => exception + transaction.set_error(exception) + raise exception + ensure + transaction.set_action_if_nil("#{channel.class}#unsubscribed") + transaction.set_metadata("path", request.path) + transaction.set_metadata("method", "websocket") + Appsignal::Transaction.complete_current! + end + end + end + end + end +end diff --git a/lib/appsignal/transaction.rb b/lib/appsignal/transaction.rb index 16ed47539..e9b812c7d 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..9485e63a0 --- /dev/null +++ b/spec/lib/appsignal/hooks/action_cable_spec.rb @@ -0,0 +1,258 @@ +describe Appsignal::Hooks::ActionCableHook do + if DependencyHelper.action_cable_present? + context "with ActionCable" do + require "action_cable/engine" + + describe ".dependencies_present?" do + subject { described_class.new.dependencies_present? } + + it "returns true" do + is_expected.to be_truthy + end + end + + describe ActionCable::Channel::Base do + let(:transaction) do + Appsignal::Transaction.new( + request_id, + Appsignal::Transaction::ACTION_CABLE, + ActionDispatch::Request.new(env) + ) + end + let(:channel) do + Class.new(ActionCable::Channel::Base) do + def speak(_data) + end + + def self.to_s + "MyChannel" + end + end + end + let(:log) { StringIO.new } + let(:server) do + ActionCable::Server::Base.new.tap do |s| + s.config.logger = Logger.new(log) + end + end + let(:connection) { ActionCable::Connection::Base.new(server, env) } + let(:identifier) { { :channel => "MyChannel" }.to_json } + let(:params) { {} } + let(:request_id) { SecureRandom.uuid } + let(:env) do + http_request_env_with_data("action_dispatch.request_id" => request_id, :params => params) + end + let(:instance) { channel.new(connection, identifier, params) } + subject { transaction.to_h } + before do + start_agent + expect(Appsignal.active?).to be_truthy + transaction + + expect(Appsignal::Transaction).to receive(:create) + .with(request_id, Appsignal::Transaction::ACTION_CABLE, kind_of(ActionDispatch::Request)) + .and_return(transaction) + allow(Appsignal::Transaction).to receive(:current).and_return(transaction) + expect(transaction.ext).to receive(:complete) # and do nothing + + # TODO: Nicer way to stub this without a websocket? + allow(connection).to receive(:websocket).and_return(double(:transmit => nil)) + end + + describe "#perform_action" do + it "creates a transaction for an action" do + instance.perform_action("message" => "foo", "action" => "speak") + + expect(subject).to include( + "action" => "MyChannel#speak", + "error" => nil, + "id" => request_id, + "namespace" => Appsignal::Transaction::ACTION_CABLE, + "metadata" => { + "method" => "websocket", + "path" => "/blog" + } + ) + expect(subject["sample_data"]).to include( + "params" => { + "action" => "speak", + "message" => "foo" + } + ) + end + + context "with an error in the action" do + let(:channel) do + Class.new(ActionCable::Channel::Base) do + def speak(_data) + raise VerySpecificError, "oh no!" + end + + def self.to_s + "MyChannel" + end + end + end + + it "registers an error on the transaction" do + expect do + instance.perform_action("message" => "foo", "action" => "speak") + end.to raise_error(VerySpecificError) + + expect(subject).to include( + "action" => "MyChannel#speak", + "id" => request_id, + "namespace" => Appsignal::Transaction::ACTION_CABLE, + "metadata" => { + "method" => "websocket", + "path" => "/blog" + } + ) + expect(subject["error"]).to include( + "backtrace" => kind_of(String), + "name" => "VerySpecificError", + "message" => "oh no!" + ) + expect(subject["sample_data"]).to include( + "params" => { + "action" => "speak", + "message" => "foo" + } + ) + end + end + end + + describe "subscribe callback" do + let(:params) { { "internal" => true } } + + it "creates a transaction for a subscription" do + instance.subscribe_to_channel + + expect(subject).to include( + "action" => "MyChannel#subscribed", + "error" => nil, + "id" => request_id, + "namespace" => Appsignal::Transaction::ACTION_CABLE, + "metadata" => { + "method" => "websocket", + "path" => "/blog" + } + ) + expect(subject["sample_data"]).to include( + "params" => { "internal" => "true" } + ) + end + + context "with an error in the callback" do + let(:channel) do + Class.new(ActionCable::Channel::Base) do + def subscribed + raise VerySpecificError, "oh no!" + end + + def self.to_s + "MyChannel" + end + end + end + + it "registers an error on the transaction" do + expect do + instance.subscribe_to_channel + end.to raise_error(VerySpecificError) + + expect(subject).to include( + "action" => "MyChannel#subscribed", + "id" => request_id, + "namespace" => Appsignal::Transaction::ACTION_CABLE, + "metadata" => { + "method" => "websocket", + "path" => "/blog" + } + ) + expect(subject["error"]).to include( + "backtrace" => kind_of(String), + "name" => "VerySpecificError", + "message" => "oh no!" + ) + expect(subject["sample_data"]).to include( + "params" => { "internal" => "true" } + ) + end + end + end + + describe "unsubscribe callback" do + let(:params) { { "internal" => true } } + + it "creates a transaction for a subscription" do + instance.unsubscribe_from_channel + + expect(subject).to include( + "action" => "MyChannel#unsubscribed", + "error" => nil, + "id" => request_id, + "namespace" => Appsignal::Transaction::ACTION_CABLE, + "metadata" => { + "method" => "websocket", + "path" => "/blog" + } + ) + expect(subject["sample_data"]).to include( + "params" => { "internal" => "true" } + ) + end + + context "with an error in the callback" do + let(:channel) do + Class.new(ActionCable::Channel::Base) do + def unsubscribed + raise VerySpecificError, "oh no!" + end + + def self.to_s + "MyChannel" + end + end + end + + it "registers an error on the transaction" do + expect do + instance.unsubscribe_from_channel + end.to raise_error(VerySpecificError) + + expect(subject).to include( + "action" => "MyChannel#unsubscribed", + "id" => request_id, + "namespace" => Appsignal::Transaction::ACTION_CABLE, + "metadata" => { + "method" => "websocket", + "path" => "/blog" + } + ) + expect(subject["error"]).to include( + "backtrace" => kind_of(String), + "name" => "VerySpecificError", + "message" => "oh no!" + ) + expect(subject["sample_data"]).to include( + "params" => { "internal" => "true" } + ) + 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 diff --git a/spec/support/helpers/env_helpers.rb b/spec/support/helpers/env_helpers.rb index 41e5b9079..5b3f74255 100644 --- a/spec/support/helpers/env_helpers.rb +++ b/spec/support/helpers/env_helpers.rb @@ -3,7 +3,7 @@ def http_request_env_with_data(args = {}) path = args.delete(:path) || "/blog" Rack::MockRequest.env_for( path, - :params => { + :params => args[:params] || { "controller" => "blog_posts", "action" => "show", "id" => "1" From db4cf6537366726107ff1705e1a131f5fb532c22 Mon Sep 17 00:00:00 2001 From: Tom de Bruijn Date: Thu, 8 Jun 2017 16:09:28 +0200 Subject: [PATCH 2/5] Wrap ActionCable callbacks in events Otherwise the event timeline would only show up if custom/other instrumentation was added/is active. They would be listed without a related (ActionCable) parent event. --- lib/appsignal/hooks/action_cable.rb | 8 +++- spec/lib/appsignal/hooks/action_cable_spec.rb | 42 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/lib/appsignal/hooks/action_cable.rb b/lib/appsignal/hooks/action_cable.rb index b9361b6a2..f2da5e425 100644 --- a/lib/appsignal/hooks/action_cable.rb +++ b/lib/appsignal/hooks/action_cable.rb @@ -56,7 +56,9 @@ def install_callbacks ) begin - inner.call + Appsignal.instrument "subscribed.action_cable" do + inner.call + end rescue => exception transaction.set_error(exception) raise exception @@ -78,7 +80,9 @@ def install_callbacks ) begin - inner.call + Appsignal.instrument "unsubscribed.action_cable" do + inner.call + end rescue => exception transaction.set_error(exception) raise exception diff --git a/spec/lib/appsignal/hooks/action_cable_spec.rb b/spec/lib/appsignal/hooks/action_cable_spec.rb index 9485e63a0..8a55b82b0 100644 --- a/spec/lib/appsignal/hooks/action_cable_spec.rb +++ b/spec/lib/appsignal/hooks/action_cable_spec.rb @@ -73,6 +73,20 @@ def self.to_s "path" => "/blog" } ) + expect(subject["events"].first).to include( + "allocation_count" => kind_of(Integer), + "body" => "", + "body_format" => Appsignal::EventFormatter::DEFAULT, + "child_allocation_count" => kind_of(Integer), + "child_duration" => kind_of(Float), + "child_gc_duration" => kind_of(Float), + "count" => 1, + "gc_duration" => kind_of(Float), + "start" => kind_of(Float), + "duration" => kind_of(Float), + "name" => "perform_action.action_cable", + "title" => "" + ) expect(subject["sample_data"]).to include( "params" => { "action" => "speak", @@ -139,6 +153,20 @@ def self.to_s "path" => "/blog" } ) + expect(subject["events"].first).to include( + "allocation_count" => kind_of(Integer), + "body" => "", + "body_format" => Appsignal::EventFormatter::DEFAULT, + "child_allocation_count" => kind_of(Integer), + "child_duration" => kind_of(Float), + "child_gc_duration" => kind_of(Float), + "count" => 1, + "gc_duration" => kind_of(Float), + "start" => kind_of(Float), + "duration" => kind_of(Float), + "name" => "subscribed.action_cable", + "title" => "" + ) expect(subject["sample_data"]).to include( "params" => { "internal" => "true" } ) @@ -199,6 +227,20 @@ def self.to_s "path" => "/blog" } ) + expect(subject["events"].first).to include( + "allocation_count" => kind_of(Integer), + "body" => "", + "body_format" => Appsignal::EventFormatter::DEFAULT, + "child_allocation_count" => kind_of(Integer), + "child_duration" => kind_of(Float), + "child_gc_duration" => kind_of(Float), + "count" => 1, + "gc_duration" => kind_of(Float), + "start" => kind_of(Float), + "duration" => kind_of(Float), + "name" => "unsubscribed.action_cable", + "title" => "" + ) expect(subject["sample_data"]).to include( "params" => { "internal" => "true" } ) From 97f82519966c4c9db64d8a4b7e515c517d3ee223 Mon Sep 17 00:00:00 2001 From: Tom de Bruijn Date: Thu, 8 Jun 2017 17:39:31 +0200 Subject: [PATCH 3/5] Update TODO comment and Logger used in tests Can't find a way to easily create a websocket class for ActionCable. Instead, I'll leave it like this. Best thing to do is create a higher level integration test, but this will do for now. --- spec/lib/appsignal/hooks/action_cable_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/appsignal/hooks/action_cable_spec.rb b/spec/lib/appsignal/hooks/action_cable_spec.rb index 8a55b82b0..8346b5740 100644 --- a/spec/lib/appsignal/hooks/action_cable_spec.rb +++ b/spec/lib/appsignal/hooks/action_cable_spec.rb @@ -32,7 +32,7 @@ def self.to_s let(:log) { StringIO.new } let(:server) do ActionCable::Server::Base.new.tap do |s| - s.config.logger = Logger.new(log) + s.config.logger = ActiveSupport::Logger.new(log) end end let(:connection) { ActionCable::Connection::Base.new(server, env) } @@ -55,7 +55,7 @@ def self.to_s allow(Appsignal::Transaction).to receive(:current).and_return(transaction) expect(transaction.ext).to receive(:complete) # and do nothing - # TODO: Nicer way to stub this without a websocket? + # Stub transmit call for subscribe/unsubscribe tests allow(connection).to receive(:websocket).and_return(double(:transmit => nil)) end From 30ab47ad65d7e70a5bbba34ee633dd99155a20fc Mon Sep 17 00:00:00 2001 From: Tom de Bruijn Date: Fri, 9 Jun 2017 00:21:52 +0200 Subject: [PATCH 4/5] Use `instance_double` for stubbed websocket --- spec/lib/appsignal/hooks/action_cable_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/lib/appsignal/hooks/action_cable_spec.rb b/spec/lib/appsignal/hooks/action_cable_spec.rb index 8346b5740..0990894dd 100644 --- a/spec/lib/appsignal/hooks/action_cable_spec.rb +++ b/spec/lib/appsignal/hooks/action_cable_spec.rb @@ -56,7 +56,8 @@ def self.to_s expect(transaction.ext).to receive(:complete) # and do nothing # Stub transmit call for subscribe/unsubscribe tests - allow(connection).to receive(:websocket).and_return(double(:transmit => nil)) + allow(connection).to receive(:websocket) + .and_return(instance_double("ActionCable::Connection::WebSocket", :transmit => nil)) end describe "#perform_action" do From ca8e55d15314de30b2b66c98d94246bd89d0d07f Mon Sep 17 00:00:00 2001 From: Tom de Bruijn Date: Tue, 13 Jun 2017 15:17:00 +0200 Subject: [PATCH 5/5] Add support for standalone ActionCable servers Standalone ActionCable servers don't have an ActionDispatch request_id in the request env. Instead, we create our own and add it to the request under a private key. This key is then used to save the request id, also for non-standalone servers, and reused throughout the duration of the websocket. This allows us to track all messages on a websocket under the same request_id. This could make searching for all samples on one websocket connection a lot easier. --- lib/appsignal/hooks/action_cable.rb | 26 ++++-- spec/lib/appsignal/hooks/action_cable_spec.rb | 87 +++++++++++++++++-- 2 files changed, 98 insertions(+), 15 deletions(-) diff --git a/lib/appsignal/hooks/action_cable.rb b/lib/appsignal/hooks/action_cable.rb index f2da5e425..68adcc4f0 100644 --- a/lib/appsignal/hooks/action_cable.rb +++ b/lib/appsignal/hooks/action_cable.rb @@ -4,6 +4,8 @@ class Hooks class ActionCableHook < Appsignal::Hooks::Hook register :action_cable + REQUEST_ID = "_appsignal_action_cable.request_id".freeze + def dependencies_present? defined?(::ActiveSupport::Notifications::Instrumenter) && defined?(::ActionCable) @@ -22,9 +24,13 @@ def patch_perform_action def perform_action(*args, &block) # The request is only the original websocket request - request = ActionDispatch::Request.new(connection.env) + env = connection.env + request = ActionDispatch::Request.new(env) + env[Appsignal::Hooks::ActionCableHook::REQUEST_ID] ||= + request.request_id || SecureRandom.uuid + transaction = Appsignal::Transaction.create( - request.request_id, + env[Appsignal::Hooks::ActionCableHook::REQUEST_ID], Appsignal::Transaction::ACTION_CABLE, request ) @@ -48,9 +54,13 @@ def perform_action(*args, &block) def install_callbacks ActionCable::Channel::Base.set_callback :subscribe, :around, :prepend => true do |channel, inner| # The request is only the original websocket request - request = ActionDispatch::Request.new(channel.connection.env) + env = channel.connection.env + request = ActionDispatch::Request.new(env) + env[Appsignal::Hooks::ActionCableHook::REQUEST_ID] ||= + request.request_id || SecureRandom.uuid + transaction = Appsignal::Transaction.create( - request.request_id, + env[Appsignal::Hooks::ActionCableHook::REQUEST_ID], Appsignal::Transaction::ACTION_CABLE, request ) @@ -72,9 +82,13 @@ def install_callbacks ActionCable::Channel::Base.set_callback :unsubscribe, :around, :prepend => true do |channel, inner| # The request is only the original websocket request - request = ActionDispatch::Request.new(channel.connection.env) + env = channel.connection.env + request = ActionDispatch::Request.new(env) + env[Appsignal::Hooks::ActionCableHook::REQUEST_ID] ||= + request.request_id || SecureRandom.uuid + transaction = Appsignal::Transaction.create( - request.request_id, + env[Appsignal::Hooks::ActionCableHook::REQUEST_ID], Appsignal::Transaction::ACTION_CABLE, request ) diff --git a/spec/lib/appsignal/hooks/action_cable_spec.rb b/spec/lib/appsignal/hooks/action_cable_spec.rb index 0990894dd..344ca2bd3 100644 --- a/spec/lib/appsignal/hooks/action_cable_spec.rb +++ b/spec/lib/appsignal/hooks/action_cable_spec.rb @@ -14,7 +14,7 @@ describe ActionCable::Channel::Base do let(:transaction) do Appsignal::Transaction.new( - request_id, + transaction_id, Appsignal::Transaction::ACTION_CABLE, ActionDispatch::Request.new(env) ) @@ -39,6 +39,7 @@ def self.to_s let(:identifier) { { :channel => "MyChannel" }.to_json } let(:params) { {} } let(:request_id) { SecureRandom.uuid } + let(:transaction_id) { request_id } let(:env) do http_request_env_with_data("action_dispatch.request_id" => request_id, :params => params) end @@ -50,10 +51,14 @@ def self.to_s transaction expect(Appsignal::Transaction).to receive(:create) - .with(request_id, Appsignal::Transaction::ACTION_CABLE, kind_of(ActionDispatch::Request)) + .with(transaction_id, Appsignal::Transaction::ACTION_CABLE, kind_of(ActionDispatch::Request)) .and_return(transaction) allow(Appsignal::Transaction).to receive(:current).and_return(transaction) - expect(transaction.ext).to receive(:complete) # and do nothing + # Make sure sample data is added + expect(transaction.ext).to receive(:finish).and_return(true) + # Stub complete call, stops it from being cleared in the extension + # And allows us to call `#to_h` on it after it's been completed. + expect(transaction.ext).to receive(:complete) # Stub transmit call for subscribe/unsubscribe tests allow(connection).to receive(:websocket) @@ -67,7 +72,7 @@ def self.to_s expect(subject).to include( "action" => "MyChannel#speak", "error" => nil, - "id" => request_id, + "id" => transaction_id, "namespace" => Appsignal::Transaction::ACTION_CABLE, "metadata" => { "method" => "websocket", @@ -96,6 +101,44 @@ def self.to_s ) end + context "without request_id (standalone server)" do + let(:request_id) { nil } + let(:transaction_id) { SecureRandom.uuid } + let(:action_transaction) do + Appsignal::Transaction.new( + transaction_id, + Appsignal::Transaction::ACTION_CABLE, + ActionDispatch::Request.new(env) + ) + end + before do + # Stub future (private AppSignal) transaction id generated by the hook. + expect(SecureRandom).to receive(:uuid).and_return(transaction_id) + end + + it "uses its own internal request_id set by the subscribed callback" do + # Subscribe action, sets the request_id + instance.subscribe_to_channel + expect(transaction.to_h["id"]).to eq(transaction_id) + + # Expect another transaction for the action. + # This transaction will use the same request_id as the + # transaction id used to subscribe to the channel. + expect(Appsignal::Transaction).to receive(:create).with( + transaction_id, + Appsignal::Transaction::ACTION_CABLE, + kind_of(ActionDispatch::Request) + ).and_return(action_transaction) + allow(Appsignal::Transaction).to receive(:current).and_return(action_transaction) + # Stub complete call, stops it from being cleared in the extension + # And allows us to call `#to_h` on it after it's been completed. + expect(action_transaction.ext).to receive(:complete) + + instance.perform_action("message" => "foo", "action" => "speak") + expect(action_transaction.to_h["id"]).to eq(transaction_id) + end + end + context "with an error in the action" do let(:channel) do Class.new(ActionCable::Channel::Base) do @@ -116,7 +159,7 @@ def self.to_s expect(subject).to include( "action" => "MyChannel#speak", - "id" => request_id, + "id" => transaction_id, "namespace" => Appsignal::Transaction::ACTION_CABLE, "metadata" => { "method" => "websocket", @@ -147,7 +190,7 @@ def self.to_s expect(subject).to include( "action" => "MyChannel#subscribed", "error" => nil, - "id" => request_id, + "id" => transaction_id, "namespace" => Appsignal::Transaction::ACTION_CABLE, "metadata" => { "method" => "websocket", @@ -173,6 +216,19 @@ def self.to_s ) end + context "without request_id (standalone server)" do + let(:request_id) { nil } + let(:transaction_id) { SecureRandom.uuid } + before do + allow(SecureRandom).to receive(:uuid).and_return(transaction_id) + instance.subscribe_to_channel + end + + it "uses its own internal request_id" do + expect(subject["id"]).to eq(transaction_id) + end + end + context "with an error in the callback" do let(:channel) do Class.new(ActionCable::Channel::Base) do @@ -193,7 +249,7 @@ def self.to_s expect(subject).to include( "action" => "MyChannel#subscribed", - "id" => request_id, + "id" => transaction_id, "namespace" => Appsignal::Transaction::ACTION_CABLE, "metadata" => { "method" => "websocket", @@ -221,7 +277,7 @@ def self.to_s expect(subject).to include( "action" => "MyChannel#unsubscribed", "error" => nil, - "id" => request_id, + "id" => transaction_id, "namespace" => Appsignal::Transaction::ACTION_CABLE, "metadata" => { "method" => "websocket", @@ -247,6 +303,19 @@ def self.to_s ) end + context "without request_id (standalone server)" do + let(:request_id) { nil } + let(:transaction_id) { SecureRandom.uuid } + before do + allow(SecureRandom).to receive(:uuid).and_return(transaction_id) + instance.unsubscribe_from_channel + end + + it "uses its own internal request_id" do + expect(subject["id"]).to eq(transaction_id) + end + end + context "with an error in the callback" do let(:channel) do Class.new(ActionCable::Channel::Base) do @@ -267,7 +336,7 @@ def self.to_s expect(subject).to include( "action" => "MyChannel#unsubscribed", - "id" => request_id, + "id" => transaction_id, "namespace" => Appsignal::Transaction::ACTION_CABLE, "metadata" => { "method" => "websocket",