Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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`.
Expand Down
1 change: 1 addition & 0 deletions lib/cronitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
require 'cronitor/error'
require 'cronitor/version'
require 'cronitor/monitor'
require 'cronitor/badge'

module Cronitor
def self.read_config(path = nil)
Expand Down
81 changes: 81 additions & 0 deletions lib/cronitor/badge.rb
Original file line number Diff line number Diff line change
@@ -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<String, Badge>] 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
2 changes: 1 addition & 1 deletion lib/cronitor/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Cronitor
VERSION = '5.3.0'
VERSION = '5.4.0'
end
177 changes: 177 additions & 0 deletions spec/cronitor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down