spec/lib/appsignal/transaction_spec.rb in appsignal-2.2.1 vs spec/lib/appsignal/transaction_spec.rb in appsignal-2.3.0.beta.1

- old
+ new

@@ -1,156 +1,217 @@ describe Appsignal::Transaction do before :context do start_agent end - let(:time) { Time.at(fixed_time) } - let(:namespace) { Appsignal::Transaction::HTTP_REQUEST } - let(:env) { {} } - let(:merged_env) { http_request_env_with_data(env) } - let(:options) { {} } - let(:request) { Rack::Request.new(merged_env) } - let(:transaction) { Appsignal::Transaction.new("1", namespace, request, options) } + let(:transaction_id) { "1" } + let(:time) { Time.at(fixed_time) } + let(:namespace) { Appsignal::Transaction::HTTP_REQUEST } + let(:env) { {} } + let(:merged_env) { http_request_env_with_data(env) } + let(:options) { {} } + let(:request) { Rack::Request.new(merged_env) } + let(:transaction) { Appsignal::Transaction.new(transaction_id, namespace, request, options) } + let(:log) { StringIO.new } before { Timecop.freeze(time) } - after { Timecop.return } + after { Timecop.return } + around do |example| + use_logger_with log do + example.run + end + end describe "class methods" do + def current_transaction + Appsignal::Transaction.current + end + describe ".create" do - it "should add the transaction to thread local" do - expect(Appsignal::Extension).to receive(:start_transaction).with("1", "http_request", 0) + def create_transaction(id = transaction_id) + Appsignal::Transaction.create(id, namespace, request, options) + end - created_transaction = Appsignal::Transaction.create("1", namespace, request, options) + context "when no transaction is running" do + let!(:transaction) { create_transaction } - expect(Thread.current[:appsignal_transaction]).to eq created_transaction - end + it "returns the created transaction" do + expect(transaction).to be_a Appsignal::Transaction + expect(transaction.transaction_id).to eq transaction_id + expect(transaction.namespace).to eq namespace + expect(transaction.request).to eq request - it "should create a transaction" do - created_transaction = Appsignal::Transaction.create("1", namespace, request, options) + expect(transaction.to_h).to include( + "id" => transaction_id, + "namespace" => namespace + ) + end - expect(created_transaction).to be_a Appsignal::Transaction - expect(created_transaction.transaction_id).to eq "1" - expect(created_transaction.namespace).to eq "http_request" + it "assigns the transaction to current" do + expect(transaction).to eq current_transaction + end end context "when a transaction is already running" do - let(:running_transaction) { double(:transaction_id => 2) } - before { Thread.current[:appsignal_transaction] = running_transaction } + before { create_transaction } - it "should not create a new transaction" do - expect( - Appsignal::Transaction.create("1", namespace, request, options) - ).to eq(running_transaction) + it "does not create a new transaction, but returns the current transaction" do + expect do + new_transaction = create_transaction("2") + expect(new_transaction).to eq(current_transaction) + expect(new_transaction.transaction_id).to eq(transaction_id) + end.to_not change { current_transaction } end - it "should output a debug message" do - expect(Appsignal.logger).to receive(:debug) - .with("Trying to start new transaction 1 but 2 is already running. Using 2") - - Appsignal::Transaction.create("1", namespace, request, options) + it "logs a debug message" do + create_transaction("2") + expect(log_contents(log)).to contains_log :debug, + "Trying to start new transaction with id '2', but a " \ + "transaction with id '#{transaction_id}' is already " \ + "running. Using transaction '#{transaction_id}'." end - context "with option to force a new transaction" do - let(:options) { { :force => true } } - it "should not create a new transaction" do - expect( - Appsignal::Transaction.create("1", namespace, request, options) - ).to_not eq(running_transaction) + context "with option :force => true" do + it "returns the newly created (and current) transaction" do + original_transaction = current_transaction + expect(original_transaction).to_not be_nil + expect(current_transaction.transaction_id).to eq transaction_id + + options[:force] = true + expect(create_transaction("2")).to_not eq original_transaction + expect(current_transaction.transaction_id).to eq "2" end end end end describe ".current" do - before { Thread.current[:appsignal_transaction] = transaction } - subject { Appsignal::Transaction.current } - context "if there is a transaction" do - before { Appsignal::Transaction.create("1", namespace, request, options) } + context "when there is a current transaction" do + let!(:transaction) do + Appsignal::Transaction.create(transaction_id, namespace, request, options) + end - it "should return the correct transaction" do - is_expected.to eq transaction + it "reads :appsignal_transaction from the current Thread" do + expect(subject).to eq Thread.current[:appsignal_transaction] + expect(subject).to eq transaction end - it "should indicate it's not a nil transaction" do - expect(subject.nil_transaction?).to be_falsy + it "is not a NilTransaction" do + expect(subject.nil_transaction?).to eq false + expect(subject).to be_a Appsignal::Transaction end end - context "if there is no transaction" do - before do - Thread.current[:appsignal_transaction] = nil + context "when there is no current transaction" do + it "has no :appsignal_transaction registered on the current Thread" do + expect(Thread.current[:appsignal_transaction]).to be_nil end - it "should return a nil transaction stub" do - is_expected.to be_a Appsignal::Transaction::NilTransaction + it "returns a NilTransaction stub" do + expect(subject.nil_transaction?).to eq true + expect(subject).to be_a Appsignal::Transaction::NilTransaction end - - it "should indicate it's a nil transaction" do - expect(subject.nil_transaction?).to be_truthy - end end end - describe "complete_current!" do - before { Appsignal::Transaction.create("2", Appsignal::Transaction::HTTP_REQUEST, {}) } + describe ".complete_current!" do + let!(:transaction) { Appsignal::Transaction.create(transaction_id, namespace, options) } - it "should complete the current transaction and set the thread appsignal_transaction to nil" do - expect(Appsignal::Transaction.current).to receive(:complete) + it "completes the current transaction" do + expect(transaction).to eq current_transaction + expect(transaction).to receive(:complete).and_call_original Appsignal::Transaction.complete_current! - - expect(Thread.current[:appsignal_transaction]).to be_nil end - it "should still clear the transaction if there is an error" do - expect(Appsignal::Transaction.current).to receive(:complete).and_raise "Error" - - Appsignal::Transaction.complete_current! - - expect(Thread.current[:appsignal_transaction]).to be_nil + it "unsets the current transaction on the current Thread" do + expect do + Appsignal::Transaction.complete_current! + end.to change { Thread.current[:appsignal_transaction] }.from(transaction).to(nil) end - context "if a transaction is discarded" do - it "should not complete the transaction" do - expect(Appsignal::Transaction.current.ext).to_not receive(:complete) + context "when encountering an error while completing" do + before do + expect(transaction).to receive(:complete).and_raise VerySpecificError + end - Appsignal::Transaction.current.discard! - expect(Appsignal::Transaction.current.discarded?).to be_truthy - + it "logs an error message" do Appsignal::Transaction.complete_current! - - expect(Thread.current[:appsignal_transaction]).to be_nil + expect(log_contents(log)).to contains_log :error, + "Failed to complete transaction ##{transaction.transaction_id}. VerySpecificError" end - it "should not be discarded when restore! is called" do - Appsignal::Transaction.current.discard! - expect(Appsignal::Transaction.current.discarded?).to be_truthy - Appsignal::Transaction.current.restore! - expect(Appsignal::Transaction.current.discarded?).to be_falsy + it "clears the current transaction" do + expect do + Appsignal::Transaction.complete_current! + end.to change { Thread.current[:appsignal_transaction] }.from(transaction).to(nil) end end end end describe "#complete" do - it "should sample data if it needs to be sampled" do - expect(transaction.ext).to receive(:finish).and_return(true) - expect(transaction).to receive(:sample_data) - expect(transaction.ext).to receive(:complete) + context "when transaction is being sampled" do + it "samples data" do + expect(transaction.ext).to receive(:finish).and_return(true) + # Stub call to extension, because that would remove the transaction + # from the extension. + expect(transaction.ext).to receive(:complete) - transaction.complete + transaction.set_tags(:foo => "bar") + transaction.complete + expect(transaction.to_h["sample_data"]).to include( + "tags" => { "foo" => "bar" } + ) + end end - it "should not sample data if it does not need to be sampled" do - expect(transaction.ext).to receive(:finish).and_return(false) - expect(transaction).to_not receive(:sample_data) - expect(transaction.ext).to receive(:complete) + context "when transaction is not being sampled" do + it "does not sample data" do + expect(transaction).to_not receive(:sample_data) + expect(transaction.ext).to receive(:finish).and_return(false) + expect(transaction.ext).to receive(:complete).and_call_original - transaction.complete + transaction.complete + end end + + context "when a transaction is marked as discarded" do + it "does not complete the transaction" do + expect(transaction.ext).to_not receive(:complete) + + expect do + transaction.discard! + end.to change { transaction.discarded? }.from(false).to(true) + + transaction.complete + end + + it "logs a debug message" do + transaction.discard! + transaction.complete + + expect(log_contents(log)).to contains_log :debug, + "Skipping transaction '#{transaction_id}' because it was manually discarded." + end + + context "when a discarded transaction is restored" do + before { transaction.discard! } + + it "completes the transaction" do + expect(transaction.ext).to receive(:complete).and_call_original + + expect do + transaction.restore! + end.to change { transaction.discarded? }.from(true).to(false) + + transaction.complete + end + end + end end context "pausing" do describe "#pause!" do it "should change the pause flag to true" do @@ -239,10 +300,41 @@ expect(transaction.store("test")).to eql("transaction" => "value") end end + describe "#params" do + subject { transaction.params } + + context "with custom params set on transaction" do + before do + transaction.params = { :foo => "bar" } + end + + it "returns custom parameters" do + expect(subject).to eq(:foo => "bar") + end + end + + context "without custom params set on transaction" do + it "returns parameters from request" do + expect(subject).to eq( + "action" => "show", + "controller" => "blog_posts", + "id" => "1" + ) + end + end + end + + describe "#params=" do + it "sets params on the transaction" do + transaction.params = { :foo => "bar" } + expect(transaction.params).to eq(:foo => "bar") + end + end + describe "#set_tags" do it "should add tags to transaction" do expect do transaction.set_tags("a" => "b") end.to change(transaction, :tags).to("a" => "b") @@ -731,23 +823,42 @@ end describe "#sanitized_params" do subject { transaction.send(:sanitized_params) } - context "without params" do + context "with custom params" do + before do + transaction.params = { :foo => "bar", :baz => :bat } + end + + it "returns custom params" do + is_expected.to eq(:foo => "bar", :baz => :bat) + end + + context "with AppSignal filtering" do + before { Appsignal.config.config_hash[:filter_parameters] = %w(foo) } + after { Appsignal.config.config_hash[:filter_parameters] = [] } + + it "returns sanitized custom params" do + expect(subject).to eq(:foo => "[FILTERED]", :baz => :bat) + end + end + end + + context "without request params" do before { allow(transaction.request).to receive(:params).and_return(nil) } it { is_expected.to be_nil } end - context "when params crashes" do + context "when request params crashes" do before { allow(transaction.request).to receive(:params).and_raise(NoMethodError) } it { is_expected.to be_nil } end - context "when params method does not exist" do + context "when request params method does not exist" do let(:options) { { :params_method => :nonsense } } it { is_expected.to be_nil } end @@ -976,9 +1087,37 @@ line.tr("2", "?") end expect(subject).to eq ["line 1", "line ?"] end end + end + end + end + + describe ".to_hash / .to_h" do + subject { transaction.to_hash } + + context "when extension returns serialized JSON" do + it "parses the result and returns a Hash" do + expect(subject).to include( + "action" => nil, + "error" => nil, + "events" => [], + "id" => transaction_id, + "metadata" => {}, + "namespace" => namespace, + "sample_data" => {} + ) + end + end + + context "when the extension returns invalid serialized JSON" do + before do + expect(transaction.ext).to receive(:to_json).and_return("foo") + end + + it "raises a JSON parse error" do + expect { subject }.to raise_error(JSON::ParserError) end end end describe Appsignal::Transaction::NilTransaction do