From b518d840b9f19a5a8e200f6453d8e88a84397a15 Mon Sep 17 00:00:00 2001 From: jwaldermo Date: Wed, 10 Dec 2025 09:51:39 -0600 Subject: [PATCH] Add Badge class for fetching status badges from Cronitor API Add support for fetching status badges via the Cronitor Badges API. Badges are indexed by tag and can be used to display monitor health status in dashboards and other interfaces. New features: - Cronitor::Badge.all - Fetch all badges, returns Hash - Badge#key - Extract badge key from SVG URL for building embed URLs - Supports both standard and detailed badge URL formats Includes comprehensive test coverage and README documentation. Bumps version to 5.4.0. --- README.md | 37 +++++++++ lib/cronitor.rb | 1 + lib/cronitor/badge.rb | 81 ++++++++++++++++++ lib/cronitor/version.rb | 2 +- spec/cronitor_spec.rb | 177 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 lib/cronitor/badge.rb 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|