Files
container-census/examples/nextjs
Self Hosters 8dde58ba88 Add trending statistics to telemetry dashboard (hottest, movers, new entries)
- Add /api/stats/hottest endpoint for popular images by containers and adoption
- Add /api/stats/movers endpoint for week-over-week risers and fallers
- Add /api/stats/new-entries endpoint for newly discovered images
- Add image_stats_weekly table with automatic weekly snapshots
- Add Trends tab to vanilla JS dashboard with interactive charts
- Add collector-build.sh and collector-run.sh scripts for local development
- Add Next.js example components (HottestImagesChart, MoversChart, NewEntriesCard)
- Update telemetry-api.ts with new trending API methods and types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 16:17:41 -05:00
..

Container Census Next.js Integration

This directory contains ready-to-use components and utilities for integrating Container Census telemetry data into your Next.js application.

Table of Contents

Overview

This integration provides:

  • Type-safe API client for fetching telemetry data
  • Server Components for SSR performance and SEO
  • Client Components with interactive Chart.js visualizations
  • Pre-built charts matching the telemetry collector dashboard
  • API key authentication for secure data access

Prerequisites

  • Node.js 18+ (for Next.js App Router)
  • Next.js 13.4+ (with App Router)
  • Access to a Container Census telemetry collector instance
  • API key for stats endpoints (set via STATS_API_KEY env var on collector)

Installation

1. Copy Files to Your Project

Copy the contents of this directory to your Next.js project:

cp -r examples/nextjs/lib/* your-nextjs-app/lib/
cp -r examples/nextjs/components/* your-nextjs-app/components/
cp -r examples/nextjs/app/* your-nextjs-app/app/

2. Install Dependencies

npm install chart.js
# or
yarn add chart.js
# or
pnpm add chart.js

3. Configure TypeScript (if needed)

Ensure your tsconfig.json includes:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./*"]
    }
  }
}

Or update import paths in the copied files to match your project structure.

Configuration

Environment Variables

Create a .env.local file in your Next.js project root:

# Required: Telemetry collector API base URL
TELEMETRY_API_URL=https://telemetry.example.com

# Required: API key for authentication
TELEMETRY_API_KEY=your-api-key-here

Important: These environment variables are server-side only and will not be exposed to the browser. This keeps your API key secure.

Telemetry Collector Setup

On your telemetry collector instance, set the following environment variables:

# Generate a secure random API key
STATS_API_KEY=your-secure-api-key-here

# Optional: Enable Basic Auth for the dashboard UI
COLLECTOR_AUTH_ENABLED=true
COLLECTOR_AUTH_USERNAME=admin
COLLECTOR_AUTH_PASSWORD=secure-password

To generate a secure API key:

# Linux/macOS
openssl rand -hex 32

# Or use Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Usage

Quick Start

The easiest way to get started is to use the pre-built dashboard page:

  1. Copy app/telemetry/page.tsx to your Next.js app
  2. Visit http://localhost:3000/telemetry to see the dashboard

API Client

The API client provides type-safe methods for fetching telemetry data:

import { createTelemetryAPI } from '@/lib/telemetry-api';

// In a Server Component
export default async function MyPage() {
  const api = createTelemetryAPI();
  const summary = await api.getSummary();

  return (
    <div>
      <h1>Total Installations: {summary.installations}</h1>
      <p>Containers: {summary.total_containers}</p>
    </div>
  );
}

Server Components

Server Components fetch data at build time or on request, providing better SEO and initial load performance:

// app/stats/page.tsx
import { TelemetryStats } from '@/components/TelemetryStats';

export default function StatsPage() {
  return (
    <div>
      <h1>Telemetry Statistics</h1>
      <TelemetryStats />
    </div>
  );
}

Client Components

Client Components enable interactive charts with tooltips and animations:

// app/charts/page.tsx
import { createTelemetryAPI } from '@/lib/telemetry-api';
import { TopImagesChart } from '@/components/TopImagesChart';

export default async function ChartsPage() {
  const api = createTelemetryAPI();
  const images = await api.getTopImages({ limit: 10 });

  return (
    <div>
      <h1>Top Container Images</h1>
      <TopImagesChart images={images} />
    </div>
  );
}

Available Components

Server Components

TelemetryStats

Displays summary statistics in a card grid.

import { TelemetryStats } from '@/components/TelemetryStats';

<TelemetryStats />

Client Components (Interactive Charts)

All chart components accept data fetched from the API and render interactive Chart.js visualizations.

TopImagesChart

Horizontal bar chart of most popular container images.

import { TopImagesChart } from '@/components/TopImagesChart';

<TopImagesChart images={data} title="Most Popular Images" />

Props:

  • images: ImageCount[] - Array of image usage data
  • title?: string - Chart title (default: "Top Container Images")

GrowthChart

Line chart showing installation growth and average container count over time.

import { GrowthChart } from '@/components/GrowthChart';

<GrowthChart data={growth} title="Growth Trends" />

Props:

  • data: Growth[] - Array of growth data points
  • title?: string - Chart title (default: "Growth Over Time")

RegistryChart

Doughnut chart showing container registry distribution.

import { RegistryChart } from '@/components/RegistryChart';

<RegistryChart data={registries} />

Props:

  • data: RegistryCount[] - Array of registry usage data
  • title?: string - Chart title (default: "Registry Distribution")

VersionChart

Bar chart showing Container Census version adoption.

import { VersionChart } from '@/components/VersionChart';

<VersionChart data={versions} />

Props:

  • data: VersionCount[] - Array of version usage data
  • title?: string - Chart title (default: "Version Adoption")

GeographyChart

Bar chart showing geographic distribution based on timezone data.

import { GeographyChart } from '@/components/GeographyChart';

<GeographyChart data={geography} />

Props:

  • data: GeographyData[] - Array of geographic data
  • title?: string - Chart title (default: "Geographic Distribution")

ContainerImagesTable

Interactive, sortable, filterable table displaying detailed container image data with pagination.

import { ContainerImagesTable } from '@/components/ContainerImagesTable';

<ContainerImagesTable images={imageDetails} title="All Container Images" />

Props:

  • images: ImageDetail[] - Array of detailed image data (includes name, count, registry, installation count)

Connection & Architecture Charts

These components visualize container connectivity patterns, Docker Compose adoption, network usage, and volume sharing.

ComposeAdoptionChart

Pie chart showing Docker Compose adoption percentage.

import { ComposeAdoptionChart } from '@/components/ComposeAdoptionChart';

<ComposeAdoptionChart metrics={connectionMetrics} />

Props:

  • metrics: ConnectionMetrics - Connection metrics data
  • title?: string - Chart title (default: "🐳 Docker Compose Adoption")

ConnectivityChart

Bar chart showing container connectivity metrics including average connections, dependencies, and compose projects.

import { ConnectivityChart } from '@/components/ConnectivityChart';

<ConnectivityChart metrics={connectionMetrics} />

Props:

  • metrics: ConnectionMetrics - Connection metrics data
  • title?: string - Chart title (default: "🔗 Container Connectivity")

SharedVolumesChart

Doughnut chart showing volume sharing patterns across containers.

import { SharedVolumesChart } from '@/components/SharedVolumesChart';

<SharedVolumesChart metrics={connectionMetrics} />

Props:

  • metrics: ConnectionMetrics - Connection metrics data
  • title?: string - Chart title (default: "📦 Shared Volumes Usage")

CustomNetworksChart

Bar chart comparing custom networks vs default Docker networks.

import { CustomNetworksChart } from '@/components/CustomNetworksChart';

<CustomNetworksChart metrics={connectionMetrics} />

Props:

  • metrics: ConnectionMetrics - Connection metrics data
  • title?: string - Chart title (default: "🕸️ Custom Networks")

Features:

  • Client-side search/filtering by image name
  • Sortable columns (name, container count)
  • Pagination (50 items per page)
  • Color-coded registry badges (Docker Hub, GHCR, Quay, GCR, MCR)
  • Percentage of total containers calculation
  • Installation count per image
  • Responsive design with Tailwind CSS

These components visualize trends in container image popularity, week-over-week changes, and newly discovered images.

HottestImagesChart

Dual-ranking horizontal bar chart showing the most popular container images by both container count and adoption rate.

import { HottestImagesChart } from '@/components/HottestImagesChart';
import { createTelemetryAPI } from '@/lib/telemetry-api';

const api = createTelemetryAPI();
const hottest = await api.getHottest({ limit: 10, days: 7 });

<HottestImagesChart data={hottest} title="Hottest Container Images" />

Props:

  • data: HottestResponse - Hottest images data with by_containers and by_adoption arrays
  • title?: string - Chart title (default: "Hottest Container Images")

Features:

  • Toggle between "By Containers" and "By Adoption" views
  • Medal badges for top 3 positions (🥇🥈🥉)
  • Rich tooltips with container count and adoption percentage
  • Color-coded bars by rank

MoversChart

Two-column layout showing week-over-week biggest gainers (risers) and losers (fallers).

import { MoversChart } from '@/components/MoversChart';
import { createTelemetryAPI } from '@/lib/telemetry-api';

const api = createTelemetryAPI();
const movers = await api.getMovers({ limit: 10, weeks: 1 });

<MoversChart data={movers} title="Biggest Movers This Week" />

Props:

  • data: MoversResponse - Movers data with risers and fallers arrays
  • title?: string - Chart title (default: "Biggest Movers This Week")

Features:

  • Side-by-side risers (green) and fallers (red) columns
  • Change percentage badges
  • Previous → Current count display
  • Installation count indicators
  • Rank badges with medals for top 3

NewEntriesCard

Card grid layout displaying newly discovered container images that have reached significant adoption.

import { NewEntriesCard } from '@/components/NewEntriesCard';
import { createTelemetryAPI } from '@/lib/telemetry-api';

const api = createTelemetryAPI();
const newEntries = await api.getNewEntries({ limit: 12, days: 30 });

<NewEntriesCard data={newEntries} title="New Entries" />

Props:

  • data: NewEntriesResponse - New entries data with new_images array
  • title?: string - Card title (default: "New Entries")

Features:

  • "NEW" badge with pulse animation for very recent images
  • Days since first seen indicator
  • Adoption percentage progress bar
  • Container and installation counts
  • Responsive grid layout (1-4 columns)

API Reference

TelemetryAPI Class

import { TelemetryAPI } from '@/lib/telemetry-api';

const api = new TelemetryAPI({
  baseURL: 'https://telemetry.example.com',
  apiKey: 'your-api-key'
});

Methods

Method Parameters Returns Description
getSummary() - Promise<Summary> Get overview statistics
getTopImages() { limit?, days? } Promise<ImageCount[]> Get most popular images
getGrowth() { days? } Promise<Growth[]> Get growth metrics
getRegistries() { days? } Promise<RegistryCount[]> Get registry distribution
getVersions() - Promise<VersionCount[]> Get version distribution
getGeography() - Promise<GeographyData[]> Get geographic distribution
getActivityHeatmap() { days? } Promise<HeatmapData[]> Get activity heatmap data
getScanIntervals() - Promise<IntervalCount[]> Get scan interval distribution
getRecentEvents() { limit?, since? } Promise<SubmissionEvent[]> Get recent submissions
getInstallations() { days? } Promise<{...}> Get installation count
getImageDetails() { limit?, offset?, days?, search?, sort_by?, sort_order? } Promise<ImageDetailsResponse> Get detailed image data with pagination and search
getConnectionMetrics() { days? } Promise<ConnectionMetrics> Get container connectivity and architecture metrics
getHottest() { limit?, days?, metric? } Promise<HottestResponse> Get hottest images by containers and/or adoption
getMovers() { limit?, weeks?, min_installations? } Promise<MoversResponse> Get week-over-week biggest risers and fallers
getNewEntries() { limit?, days?, min_installations? } Promise<NewEntriesResponse> Get newly discovered images with significant adoption

Data Types

All TypeScript types are exported from @/lib/telemetry-api:

import {
  Summary,
  ImageCount,
  Growth,
  RegistryCount,
  VersionCount,
  GeographyData,
  HeatmapData,
  IntervalCount,
  SubmissionEvent,
  ImageDetail,
  ImageDetailsResponse,
  ConnectionMetrics,
  HotImage,
  HottestResponse,
  Mover,
  MoversResponse,
  NewEntry,
  NewEntriesResponse
} from '@/lib/telemetry-api';

Image Details Types:

interface ImageDetail {
  image: string;              // Container image name (normalized)
  count: number;              // Number of containers using this image
  registry: string;           // Registry source (Docker Hub, GHCR, etc.)
  installation_count: number; // Number of installations using this image
}

interface ImageDetailsResponse {
  images: ImageDetail[];
  pagination: {
    total: number;   // Total number of images
    limit: number;   // Page size
    offset: number;  // Current offset
  };
}

Connection Metrics Type:

interface ConnectionMetrics {
  total_containers: number;              // Total containers across all installations
  compose_project_count: number;         // Number of unique Docker Compose projects
  containers_in_compose: number;         // Containers managed by Compose
  compose_percentage: number;            // Percentage of containers using Compose
  network_count: number;                 // Total Docker networks
  custom_network_count: number;          // User-created networks (excludes bridge, host, none)
  shared_volume_count: number;           // Volumes shared by 2+ containers
  total_volumes: number;                 // Estimated total volumes
  containers_with_deps: number;          // Containers with depends_on configured
  total_dependencies: number;            // Total dependency relationships
  avg_connections_per_container: number; // Average network + volume connections per container
  installations: number;                 // Number of installations reporting this data
}

Trending Statistics Types:

interface HotImage {
  rank: number;                // Position in the ranking
  image: string;               // Normalized image name (no registry prefix or tag)
  total_containers: number;    // Total container instances across all installations
  installation_count: number;  // Number of unique installations using this image
  adoption_percentage: number; // Percentage of installations using this image
}

interface HottestResponse {
  by_containers?: HotImage[];  // Ranked by total container count
  by_adoption?: HotImage[];    // Ranked by adoption percentage
  total_installations: number; // Total installations in the period
  period_days: number;         // Number of days included
}

interface Mover {
  rank: number;                    // Position in risers/fallers list
  image: string;                   // Normalized image name
  current_count: number;           // Current week container count
  previous_count: number;          // Previous week container count
  change: number;                  // Absolute change (current - previous)
  change_percentage: number;       // Percentage change
  current_installations: number;   // Current week installation count
  previous_installations: number;  // Previous week installation count
}

interface MoversResponse {
  risers: Mover[];             // Images that gained the most containers
  fallers: Mover[];            // Images that lost the most containers
  comparison_weeks: number;    // Number of weeks compared
  current_week: string;        // Current week start date (YYYY-MM-DD)
  previous_week: string;       // Previous week start date (YYYY-MM-DD)
  min_installations: number;   // Minimum installations threshold used
}

interface NewEntry {
  rank: number;                  // Position in the list
  image: string;                 // Normalized image name
  first_seen: string;            // Date first observed (YYYY-MM-DD)
  days_since_first_seen: number; // Days since first seen
  total_containers: number;      // Current total container count
  installation_count: number;    // Current installation count
  adoption_percentage: number;   // Percentage of installations using this image
}

interface NewEntriesResponse {
  new_images: NewEntry[];      // List of newly discovered images
  period_days: number;         // Lookback period in days
  min_installations: number;   // Minimum installations threshold used
  total_new_images: number;    // Total count of new images
}

Security Best Practices

1. Keep API Keys Server-Side

DO: Use environment variables accessed only in Server Components

// Server Component - ✅ SAFE
const api = createTelemetryAPI(); // Uses process.env

DON'T: Expose API keys in Client Components or browser code

// Client Component - ❌ UNSAFE
'use client';
const apiKey = process.env.TELEMETRY_API_KEY; // This won't work anyway

2. Use Next.js Environment Variable Conventions

  • Prefix public variables with NEXT_PUBLIC_ (but don't do this for API keys)
  • Server-only variables (like TELEMETRY_API_KEY) are automatically protected

3. Implement Rate Limiting

For public-facing sites, implement your own rate limiting:

// middleware.ts
import { NextResponse } from 'next/server';
import { Ratelimit } from '@upstash/ratelimit';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'),
});

export async function middleware(request: Request) {
  if (request.url.includes('/telemetry')) {
    const ip = request.headers.get('x-forwarded-for') ?? 'anonymous';
    const { success } = await ratelimit.limit(ip);

    if (!success) {
      return new NextResponse('Rate limit exceeded', { status: 429 });
    }
  }

  return NextResponse.next();
}

4. Use HTTPS in Production

Always use HTTPS for your telemetry collector API in production:

# ✅ GOOD
TELEMETRY_API_URL=https://telemetry.example.com

# ❌ BAD (development only)
TELEMETRY_API_URL=http://telemetry.example.com

Deployment

Vercel

  1. Add environment variables in Project Settings → Environment Variables:

    • TELEMETRY_API_URL
    • TELEMETRY_API_KEY
  2. Deploy:

    vercel
    

Docker

FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

ENV TELEMETRY_API_URL=https://telemetry.example.com
ENV TELEMETRY_API_KEY=your-api-key

EXPOSE 3000
CMD ["npm", "start"]

Static Export (ISR)

For static sites with Incremental Static Regeneration:

// app/telemetry/page.tsx
export const revalidate = 300; // Revalidate every 5 minutes

export default async function Page() {
  const api = createTelemetryAPI();
  const data = await api.getSummary();

  return <div>{/* ... */}</div>;
}

Troubleshooting

"TELEMETRY_API_URL is not set" Error

Problem: Environment variable not loaded

Solutions:

  • Ensure .env.local exists in project root
  • Restart Next.js dev server (npm run dev)
  • Check variable names (no typos)

"Invalid or missing API key" (401 Error)

Problem: API key authentication failed

Solutions:

  • Verify TELEMETRY_API_KEY matches STATS_API_KEY on collector
  • Check for whitespace in environment variables
  • Restart both Next.js and telemetry collector services

CORS Errors in Browser

Problem: Cross-origin requests blocked

Solution: CORS is handled by the API middleware, but verify:

  • Using correct API endpoints (should include /api/stats/)
  • API requests from Server Components (not Client Components making direct fetch calls)

If you need Client Components to fetch directly:

// Create an API route proxy
// app/api/telemetry/[...path]/route.ts
import { createTelemetryAPI } from '@/lib/telemetry-api';

export async function GET(request: Request) {
  const api = createTelemetryAPI();
  // Forward request to telemetry API
  // This keeps API key server-side
}

Charts Not Rendering

Problem: Chart.js not loading or canvas errors

Solutions:

  • Ensure chart.js is installed: npm list chart.js
  • Check browser console for JavaScript errors
  • Verify component is marked with 'use client' directive
  • Confirm data is not empty

Stale Data Showing

Problem: Data not updating

Solutions:

  • Check revalidate setting in page component
  • Clear Next.js cache: rm -rf .next
  • Verify telemetry collector is receiving new data

Examples

Custom Styling

Customize chart colors to match your brand:

// components/BrandedTopImagesChart.tsx
'use client';

import { TopImagesChart } from '@/components/TopImagesChart';
import type { ImageCount } from '@/lib/telemetry-api';

// Override colorPalette with your brand colors
const brandColors = ['#FF0000', '#00FF00', '#0000FF'];

export function BrandedTopImagesChart({ images }: { images: ImageCount[] }) {
  // Modify the component to use brandColors
  return <TopImagesChart images={images} />;
}

Filtering Data

Show only specific data:

export default async function FilteredPage() {
  const api = createTelemetryAPI();
  const allImages = await api.getTopImages({ limit: 100 });

  // Filter to only show official images
  const officialImages = allImages.filter(img =>
    !img.image.includes('/')
  );

  return <TopImagesChart images={officialImages} />;
}

Multiple Dashboards

Create different views for different audiences:

// app/public-stats/page.tsx - Public view
export default async function PublicStats() {
  const api = createTelemetryAPI();
  const summary = await api.getSummary();

  return (
    <div>
      <h1>{summary.installations} Installations</h1>
      <p>Join the community!</p>
    </div>
  );
}

// app/admin/analytics/page.tsx - Admin view
import { headers } from 'next/headers';

export default async function AdminAnalytics() {
  // Check authentication
  const headersList = headers();
  const session = await getSession(headersList);

  if (!session?.isAdmin) {
    return <div>Unauthorized</div>;
  }

  // Full analytics dashboard
  const api = createTelemetryAPI();
  const [summary, growth, images] = await Promise.all([
    api.getSummary(),
    api.getGrowth({ days: 365 }),
    api.getTopImages({ limit: 50 })
  ]);

  return (
    <div>
      {/* Comprehensive dashboard */}
    </div>
  );
}

Support

For issues or questions:

License

This integration code is provided as part of the Container Census project. See the main project LICENSE file for details.