Conversant Ruby Gem

A Ruby client library for interacting with Conversant/SwiftFederation CDN services, providing clean interfaces for Portal, CDN, and LMS APIs with V3 authentication support.

Features

  • Zero Configuration - Automatically uses existing CONVERSANT environment variables
  • V3 SSO Authentication - Handles complex authentication flow with session management
  • Service-Oriented Architecture - Separate classes for Portal, CDN, LMS, VMS, and OSS services
  • Clean Service Structure - Each service organized with focused sub-services (analytics, domain, dashboard, etc.)
  • 79 API Methods - Complete coverage across 4 main services (CDN: 46, LMS: 19, VMS: 9, Portal: 5)
  • Comprehensive CDN Management - Full suite of CDN operations including domains, certificates, analytics
  • Inheritance-Friendly - Easy to extend with your custom business logic
  • Redis Session Caching - Automatic session management with configurable TTL
  • Thread-Safe - Proper cookie jar management for concurrent requests
  • Drop-in Replacement - Works with existing Utils::Conversant::V3::Private classes
  • YARD Documentation - Complete API documentation with examples
  • RBS Type Signatures - Full type coverage for better IDE support

Installation

Add this line to your application's Gemfile:

gem 'conversant'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install conversant

Quick Start

Zero Configuration (If ENV Variables Already Set)

If you're already using the CONVERSANT system, the gem works immediately:

require 'conversant'

# No configuration needed! The gem automatically uses:
# - ENV['PRIVATE_CDN_ENDPOINT']
# - ENV['PRIVATE_LMS_ENDPOINT']
# - ENV['SWIFTSERVE_IDENTIFIER_ID']
# - ENV['SWIFTSERVE_IDENTIFIER_HASH']
# - All other CONVERSANT ENV variables

# Just start using it:
portal = Conversant::V3.portal(customer_id)
cdn = Conversant::V3.cdn(customer_id)
lms = Conversant::V3.lms(customer_id)
vms = Conversant::V3.vms(customer_id)
oss = Conversant::V3.oss(customer_id)

Configuration

Environment Variables

The gem automatically uses these CONVERSANT environment variables:

Variable Description Default Required
PORTAL_ROOT_HOSTNAME Portal hostname console.swiftfederation.com No
PORTAL_SSO_HOSTNAME SSO hostname sso.swiftfederation.com No
PRIVATE_PORTAL_ENDPOINT Portal API endpoint https://{PORTAL_ROOT_HOSTNAME} No
PRIVATE_SSO_ENDPOINT SSO endpoint https://{PORTAL_SSO_HOSTNAME} No
PRIVATE_CDN_ENDPOINT CDN API endpoint - Yes
PRIVATE_LMS_ENDPOINT LMS API endpoint - Yes
PRIVATE_VMS_ENDPOINT VMS API endpoint - No
PRIVATE_OSS_ENDPOINT OSS API endpoint - No
CONVERSANT_CDN_API_URL CDN API URL for http_code - No
SWIFTSERVE_IDENTIFIER_ID Authentication ID - Yes
SWIFTSERVE_IDENTIFIER_HASH Authentication hash - Yes
DEFAULT_UA User agent string Mozilla/5.0... No
DEFAULT_CONTENT_TYPE Content type header application/json No
CONVERSANT_DEBUG Enable debug logging false No
CONVERSANT_CACHE_TTL Session cache TTL (seconds) 1200 No
REDIS_URL Redis connection URL - No*
RAILS_ENV Rails environment (affects SSL) - No

*Redis is auto-detected from $redis global or ENV['REDIS_URL'].

Auto-Configuration (With Validation)

To ensure all required variables are present:

# In config/initializers/conversant.rb (Rails)
require 'conversant'

# Validate that all required ENV variables are set
Conversant.auto_configure!

Manual Configuration

Override specific settings if needed:

Conversant.configure do |config|
  # Override only what you need
  config.cache_ttl = 3600 # 1 hour instead of default 20 minutes
  config.logger = Rails.logger
  config.debug_mode = true
end

Usage

Basic Usage

require 'conversant'

# Initialize services
customer_id = 12345
cdn = Conversant::V3.cdn(customer_id)
lms = Conversant::V3.lms(customer_id)
vms = Conversant::V3.vms(customer_id)

CDN Domain Management

# List all domains
domains = cdn.domain.all
domains.each do |domain|
  puts "Domain: #{domain.name} (ID: #{domain.id})"
  puts "  Type: #{domain.businessScenarioType == 1 ? 'CDN' : 'Storage'}"
  puts "  Status: #{domain.status == 1 ? 'Active' : 'Inactive'}"
  puts "  Usage: #{domain.current_disk_usage / 1_073_741_824.0} GB"
end

# Find specific domain
domain = cdn.domain.find_by(name: 'cdn.example.com')
if domain
  puts "Found domain: #{domain.name} with ID: #{domain.id}"
end

# Create new CDN domain
domain_id = cdn.domain.create({
  businessScenarioType: 1,
  name: "cdn.example.com",
  origins: [{ url_prefix: "203.0.113.10" }],
  redirect_http_to_https: true,
  http2_enabled: true
})
puts "Created domain with ID: #{domain_id}" if domain_id

CDN Analytics & Reporting

# Get bandwidth usage for the current month
require 'date'
start_time = Date.today.beginning_of_month.strftime("%Y-%m-%dT00:00:00Z")
end_time = Date.today.end_of_month.strftime("%Y-%m-%dT23:59:59Z")

bandwidth = cdn.analytics.bandwidths({
  startTime: start_time,
  endTime: end_time,
  pageSize: 1000
})

total_bandwidth = bandwidth.sum { |b| b['bandwidth'] || 0 }
puts "Total bandwidth this month: #{total_bandwidth / 1_099_511_627_776.0} TB"

# Get 95th percentile for billing
bandwidth_95th = cdn.business.bandwidth95th(year: 2025)
annual_95th = bandwidth_95th.map { |m| m['bandwidth_95th'] }.max
puts "Annual 95th percentile: #{annual_95th / 1_000_000.0} Mbps"

# Top URLs analysis
top_urls = cdn.analytics.top_urls({
  startTime: start_time,
  endTime: end_time,
  limit: 10
})

puts "Top 10 URLs:"
top_urls.each_with_index do |url, i|
  puts "  #{i+1}. #{url['url']} - #{url['hits']} hits"
end

Certificate Management

# List all certificates
certificates = cdn.certificate.all
certificates.each do |cert|
  puts "Certificate: #{cert['name']}"
  puts "  Status: #{cert['status']}"
  puts "  Expires: #{cert['expiry_date']}"
  puts "  Domains: #{cert['domains']&.join(', ')}"
end

# Deploy Let's Encrypt certificate
success = cdn.certificate.deploy_auto_certificate(12345, "example.com", {
  enable: true,
  domains: ["example.com", "www.example.com"],
  validation_method: "http",
  auto_renewal: true
})

if success
  puts "Auto-certificate deployed successfully"
else
  puts "Failed to deploy auto-certificate"
end

Monitoring & Alerts

# Monitor domain health
domain_list = cdn.monitoring.delivery_domain_list
domain_list.each do |domain|
  if domain['status'] != 'healthy'
    puts "Alert: Domain #{domain['name']} status is #{domain['status']}"
  end
end

# Get detailed monitoring for specific domain
domain_detail = cdn.monitoring.delivery_domain_detail(domain_id)
if domain_detail
  puts "Domain: #{domain_detail['name']}"
  puts "  Response Time: #{domain_detail['response_time']}ms"
  puts "  Cache Hit Rate: #{domain_detail['cache_hit_rate']}%"
  puts "  Error Rate: #{domain_detail['error_rate']}%"
end

Audit Logging

# Get audit logs for the last 24 hours
yesterday = (Time.now - 86400).strftime("%Y-%m-%dT%H:%M:%SZ")
now = Time.now.strftime("%Y-%m-%dT%H:%M:%SZ")

audit_logs = cdn.audit.delivery_domain_operations({
  startTime: yesterday,
  endTime: now,
  pageSize: 100
})

audit_logs.each do |log|
  puts "[#{log['timestamp']}] #{log['action']} by #{log['user']}"
  puts "  Domain: #{log['domain_name']}"
  puts "  Details: #{log['details']}"
end

Inheritance Pattern (Extend with Your Logic)

Create your own classes that inherit from the gem's services:

module Utils
  module Conversant
    module V3
      class CDN < ::Conversant::V3::Services::CDN
        # Add your custom methods
        def daily_summary(date = Date.today)
          start_time = date.strftime("%Y-%m-%dT00:00:00Z")
          end_time = date.strftime("%Y-%m-%dT23:59:59Z")

          {
            bandwidth: analytics.bandwidths({startTime: start_time, endTime: end_time}),
            volumes: analytics.volumes({startTime: start_time, endTime: end_time}),
            spots: monitoring.spots({startTime: start_time, endTime: end_time})
          }
        end
      end
    end
  end
end

# Use your extended class
cdn = Utils::Conversant::V3::CDN.new(customer_id)
summary = cdn.daily_summary  # Your custom method
bandwidth = cdn.analytics.bandwidths(payload)  # Parent's method

Integration with Existing Code

Option 1: Drop-in Replacement for ConversantHttpClientV3

Replace your existing authentication module:

# app/models/concerns/conversant_http_client_v3.rb
require 'conversant'

Conversant.auto_configure! unless Conversant.configured?

module ConversantHttpClientV3
  def self.included(base)
    base.class_eval do
      include ::Conversant::V3::Mixins::Authentication
      use_conversant_auth!
    end
  end

  def authenticate
    conversant_authenticate
  end

  def signature
    sessions = authenticate
    sessions && sessions[:session]
  end
end

Option 2: Update Only Authorization Headers

Keep your existing classes, just update the auth:

class Utils::Conversant::V3::Private::Operation
  def authorized_headers
    # Only change this method - use gem's auth
    ::Conversant::V3.portal(@customer_id).send(:authorized_headers)
  end

  # All your existing methods remain unchanged
  def appliances(gte = nil, lte = nil)
    # Your existing implementation
  end
end

Option 3: Using the Mixin

For classes that can't change inheritance:

class Utils::Conversant::V3::Private::Operation
  include Conversant::V3::Mixins::Authentication
  use_conversant_auth!

  def initialize(customer_id, type = 2)
    @customer_id = customer_id
    @type = type
    initialize_conversant_auth(customer_id, type)
  end

  def your_existing_method
    headers = portal_authorized_headers  # Use gem's auth
    # Your existing logic...
  end
end

API Reference

Portal Service

portal = Conversant::V3.portal(customer_id)

# Get appliances
appliances = portal.appliances(start_date, end_date)
# Returns: Array of appliance hashes with ip, hostname, deleted, pop, volume, federation

CDN Service

The CDN service provides comprehensive domain, certificate, analytics, and monitoring capabilities.

cdn = Conversant::V3.cdn(customer_id)

# Domain Management
domains = cdn.domain.all                           # Get all domains
domain = cdn.domain.find(domain_id)               # Find by ID
domain = cdn.domain.find_by(name: 'example.com')  # Find by name
total_usage = cdn.domain.total_current_disk_usage # Storage usage in bytes

# Create new domain
domain_id = cdn.domain.create({
  businessScenarioType: 1,      # 1=CDN, 2=Storage
  name: "cdn.example.com",
  origins: [{ url_prefix: "203.0.113.10" }],
  redirect_http_to_https: true,
  http2_enabled: true,
  streaming_service: false,
  disabled: false
})

# Certificate Management
certificates = cdn.certificate.all                    # List all certificates
cert_info = cdn.certificate.auto_certificate_info(cert_id, domain_name)

# Deploy Let's Encrypt certificate
success = cdn.certificate.deploy_auto_certificate(cert_id, domain_name, {
  enable: true,
  domains: ["example.com", "www.example.com"],
  validation_method: "http",
  auto_renewal: true
})

# Analytics
bandwidth = cdn.analytics.bandwidths({
  startTime: "2025-01-01T00:00:00Z",
  endTime: "2025-01-31T23:59:59Z",
  pageSize: 1000,
  pageNumber: 0
})

hits = cdn.analytics.hits({
  startTime: start_time,
  endTime: end_time,
  groupBy: "hour"
})

status_codes = cdn.analytics.status_codes({
  startTime: start_time,
  endTime: end_time,
  statusCode: "200,404,500"
})

top_urls = cdn.analytics.top_urls({
  startTime: start_time,
  endTime: end_time,
  limit: 100
})

# Business Metrics (Billing)
usage = cdn.business.bandwidth({
  startTime: "2025-01-01T00:00:00Z",
  endTime: "2025-01-31T23:59:59Z",
  pageSize: 1000
})

# Get 95th percentile bandwidth
bandwidth_95th = cdn.business.bandwidth95th(year: 2025)

# Monitoring
domain_list = cdn.monitoring.delivery_domain_list
domain_detail = cdn.monitoring.delivery_domain_detail(domain_id)
customer_details = cdn.monitoring.customer_details

# Audit Logs
audit_logs = cdn.audit.delivery_domain_operations({
  startTime: start_time,
  endTime: end_time,
  pageSize: 50
})

LMS Service (Live Media Streaming)

lms = Conversant::V3.lms(customer_id)

# Job Management - Query streaming jobs
jobs = lms.job.where(
  page_number: 1,
  page_size: 50,
  status: 'active'
)

jobs.each do |job|
  puts "Job #{job['id']}: #{job['name']}"
  puts "  Status: #{job['status']}"
  puts "  Created: #{job['created_at']}"
end

# Domain Management
domains = lms.domain.all(page_number: 1, page_size: 50)
domains.each do |domain|
  puts "Domain: #{domain['name']} (ID: #{domain['id']})"
end

# Get specific domain details
domain = lms.domain.show(domain_id)

# Create streaming domain
domain_id = lms.domain.create({
  name: "stream.example.com",
  type: "live",
  protocol: "rtmp"
})

# Update streaming domain
lms.domain.update({
  id: domain_id,
  name: "updated-stream.example.com"
})

# Delete streaming domain
lms.domain.delete(domain_id)

# Dashboard Metrics
live_duration = lms.dashboard.live
vod_duration = lms.dashboard.vod
dvr_duration = lms.dashboard.dvr
recent_jobs = lms.dashboard.recent_jobs

# Transcoding Presets
presets = lms.preset.all
presets.each do |preset|
  puts "Preset: #{preset['name']} - #{preset['resolution']}"
end

VMS Service (Video Management System)

vms = Conversant::V3.vms(customer_id)

# Transcoding Jobs and Presets
jobs = vms.transcoding.jobs(page_size: 50, job_status: 2)
jobs.each do |job|
  puts "Job #{job['id']}: #{job['file_name']} - Status: #{job['status']}"
end

# Get available transcoding presets
presets = vms.transcoding.presets(page_size: 50)
presets.each do |preset|
  puts "Preset: #{preset['name']} - #{preset['description']}"
end

# Analytics - Transcoding duration and volume
duration = vms.analytics.transcoding({
  startTime: "2025-01-01",
  endTime: "2025-01-31"
})

volume = vms.analytics.volume({
  startTime: "2025-01-01",
  endTime: "2025-01-31"
})

# Business Metrics - Detailed transcoding breakdown
transcoding_breakdown = vms.business.transcoding({
  startTime: Time.now.beginning_of_month
})
puts "SD: #{transcoding_breakdown[:vms_transcoding_sd]} minutes"
puts "HD: #{transcoding_breakdown[:vms_transcoding_hd]} minutes"
puts "UHD: #{transcoding_breakdown[:vms_transcoding_uhd]} minutes"
puts "Total: #{transcoding_breakdown[:vms_transcoding]} minutes"

# Duration and volume for billing
duration_metrics = vms.business.duration({
  startTime: Time.now.beginning_of_month
})

volume_metrics = vms.business.volume({
  startTime: Time.now.beginning_of_month
})

OSS Service (Object Storage Service)

oss = Conversant::V3.oss(customer_id)

# Bucket operations
buckets = oss.list_buckets
bucket = oss.get_bucket(bucket_name)
oss.create_bucket(bucket_name, region: 'us-east-1')

# Object operations
objects = oss.list_objects(bucket_name, prefix: 'videos/')
object = oss.get_object(bucket_name, object_key)
oss.put_object(bucket_name, object_key, file_content)
oss.delete_object(bucket_name, object_key)

# Presigned URLs
url = oss.presigned_url(bucket_name, object_key, expires_in: 3600)

Error Handling

begin
  portal = Conversant::V3.portal(customer_id)
  appliances = portal.appliances
rescue Conversant::AuthenticationError => e
  # Handle authentication failures
  puts "Authentication failed: #{e.message}"
rescue Conversant::ApiError => e
  # Handle API errors
  puts "API error: #{e.message}"
rescue Conversant::Error => e
  # Handle general errors
  puts "Error: #{e.message}"
end

Migration Guide

From Existing Utils::Conversant::V3::Private

Phase 1: Authentication Only

  1. Add gem 'conversant' to Gemfile
  2. Replace ConversantHttpClientV3 module with gem's wrapper
  3. Keep all business logic unchanged

Phase 2: Gradual Migration (Optional)

  • Migrate services one by one
  • Start with least critical service
  • Keep custom business logic where needed

Phase 3: Full Integration (Optional)

  • Use inheritance pattern for all services
  • Add custom methods as needed

Rollback Plan

If you need to rollback:

  1. Restore original ConversantHttpClientV3 module
  2. Remove gem from Gemfile
  3. No changes needed to business logic

Authentication Architecture

The gem implements a multi-tier authentication system with automatic session management using a consistent Authorization module pattern across all services.

Authentication Flow

  1. Root Authentication:

    • Logs into SSO with master credentials (SWIFTSERVE_IDENTIFIER_ID/HASH)
    • Obtains SESSION and SSO_GW_SESSION2 cookies
    • Cached in Redis with TTL management
  2. Service-Specific Sessions: All services follow the same authentication pattern via the Authorization module:

    • Portal: Exchanges root sessions for customer-specific SESSION cookie
    • CDN: Exchanges root sessions for customer-specific SESSION cookie
    • LMS: Exchanges root sessions for Java-based JSESSIONID cookie
    • VMS: Exchanges root sessions for Java-based JSESSIONID cookie
    • OSS: Uses root sessions with S3-compatible authentication
  3. Authorization Module Pattern: Each service implements the Authorization module which provides:

    • Automatic session fetching and caching
    • Session validation before API calls
    • Consistent error handling
    • Redis-based session storage

Session Caching

All sessions are cached in Redis with automatic refresh using a standardized key format:

# Sessions are cached with these keys:
"CONVERSANT.V3.PORTAL.SESSION.{customer_id}"     # Portal SESSION cookie
"CONVERSANT.V3.PORTAL.SSO_GW_SESSION2.{customer_id}" # SSO session (shared)
"CONVERSANT.V3.CDN.SESSION.{customer_id}"        # CDN SESSION cookie
"CONVERSANT.V3.LMS.JSESSIONID.{customer_id}"     # LMS JSESSIONID cookie
"CONVERSANT.V3.VMS.JSESSIONID.{customer_id}"     # VMS JSESSIONID cookie

# Default TTL: 20 minutes (configurable)

Session Validation

All services automatically validate session cookies before making API calls:

# Each service checks for its required cookie before making requests:
# - Portal, CDN: Validates SESSION cookie presence
# - LMS, VMS: Validates JSESSIONID cookie presence
# - If missing, automatically fetches new session
# - If fetch fails, raises AuthenticationError

Using Custom Authentication

If you need access to raw authentication headers:

# Get authorized headers for direct API calls
cdn = Conversant::V3.cdn(customer_id)
headers = cdn.send(:authorized_headers)

# Make custom API call
response = RestClient.get("#{cdn_endpoint}/custom-endpoint", headers)

Development

See DEVELOPER_GUIDE.md for detailed information on:

  • Building and testing the gem
  • Publishing to RubyGems
  • Version management
  • Documentation generation
  • GitLab CI/CD setup
  • Troubleshooting

Quick start:

bundle install        # Install dependencies
bundle exec rspec    # Run tests
bundle exec yard doc # Generate documentation
./publish.sh        # Publish to RubyGems

Debugging

Enable debug mode for verbose logging:

Conversant.configure do |config|
  config.debug_mode = true
  config.logger = Logger.new($stdout)
end

Examples

See the examples/ directory for:

  • inheritance_integration.rb - Complete inheritance patterns
  • rails_initializer.rb - Rails setup example

Troubleshooting

Common Issues

Authentication Errors

# Error: Conversant::AuthenticationError
# Solution: Check environment variables
ENV['SWIFTSERVE_IDENTIFIER_ID']   # Must be set
ENV['SWIFTSERVE_IDENTIFIER_HASH'] # Must be set
ENV['PRIVATE_CDN_ENDPOINT']       # Must be set
ENV['PRIVATE_LMS_ENDPOINT']       # Must be set

# Clear cache if credentials changed
redis.del("CONVERSANT.V3.PORTAL.SESSION.#{customer_id}")
redis.del("CONVERSANT.V3.PORTAL.SSO_GW_SESSION2.#{customer_id}")

Redis Connection Issues

# Error: Redis::CannotConnectError
# Solution: Configure Redis properly
Conversant.configure do |config|
  config.redis = Redis.new(url: ENV['REDIS_URL'])
end

# Or use global $redis
$redis = Redis.new(url: ENV['REDIS_URL'])

Domain Creation Failures

# Common issues:
# 1. Domain name already exists
# 2. Invalid origin configuration
# 3. Missing FTP password for storage domains (businessScenarioType: 2)

# Check response for details:
domain_id = cdn.domain.create(payload)
if domain_id.nil?
  # Check logs for error details
  # Enable debug mode for more info
  Conversant.configure { |c| c.debug_mode = true }
end

Certificate Deployment Issues

# Let's Encrypt validation failures:
# 1. Domain must be publicly accessible
# 2. HTTP validation requires port 80 open
# 3. DNS validation requires DNS control

# Check certificate status:
cert_info = cdn.certificate.auto_certificate_info(cert_id, domain)
puts cert_info['validation_status']  # Should be 'valid'
puts cert_info['error_message']      # If failed

Debug Mode

Enable detailed logging for troubleshooting:

Conversant.configure do |config|
  config.debug_mode = true
  config.logger = Logger.new($stdout)
  config.log_level = :debug
end

# This will log:
# - All API requests and responses
# - Authentication attempts
# - Session cache hits/misses
# - Error details with stack traces

Performance Tips

  1. Use Redis connection pooling: ```ruby require 'connection_pool'

redis_pool = ConnectionPool.new(size: 5) { Redis.new } Conversant.configure do |config| config.redis = redis_pool end


2. **Batch API calls when possible**:
```ruby
# Instead of multiple calls:
domains.each { |d| cdn.domain.find(d.id) }

# Use single call with filtering:
all_domains = cdn.domain.all
my_domains = all_domains.select { |d| domain_ids.include?(d.id) }
  1. Increase cache TTL for stable data: ruby Conversant.configure do |config| config.cache_ttl = 3600 # 1 hour for stable environments end

Version Notes

Latest Release: 1.0.10 (2025-09-30)

Documentation Update: Enhanced documentation for v1.0.9 critical fixes

Version 1.0.9 - Critical Bug Fix

Fixed authentication issues with LMS and VMS services:

  • ✅ Fixed JSESSIONID authentication for LMS service (resolves "Missing JSESSIONID" error)
  • ✅ Fixed JSESSIONID authentication for VMS service
  • ✅ Improved RestClient cookie handling (switched from manual 'Cookie' header to :cookies option)
  • ✅ Fixed method visibility issues in Authorization module

Upgrade recommended if you're using LMS or VMS services. CDN and Portal services are unaffected.

See CHANGELOG.md for complete version history.

Contributing

Bug reports and pull requests are welcome on GitLab at https://gitlab.vnetwork.dev/gems/conversant.

License

The gem is available as open source under the terms of the MIT License.