diff --git a/README.md b/README.md index c76ac5a..a0d1983 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ In this guide: - [Monitoring Background Jobs](#monitoring-background-jobs) - [Sending Telemetry Events](#sending-telemetry-events) - [Configuring Monitors](#configuring-monitors) +- [Fetching Status Badges](#fetching-status-badges) - [Package Configuration & Env Vars](#package-configuration) - [Contributing](#contributing) @@ -203,6 +204,42 @@ monitor.ok # manually reset to a passing state alias for monitor.ping({state: ok monitor.delete # destroy the monitor ``` +## Fetching Status Badges + +[Status badges](https://cronitor.io/docs/status-badges) provide a visual indicator of your monitor's health. Badges are indexed by tag, so to get a badge for a specific monitor, you must first assign it a unique tag. + +```ruby +require 'cronitor' +Cronitor.api_key = 'api_key_123' + +# Fetch all badges +badges = Cronitor::Badge.all + +# Each badge is indexed by its tag +badges.each do |tag, badge| + puts "Tag: #{tag}" + puts "SVG URL: #{badge.svg_url}" + puts "Badge Key: #{badge.key}" +end + +# Access a specific badge by tag +if badge = badges['my-monitor-tag'] + # Extract the badge key for building embed URLs + key = badge.key # e.g., 'abc123xyz' + + # Build your own embed URL + # Standard: https://cronitor.io/badges/ACCOUNT_ID/production/KEY.svg + # Detailed: https://cronitor.io/badges/ACCOUNT_ID/production/KEY/detailed.svg +end +``` + +### Badge Attributes + +- `tag` - The tag name used to index this badge +- `svg_url` - Direct URL to the badge SVG image +- `url` - Alternative URL for the badge +- `key` - The unique badge key extracted from the SVG URL (useful for constructing embed URLs) + ## Package Configuration The package needs to be configured with your account's `API key`, which is available on the [account settings](https://cronitor.io/settings) page. You can also optionally specify an `api_version` and an `environment`. If not provided, your account default is used. These can also be supplied using the environment variables `CRONITOR_API_KEY`, `CRONITOR_API_VERSION`, `CRONITOR_ENVIRONMENT`. diff --git a/lib/cronitor.rb b/lib/cronitor.rb index d0d622a..c511550 100644 --- a/lib/cronitor.rb +++ b/lib/cronitor.rb @@ -11,6 +11,7 @@ require 'cronitor/error' require 'cronitor/version' require 'cronitor/monitor' +require 'cronitor/badge' module Cronitor def self.read_config(path = nil) diff --git a/lib/cronitor/badge.rb b/lib/cronitor/badge.rb new file mode 100644 index 0000000..05220fa --- /dev/null +++ b/lib/cronitor/badge.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Cronitor + class Badge + BADGE_API_URL = 'https://cronitor.io/api/badges' + + # Fetch all badges from the Cronitor API + # Returns a hash where keys are tag names and values are Badge objects + # + # @param api_key [String] Optional API key (defaults to Cronitor.api_key) + # @param api_version [String] Optional API version header + # @return [Hash] Hash of tag => Badge objects + # @raise [Cronitor::Error] If API key is missing or API returns an error + # + # @example + # Cronitor.api_key = 'your-api-key' + # badges = Cronitor::Badge.all + # badges.each do |tag, badge| + # puts "#{tag}: #{badge.svg_url}" + # puts "Badge key: #{badge.key}" + # end + def self.all(api_key: nil, api_version: nil) + api_key ||= Cronitor.api_key + + unless api_key + raise Error.new('No API key detected. Set Cronitor.api_key or pass api_key parameter') + end + + headers = Monitor::Headers::JSON.dup + headers[:'Cronitor-Version'] = api_version if api_version + + resp = HTTParty.get( + BADGE_API_URL, + basic_auth: { + username: api_key, + password: '' + }, + headers: headers, + timeout: Cronitor.timeout || 10 + ) + + case resp.code + when 200 + data = JSON.parse(resp.body) + badges = {} + data.each do |tag, badge_data| + badges[tag] = Badge.new( + tag: tag, + svg_url: badge_data['svg'], + url: badge_data['url'] + ) + end + badges + else + raise Error.new("Error fetching badges: #{resp.code} - #{resp.body}") + end + end + + attr_reader :tag, :svg_url, :url + + def initialize(tag:, svg_url: nil, url: nil) + @tag = tag + @svg_url = svg_url || url + @url = url || svg_url + end + + # Extract the badge key from the SVG URL + # Badge URLs follow these patterns: + # Standard: https://cronitor.io/badges/ACCOUNT/production/KEY.svg + # Detailed: https://cronitor.io/badges/ACCOUNT/production/KEY/detailed.svg + # + # @return [String, nil] The badge key or nil if URL doesn't match expected pattern + def key + return nil unless svg_url + + # Match the key segment after /production/ - stops at next / or . + match = svg_url.match(%r{/production/([^/.]+)}) + match&.[](1) + end + end +end diff --git a/lib/cronitor/version.rb b/lib/cronitor/version.rb index b565d6a..013f070 100644 --- a/lib/cronitor/version.rb +++ b/lib/cronitor/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Cronitor - VERSION = '5.3.0' + VERSION = '5.4.0' end diff --git a/spec/cronitor_spec.rb b/spec/cronitor_spec.rb index 8e2c81b..71a96b7 100644 --- a/spec/cronitor_spec.rb +++ b/spec/cronitor_spec.rb @@ -377,6 +377,183 @@ end end + describe 'Badge' do + before(:all) do + Cronitor.configure do |cronitor| + cronitor.api_key = FAKE_API_KEY + end + end + + let(:sample_badge_response) do + { + 'production' => { + 'svg' => 'https://cronitor.io/badges/abc123/production/badge-key-1.svg', + 'url' => 'https://cronitor.io/badges/abc123/production/badge-key-1' + }, + 'loyalty-42' => { + 'svg' => 'https://cronitor.io/badges/abc123/production/loyalty-badge-key.svg', + 'url' => 'https://cronitor.io/badges/abc123/production/loyalty-badge-key' + }, + 'qma-99' => { + 'svg' => 'https://cronitor.io/badges/abc123/production/qma-badge-key.svg', + 'url' => 'https://cronitor.io/badges/abc123/production/qma-badge-key' + } + } + end + + context '.all' do + it 'fetches all badges from the API' do + expect(HTTParty).to receive(:get).with( + 'https://cronitor.io/api/badges', + hash_including( + basic_auth: { username: FAKE_API_KEY, password: '' }, + headers: hash_including('Content-Type': 'application/json') + ) + ).and_return(instance_double(HTTParty::Response, code: 200, body: sample_badge_response.to_json)) + + badges = Cronitor::Badge.all + expect(badges).to be_a(Hash) + expect(badges.keys).to contain_exactly('production', 'loyalty-42', 'qma-99') + end + + it 'returns Badge objects with correct attributes' do + expect(HTTParty).to receive(:get).and_return( + instance_double(HTTParty::Response, code: 200, body: sample_badge_response.to_json) + ) + + badges = Cronitor::Badge.all + badge = badges['production'] + + expect(badge).to be_a(Cronitor::Badge) + expect(badge.tag).to eq('production') + expect(badge.svg_url).to eq('https://cronitor.io/badges/abc123/production/badge-key-1.svg') + expect(badge.url).to eq('https://cronitor.io/badges/abc123/production/badge-key-1') + end + + it 'uses custom api_key when provided' do + custom_key = 'custom_api_key' + expect(HTTParty).to receive(:get).with( + 'https://cronitor.io/api/badges', + hash_including(basic_auth: { username: custom_key, password: '' }) + ).and_return(instance_double(HTTParty::Response, code: 200, body: sample_badge_response.to_json)) + + Cronitor::Badge.all(api_key: custom_key) + end + + it 'uses custom api_version when provided' do + api_version = '2024-01-01' + expect(HTTParty).to receive(:get).with( + 'https://cronitor.io/api/badges', + hash_including(headers: hash_including('Cronitor-Version': api_version)) + ).and_return(instance_double(HTTParty::Response, code: 200, body: sample_badge_response.to_json)) + + Cronitor::Badge.all(api_version: api_version) + end + + context 'when no API key is available' do + it 'raises an error' do + original_api_key = Cronitor.api_key + Cronitor.api_key = nil + + expect { Cronitor::Badge.all }.to raise_error(Cronitor::Error, /No API key detected/) + + Cronitor.api_key = original_api_key + end + end + + context 'when API returns an error' do + it 'raises an error with response details' do + expect(HTTParty).to receive(:get).and_return( + instance_double(HTTParty::Response, code: 401, body: 'Unauthorized') + ) + + expect { Cronitor::Badge.all }.to raise_error(Cronitor::Error, /Error fetching badges: 401/) + end + end + + context 'when API returns empty response' do + it 'returns an empty hash' do + expect(HTTParty).to receive(:get).and_return( + instance_double(HTTParty::Response, code: 200, body: {}.to_json) + ) + + badges = Cronitor::Badge.all + expect(badges).to eq({}) + end + end + end + + context '#initialize' do + it 'sets tag, svg_url, and url' do + badge = Cronitor::Badge.new( + tag: 'test-tag', + svg_url: 'https://example.com/badge.svg', + url: 'https://example.com/badge' + ) + + expect(badge.tag).to eq('test-tag') + expect(badge.svg_url).to eq('https://example.com/badge.svg') + expect(badge.url).to eq('https://example.com/badge') + end + + it 'uses url as fallback for svg_url when svg_url is nil' do + badge = Cronitor::Badge.new(tag: 'test', url: 'https://example.com/badge') + + expect(badge.svg_url).to eq('https://example.com/badge') + end + + it 'uses svg_url as fallback for url when url is nil' do + badge = Cronitor::Badge.new(tag: 'test', svg_url: 'https://example.com/badge.svg') + + expect(badge.url).to eq('https://example.com/badge.svg') + end + end + + context '#key' do + it 'extracts badge key from standard svg_url' do + badge = Cronitor::Badge.new( + tag: 'test', + svg_url: 'https://cronitor.io/badges/abc123/production/my-badge-key.svg' + ) + + expect(badge.key).to eq('my-badge-key') + end + + it 'extracts badge key from detailed badge svg_url' do + badge = Cronitor::Badge.new( + tag: 'test', + svg_url: 'https://cronitor.io/badges/abc123/production/my-badge-key/detailed.svg' + ) + + expect(badge.key).to eq('my-badge-key') + end + + it 'returns nil when svg_url is nil' do + badge = Cronitor::Badge.new(tag: 'test') + + expect(badge.key).to be_nil + end + + it 'returns nil when svg_url does not match expected pattern' do + badge = Cronitor::Badge.new( + tag: 'test', + svg_url: 'https://example.com/some-other-url.svg' + ) + + expect(badge.key).to be_nil + end + + it 'handles badge keys with hyphens and underscores' do + badge = Cronitor::Badge.new( + tag: 'test', + svg_url: 'https://cronitor.io/badges/abc/production/my_badge-key_123.svg' + ) + + expect(badge.key).to eq('my_badge-key_123') + end + end + end + describe 'functional tests - ', type: 'functional' do before(:all) do Cronitor.configure do |cronitor|