diff --git a/.changesets/add-helper-to-create-custom-markers-from-the-ruby-gem.md b/.changesets/add-helper-to-create-custom-markers-from-the-ruby-gem.md new file mode 100644 index 00000000..7a97cf84 --- /dev/null +++ b/.changesets/add-helper-to-create-custom-markers-from-the-ruby-gem.md @@ -0,0 +1,31 @@ +--- +bump: minor +type: add +--- + +Add a helper to create custom markers from the Ruby gem. + +Create a custom marker (a little icon shown in the graph timeline on AppSignal.com) to mark events on the timeline. + +Create a marker with all the available options: + +```ruby +Appsignal::CustomMarker.report( + # The icon shown on the timeline + :icon => "🎉", + # The message shown on hover + :message => "Migration completed", + # Any time object or a string with a ISO8601 valid time is accepted + :created_at => Time.now +) +``` + +Create a marker with just a message: + +```ruby +Appsignal::CustomMarker.report( + :message => "Migration completed", +) +``` + +_The default icon is the 🚀 icon. The default time is the time the request is received by our servers._ diff --git a/lib/appsignal.rb b/lib/appsignal.rb index 1859ba99..b6f012d6 100644 --- a/lib/appsignal.rb +++ b/lib/appsignal.rb @@ -558,6 +558,7 @@ def collect_environment_metadata require "appsignal/hooks" require "appsignal/probes" require "appsignal/marker" +require "appsignal/custom_marker" require "appsignal/garbage_collection" require "appsignal/rack" require "appsignal/rack/body_wrapper" diff --git a/lib/appsignal/custom_marker.rb b/lib/appsignal/custom_marker.rb new file mode 100644 index 00000000..e5c60306 --- /dev/null +++ b/lib/appsignal/custom_marker.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Appsignal + # Custom markers are used on AppSignal.com to indicate events in an + # application, to give additional context on graph timelines. + # + # This helper class will send a request to the AppSignal public endpoint to + # create a Custom marker for the application on AppSignal.com. + # + # @see https://docs.appsignal.com/api/public-endpoint/custom-markers.html + # Public Endpoint API markers endpoint documentation + # @see https://docs.appsignal.com/appsignal/terminology.html#markers + # Terminology: Markers + class CustomMarker + # @param icon [String] icon to use for the marker, like an emoji. + # @param message [String] name of the user that is creating the + # marker. + # @param created_at [Time/String] A Ruby time object or a valid ISO8601 + # timestamp. + # @return [Boolean] + def self.report( + icon: nil, + message: nil, + created_at: nil + ) + new( + { + :icon => icon, + :message => message, + :created_at => created_at.respond_to?(:iso8601) ? created_at.iso8601 : created_at + }.compact + ).transmit + end + + # @api private + def initialize(marker_data) + @marker_data = marker_data + end + + # @api private + def transmit + unless Appsignal.config + Appsignal.internal_logger.warn( + "Did not transmit custom marker: no AppSignal config loaded" + ) + return false + end + + transmitter = Transmitter.new( + "#{Appsignal.config[:logging_endpoint]}/markers", + Appsignal.config + ) + response = transmitter.transmit(@marker_data) + + if (200...300).include?(response.code.to_i) + Appsignal.internal_logger.info("Transmitted custom marker") + true + else + Appsignal.internal_logger.error( + "Failed to transmit custom marker: #{response.code} status code" + ) + false + end + rescue => e + Appsignal.internal_logger.error( + "Failed to transmit custom marker: #{e.class}: #{e.message}\n" \ + "#{e.backtrace}" + ) + false + end + end +end diff --git a/spec/lib/appsignal/custom_marker_spec.rb b/spec/lib/appsignal/custom_marker_spec.rb new file mode 100644 index 00000000..4e727983 --- /dev/null +++ b/spec/lib/appsignal/custom_marker_spec.rb @@ -0,0 +1,155 @@ +describe Appsignal::CustomMarker do + let(:config) { build_config } + + describe "#transmit" do + # def stub_marker_request + # stub_api_request config, "markers", marker.marker_data + # end + + def create_marker( + icon: nil, + message: nil, + created_at: nil + ) + described_class.report( + :icon => icon, + :message => message, + :created_at => created_at + ) + end + + context "without Appsignal.config" do + it "logs a warning" do + logs = + capture_logs do + expect(create_marker( + :icon => "🎉", + :message => "Migration completed", + :created_at => Time.now + )).to be(false) + end + expect(logs) + .to contains_log(:warn, "Did not transmit custom marker: no AppSignal config loaded") + end + end + + context "with Appsignal.config" do + before { configure } + + context "when request is valid" do + it "sends request with all parameters and logs success" do + time = "2025-02-21T11:03:48+01:00" + stub_public_endpoint_markers_request( + :marker_data => { + "icon" => "🎉", + "message" => "Migration completed", + "created_at" => time + } + ).to_return(:status => 200) + + logs = + capture_logs do + expect(create_marker( + :icon => "🎉", + :message => "Migration completed", + :created_at => time + )).to be(true) + end + + expect(logs).to contains_log(:info, "Transmitted custom marker") + expect(logs).to_not contains_log(:error, "Failed to transmit custom marker") + end + + it "sends request with time object as parameter and logs success" do + time = Time.now.utc + stub_public_endpoint_markers_request( + :marker_data => { + "icon" => "🎉", + "message" => "Migration completed", + "created_at" => time.iso8601 + } + ).to_return(:status => 200) + + logs = + capture_logs do + expect(create_marker( + :icon => "🎉", + :message => "Migration completed", + :created_at => time + )).to be(true) + end + + expect(logs).to contains_log(:info, "Transmitted custom marker") + expect(logs).to_not contains_log(:error, "Failed to transmit custom marker") + end + + it "sends request with some parameters and logs success" do + stub_public_endpoint_markers_request( + :marker_data => { + "message" => "Migration completed" + } + ).to_return(:status => 200) + + logs = + capture_logs do + expect(create_marker(:message => "Migration completed")).to be(true) + end + + expect(logs).to contains_log(:info, "Transmitted custom marker") + expect(logs).to_not contains_log(:error, "Failed to transmit custom marker") + end + end + + context "when request failed" do + it "logs error" do + time = Time.now.utc + stub_public_endpoint_markers_request( + :marker_data => { + "icon" => "🎉", + "message" => "Migration completed", + "created_at" => time.iso8601 + } + ).to_return(:status => 500) + + logs = + capture_logs do + expect(create_marker( + :icon => "🎉", + :message => "Migration completed", + :created_at => time + )).to be(false) + end + + expect(logs).to_not contains_log(:info, "Transmitted custom marker") + expect(logs).to contains_log(:error, "Failed to transmit custom marker: 500 status code") + end + end + + context "when request raised an error" do + it "logs error" do + time = Time.now.utc + stub_public_endpoint_markers_request( + :marker_data => { + "icon" => "🎉", + "message" => "Migration completed", + "created_at" => time.iso8601 + } + ).to_raise(RuntimeError.new("uh oh")) + + logs = + capture_logs do + expect(create_marker( + :icon => "🎉", + :message => "Migration completed", + :created_at => time + )).to be(false) + end + + expect(logs).to_not contains_log(:info, "Transmitted custom marker") + expect(logs) + .to contains_log(:error, "Failed to transmit custom marker: RuntimeError: uh oh") + end + end + end + end +end diff --git a/spec/support/helpers/api_request_helper.rb b/spec/support/helpers/api_request_helper.rb index 192a2495..9250a30e 100644 --- a/spec/support/helpers/api_request_helper.rb +++ b/spec/support/helpers/api_request_helper.rb @@ -30,6 +30,27 @@ def stub_api_request(config, path, body = nil) stub_request(:post, "#{endpoint}/1/#{path}").with(options) end + def stub_public_endpoint_markers_request(marker_data:) + config = Appsignal.config + options = { + :query => { + :api_key => config[:push_api_key], + :name => config[:name], + :environment => config.respond_to?(:env) ? config.env : config[:environment], + :hostname => config[:hostname], + :gem_version => Appsignal::VERSION + }, + :headers => { "Content-Type" => "application/json; charset=UTF-8" } + } + stub_request( + :post, + "#{config[:logging_endpoint]}/markers" + ).with(options) do |request| + payload = JSON.parse(request.body) + expect(payload).to match(marker_data) + end + end + def stub_cron_check_in_request(events:, response: { :status => 200 }) stub_check_in_requests( :requests => [events],