Cloudflare Analytics gives you data, but the default dashboard is limited. You can't combine metrics from different time periods, create custom visualizations, or correlate traffic with business events. You're stuck with predefined charts and can't build the specific insights you need. This limitation prevents you from truly understanding your audience and making data-driven decisions. The solution is building custom dashboards using Cloudflare's API and Ruby's rich visualization ecosystem.

In This Article

Designing a Custom Dashboard Architecture

Building effective dashboards requires thoughtful architecture. Your dashboard should serve different stakeholders: content creators need traffic insights, developers need performance metrics, and business owners need conversion data. Each needs different visualizations and data granularity.

The architecture has three layers: data collection (Cloudflare API + Ruby scripts), data processing (ETL pipelines in Ruby), and visualization (web interface or static reports). Data flows from Cloudflare to your processing scripts, which transform and aggregate it, then to visualization components that present it. This separation allows you to change visualizations without affecting data collection, and to add new data sources easily.

Dashboard Component Architecture

Component Technology Purpose Update Frequency
Data Collection Cloudflare API + ruby-cloudflare gem Fetch raw metrics from Cloudflare Real-time to hourly
Data Storage SQLite/Redis + sequel gem Store historical data for trends On collection
Data Processing Ruby scripts + daru gem Calculate derived metrics, aggregates On demand or scheduled
Visualization Chartkick + sinatra/rails Render charts and graphs On page load
Presentation HTML/CSS + bootstrap User interface and layout Static

Extracting Data from Cloudflare API

Cloudflare's GraphQL Analytics API provides comprehensive data. Use the `cloudflare` gem:

gem 'cloudflare'

# Configure client
cf = Cloudflare.connect(
  email: ENV['CLOUDFLARE_EMAIL'],
  key: ENV['CLOUDFLARE_API_KEY']
)

# Fetch zone analytics
def fetch_zone_analytics(start_time, end_time, metrics, dimensions = [])
  query = {
    query: "
      query {
        viewer {
          zones(filter: {zoneTag: \"#{ENV['CLOUDFLARE_ZONE_ID']}\"}) {
            httpRequests1mGroups(
              limit: 10000,
              filter: {
                datetime_geq: \"#{start_time}\",
                datetime_leq: \"#{end_time}\"
              },
              orderBy: [datetime_ASC],
              #{dimensions.any? ? "dimensions: #{dimensions}," : ""}
            ) {
              dimensions {
                #{dimensions.join("\n")}
              }
              sum {
                #{metrics.join("\n")}
              }
              dimensions {
                datetime
              }
            }
          }
        }
      }
    "
  }
  
  cf.graphql.post(query)
end

# Common metrics and dimensions
METRICS = [
  'visits',
  'pageViews',
  'requests',
  'bytes',
  'cachedBytes',
  'cachedRequests',
  'threats',
  'countryMap { bytes, requests, clientCountryName }'
]

DIMENSIONS = [
  'clientCountryName',
  'clientRequestPath',
  'clientDeviceType',
  'clientBrowserName',
  'originResponseStatus'
]

Create a data collector service:

# lib/data_collector.rb
class DataCollector
  def self.collect_hourly_metrics
    end_time = Time.now.utc
    start_time = end_time - 3600
    
    data = fetch_zone_analytics(
      start_time.iso8601,
      end_time.iso8601,
      METRICS,
      ['clientCountryName', 'clientRequestPath']
    )
    
    # Store in database
    store_in_database(data, 'hourly_metrics')
    
    # Calculate aggregates
    calculate_aggregates(data)
  end
  
  def self.store_in_database(data, table)
    DB[table].insert(
      collected_at: Time.now,
      data: Sequel.pg_json(data),
      period_start: start_time,
      period_end: end_time
    )
  end
  
  def self.calculate_aggregates(data)
    # Calculate traffic by country
    by_country = data.group_by { |d| d['dimensions']['clientCountryName'] }
    
    # Calculate top pages
    by_page = data.group_by { |d| d['dimensions']['clientRequestPath'] }
    
    # Store aggregates
    DB[:aggregates].insert(
      calculated_at: Time.now,
      top_countries: Sequel.pg_json(top_10(by_country)),
      top_pages: Sequel.pg_json(top_10(by_page)),
      total_visits: data.sum { |d| d['sum']['visits'] }
    )
  end
end

# Run every hour
DataCollector.collect_hourly_metrics

Ruby Gems for Data Visualization

Choose gems based on your needs:

1. chartkick - Easy Charts

gem 'chartkick'

# Simple usage
<%= line_chart Visit.group_by_day(:created_at).count %>
<%= pie_chart Visit.group(:country).count %>
<%= column_chart PageView.group(:browser).count %>

# With Cloudflare data
def traffic_over_time_chart
  data = DB[:hourly_metrics].select(
    Sequel.lit("DATE_TRUNC('hour', period_start) as hour"),
    Sequel.lit("SUM((data->>'visits')::int) as visits")
  ).group(:hour).order(:hour).last(48)
  
  line_chart data.map { |r| [r[:hour], r[:visits]] }
end

2. gruff - Server-side Image Charts

gem 'gruff'

# Create charts as images
def create_traffic_chart_image
  g = Gruff::Line.new
  g.title = 'Traffic Last 7 Days'
  
  # Add data
  g.data('Visits', visits_last_7_days)
  g.data('Pageviews', pageviews_last_7_days)
  
  # Customize
  g.labels = date_labels_for_last_7_days
  g.theme = {
    colors: ['#ff9900', '#3366cc'],
    marker_color: '#aaa',
    font_color: 'black',
    background_colors: 'white'
  }
  
  # Write to file
  g.write('public/images/traffic_chart.png')
end

3. daru - Data Analysis and Visualization

gem 'daru'
gem 'daru-view'  # For visualization

# Load Cloudflare data into dataframe
df = Daru::DataFrame.from_csv('cloudflare_data.csv')

# Analyze
daily_traffic = df.group_by([:date]).aggregate(visits: :sum, pageviews: :sum)

# Create visualization
Daru::View::Plot.new(
  daily_traffic[:visits],
  type: :line,
  title: 'Daily Traffic'
).show

4. rails-charts - For Rails-like Applications

gem 'rails-charts'

# Even without Rails
class DashboardController
  def index
    @charts = {
      traffic: RailsCharts::LineChart.new(
        traffic_data,
        title: 'Traffic Trends',
        height: 300
      ),
      sources: RailsCharts::PieChart.new(
        source_data,
        title: 'Traffic Sources'
      )
    }
  end
end

Building Real Time Dashboards

Create dashboards that update in real-time:

Option 1: Sinatra + Server-Sent Events

# app.rb
require 'sinatra'
require 'json'
require 'cloudflare'

get '/dashboard' do
  erb :dashboard
end

get '/stream' do
  content_type 'text/event-stream'
  
  stream do |out|
    loop do
      # Fetch latest data
      data = fetch_realtime_metrics
      
      # Send as SSE
      out   "data: #{data.to_json}\n\n"
      
      sleep 30  # Update every 30 seconds
    end
  end
end

# JavaScript in dashboard
const eventSource = new EventSource('/stream');
eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  updateCharts(data);
};

Option 2: Static Dashboard with Auto-refresh

# Generate static dashboard every minute
namespace :dashboard do
  desc "Generate static dashboard"
  task :generate do
    # Fetch data
    metrics = fetch_all_metrics
    
    # Generate HTML with embedded data
    template = File.read('templates/dashboard.html.erb')
    html = ERB.new(template).result(binding)
    
    # Write to file
    File.write('public/dashboard/index.html', html)
    
    # Also generate JSON for AJAX updates
    File.write('public/dashboard/data.json', metrics.to_json)
  end
end

# Schedule with cron
# */5 * * * * cd /path && rake dashboard:generate

Option 3: WebSocket Dashboard

gem 'faye-websocket'

require 'faye/websocket'

App = lambda do |env|
  if Faye::WebSocket.websocket?(env)
    ws = Faye::WebSocket.new(env)
    
    ws.on :open do |event|
      # Send initial data
      ws.send(initial_dashboard_data.to_json)
      
      # Start update timer
      timer = EM.add_periodic_timer(30) do
        ws.send(update_dashboard_data.to_json)
      end
      
      ws.on :close do |event|
        EM.cancel_timer(timer)
        ws = nil
      end
    end
    
    ws.rack_response
  else
    # Serve static dashboard
    [200, {'Content-Type' => 'text/html'}, [File.read('public/dashboard.html')]]
  end
end

Automated Scheduled Reports

Generate and distribute reports automatically:

# lib/reporting/daily_report.rb
class DailyReport
  def self.generate
    # Fetch data for yesterday
    start_time = Date.yesterday.beginning_of_day
    end_time = Date.yesterday.end_of_day
    
    data = {
      summary: daily_summary(start_time, end_time),
      top_pages: top_pages(start_time, end_time, limit: 10),
      traffic_sources: traffic_sources(start_time, end_time),
      performance: performance_metrics(start_time, end_time),
      anomalies: detect_anomalies(start_time, end_time)
    }
    
    # Generate report in multiple formats
    generate_html_report(data)
    generate_pdf_report(data)
    generate_email_report(data)
    generate_slack_report(data)
    
    # Archive
    archive_report(data, Date.yesterday)
  end
  
  def self.generate_html_report(data)
    template = File.read('templates/report.html.erb')
    html = ERB.new(template).result_with_hash(data)
    
    File.write("reports/daily/#{Date.yesterday}.html", html)
    
    # Upload to S3 for sharing
    upload_to_s3("reports/daily/#{Date.yesterday}.html")
  end
  
  def self.generate_email_report(data)
    html = render_template('templates/email_report.html.erb', data)
    text = render_template('templates/email_report.txt.erb', data)
    
    Mail.deliver do
      to ENV['REPORT_RECIPIENTS'].split(',')
      subject "Daily Report for #{Date.yesterday}"
      html_part do
        content_type 'text/html; charset=UTF-8'
        body html
      end
      text_part do
        body text
      end
    end
  end
  
  def self.generate_slack_report(data)
    attachments = [
      {
        title: "📊 Daily Report - #{Date.yesterday}",
        fields: [
          {
            title: "Total Visits",
            value: data[:summary][:visits].to_s,
            short: true
          },
          {
            title: "Top Page",
            value: data[:top_pages].first[:path],
            short: true
          }
        ],
        color: "good"
      }
    ]
    
    Slack.notify(
      channel: '#reports',
      attachments: attachments
    )
  end
end

# Schedule with whenever
every :day, at: '6am' do
  runner "DailyReport.generate"
end

Adding Interactive Features

Make dashboards interactive:

1. Date Range Selector

# In your dashboard template
# Backend API endpoint get '/api/metrics' do start_date = params[:start_date] || 7.days.ago.to_s end_date = params[:end_date] || Date.today.to_s metrics = fetch_metrics_for_range(start_date, end_date) content_type :json metrics.to_json end

2. Drill-down Capabilities

# Click on a country to see regional data
<%= pie_chart traffic_by_country, 
  library: {
    onClick: 'function(point) { 
      window.location.href = "/dashboard/country/" + point.name;
    }'
  }
%>

# Country detail page
get '/dashboard/country/:country' do
  @country = params[:country]
  @metrics = fetch_country_metrics(@country)
  
  erb :country_dashboard
end

3. Comparative Analysis

# Compare periods
def compare_periods(current_start, current_end, previous_start, previous_end)
  current = fetch_metrics(current_start, current_end)
  previous = fetch_metrics(previous_start, previous_end)
  
  {
    current: current,
    previous: previous,
    change: calculate_percentage_change(current, previous)
  }
end

# Display comparison

Visits: <%= current[:visits] %> (<%= change[:visits] %>%)

Dashboard Deployment and Optimization

Deploy dashboards efficiently:

1. Caching Strategy

# Cache dashboard data
def cached_dashboard_data
  Rails.cache.fetch('dashboard_data', expires_in: 5.minutes) do
    fetch_dashboard_data
  end
end

# Cache individual charts
def cached_chart(name, &block)
  Rails.cache.fetch("chart_#{name}_#{Date.today}", &block)
end

2. Incremental Data Loading

# Load initial data, then update incrementally

3. Static Export for Sharing

# Export dashboard as static HTML
task :export_dashboard do
  # Fetch all data
  data = fetch_complete_dashboard_data
  
  # Generate standalone HTML with embedded data
  html = generate_standalone_html(data)
  
  # Compress
  compressed = Zlib::Deflate.deflate(html)
  
  # Save
  File.write('dashboard_export.html.gz', compressed)
end

4. Performance Optimization

# Optimize database queries
def optimized_metrics_query
  DB[:metrics].select(
    :timestamp,
    Sequel.lit("SUM(visits) as visits"),
    Sequel.lit("SUM(pageviews) as pageviews")
  ).where(timestamp: start_time..end_time)
   .group(Sequel.lit("DATE_TRUNC('hour', timestamp)"))
   .order(:timestamp)
   .naked
   .all
end

# Use materialized views for complex aggregations
DB.run( SQL)
  CREATE MATERIALIZED VIEW daily_aggregates AS
  SELECT 
    DATE(timestamp) as date,
    SUM(visits) as visits,
    SUM(pageviews) as pageviews,
    COUNT(DISTINCT ip) as unique_visitors
  FROM metrics
  GROUP BY DATE(timestamp)
SQL

Start building your custom dashboard today. Begin with a simple HTML page that displays basic Cloudflare metrics. Then add Ruby scripts to automate data collection. Gradually introduce more sophisticated visualizations and interactive features. Within weeks, you'll have a powerful analytics platform that gives you insights no standard dashboard can provide.