diff --git a/.github/workflows/ci-test-go.yml b/.github/workflows/ci-test-go.yml
index bb3e6b43cf..fea34e62ee 100644
--- a/.github/workflows/ci-test-go.yml
+++ b/.github/workflows/ci-test-go.yml
@@ -55,7 +55,7 @@ jobs:
steps:
- name: Setup rootless Docker
if: ${{ inputs.rootless-docker }}
- uses: docker/setup-docker-action@b60f85385d03ac8acfca6d9996982511d8620a19 # v4
+ uses: docker/setup-docker-action@efe9e3891a4f7307e689f2100b33a155b900a608 # v4.5.0
with:
rootless: true
diff --git a/.github/workflows/usage-metrics.yml b/.github/workflows/usage-metrics.yml
new file mode 100644
index 0000000000..c550468862
--- /dev/null
+++ b/.github/workflows/usage-metrics.yml
@@ -0,0 +1,85 @@
+name: Update Usage Metrics
+
+on:
+ schedule:
+ # Run monthly on the 1st at 9 AM UTC
+ - cron: '0 9 1 * *'
+ workflow_dispatch:
+ inputs:
+ versions:
+ description: 'Comma-separated versions to query (leave empty for all versions)'
+ required: false
+ default: ''
+
+permissions:
+ contents: write
+
+jobs:
+ collect-metrics:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - name: Set up Go
+ uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
+ with:
+ go-version-file: 'usage-metrics/go.mod'
+
+ - name: Query versions
+ id: query
+ working-directory: usage-metrics
+ run: |
+ # Get versions to query
+ VERSIONS="${{ github.event.inputs.versions }}"
+ if [ -z "$VERSIONS" ]; then
+ # Get all versions from v0.13.0 to latest from the main repository
+ VERSIONS=$(git ls-remote --tags https://github.com/testcontainers/testcontainers-go.git | \
+ grep -E 'refs/tags/v0\.[0-9]+\.[0-9]+$' | \
+ sed 's|.*refs/tags/||' | \
+ sort -V | \
+ awk '/v0.13.0/,0' | \
+ tr '\n' ',' | \
+ sed 's/,$//')
+ fi
+
+ echo "Querying versions: $VERSIONS"
+
+ # Build version flags
+ VERSION_FLAGS=""
+ IFS=',' read -ra VERSION_ARRAY <<< "$VERSIONS"
+ for version in "${VERSION_ARRAY[@]}"; do
+ version=$(echo "$version" | xargs) # trim whitespace
+ if [ -z "$version" ]; then
+ continue
+ fi
+ VERSION_FLAGS="$VERSION_FLAGS -version $version"
+ done
+
+ # Query all versions in a single command
+ go run collect-metrics.go $VERSION_FLAGS -csv "../../docs/usage-metrics.csv"
+
+ - name: Create Pull Request
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add docs/usage-metrics.csv
+
+ if git diff --staged --quiet; then
+ echo "No changes to commit"
+ exit 0
+ fi
+
+ # Create a new branch for the PR
+ DATE=$(date +%Y-%m-%d)
+ BRANCH_NAME="chore/update-usage-metrics-$DATE"
+ git checkout -b "$BRANCH_NAME"
+
+ git commit -m "chore(metrics): update usage metrics ($DATE)"
+ git push -u origin "$BRANCH_NAME"
+
+ # Create PR using gh CLI
+ gh pr create \
+ --title "chore: update usage metrics ($DATE)" \
+ --body "Automated update of usage metrics data. This PR updates the usage metrics CSV file with the latest GitHub usage data for testcontainers-go versions." \
+ --base main
diff --git a/.gitignore b/.gitignore
index b5fd75ccce..1693b100b6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,6 @@ TEST-*.xml
# Coverage files
coverage.out
+
+# Usage metrics script binary
+usage-metrics/scripts/collect-metrics
diff --git a/docs/css/usage-metrics.css b/docs/css/usage-metrics.css
new file mode 100644
index 0000000000..a716c29275
--- /dev/null
+++ b/docs/css/usage-metrics.css
@@ -0,0 +1,111 @@
+/* Usage Metrics Dashboard Styles */
+
+/* Expand content to use full available width - only on usage metrics page */
+body:has(#content.usage-metrics) .md-content__inner,
+body:has(.stats-grid) .md-content__inner {
+ max-width: none !important;
+ margin: 0 !important;
+}
+
+body:has(#content.usage-metrics) .md-content__inner > article,
+body:has(.stats-grid) .md-content__inner > article {
+ padding: 0 2rem;
+}
+
+@media screen and (min-width: 76.25em) {
+ body:has(#content.usage-metrics) .md-content__inner > article,
+ body:has(.stats-grid) .md-content__inner > article {
+ padding: 0 4rem;
+ }
+}
+
+@media screen and (min-width: 100em) {
+ body:has(#content.usage-metrics) .md-content__inner > article,
+ body:has(.stats-grid) .md-content__inner > article {
+ padding: 0 6rem;
+ }
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 20px;
+ margin-bottom: 40px;
+}
+
+.stat-card {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ padding: 25px;
+ border-radius: 15px;
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
+}
+
+.stat-label {
+ font-size: 0.9rem;
+ opacity: 0.9;
+ margin-bottom: 5px;
+}
+
+.stat-value {
+ font-size: 2.5rem;
+ font-weight: bold;
+}
+
+.chart-container {
+ background: white;
+ border-radius: 15px;
+ padding: 30px;
+ margin-bottom: 30px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+}
+
+.chart-title {
+ font-size: 1.5rem;
+ color: #333;
+ margin-bottom: 20px;
+ font-weight: 600;
+}
+
+.chart-container canvas {
+ max-height: 400px;
+}
+
+.loading {
+ text-align: center;
+ padding: 40px;
+ color: #666;
+ font-size: 1.2rem;
+}
+
+.error {
+ background: #fee;
+ border: 1px solid #fcc;
+ border-radius: 10px;
+ padding: 20px;
+ color: #c33;
+ margin: 20px 0;
+}
+
+.metrics-info {
+ text-align: center;
+ margin-top: 40px;
+ padding-top: 20px;
+ border-top: 1px solid #eee;
+ color: #666;
+ font-size: 0.9rem;
+}
+
+.metrics-info p {
+ margin: 5px 0;
+}
+
+@media (max-width: 768px) {
+ .stat-value {
+ font-size: 2rem;
+ }
+
+ .chart-container {
+ padding: 20px;
+ }
+}
diff --git a/docs/js/usage-metrics.js b/docs/js/usage-metrics.js
new file mode 100644
index 0000000000..544f899609
--- /dev/null
+++ b/docs/js/usage-metrics.js
@@ -0,0 +1,369 @@
+// Usage Metrics Dashboard JavaScript
+
+// Store chart instances to prevent canvas reuse errors
+let chartInstances = {};
+
+// Track if already initialized
+let isInitialized = false;
+
+// Load and parse CSV data
+async function loadData() {
+ try {
+ const response = await fetch('../usage-metrics.csv');
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const csvText = await response.text();
+
+ const parsed = Papa.parse(csvText, {
+ header: true,
+ dynamicTyping: true,
+ skipEmptyLines: true
+ });
+
+ if (parsed.errors.length > 0) {
+ console.error('CSV parsing errors:', parsed.errors);
+ }
+
+ return parsed.data;
+ } catch (error) {
+ showError(`Failed to load data: ${error.message}`);
+ throw error;
+ }
+}
+
+function showError(message) {
+ const loadingEl = document.getElementById('loading');
+ const errorEl = document.getElementById('error');
+ if (loadingEl) loadingEl.style.display = 'none';
+ if (errorEl) {
+ errorEl.textContent = message;
+ errorEl.style.display = 'block';
+ }
+}
+
+function processData(data) {
+ // Sort by date
+ data.sort((a, b) => new Date(a.date) - new Date(b.date));
+
+ // Get unique versions
+ const versions = [...new Set(data.map(d => d.version))].sort();
+
+ // Group by version
+ const byVersion = {};
+ data.forEach(item => {
+ if (!byVersion[item.version]) {
+ byVersion[item.version] = [];
+ }
+ byVersion[item.version].push(item);
+ });
+
+ return { data, versions, byVersion };
+}
+
+function createStats(processedData) {
+ const { data, versions, byVersion } = processedData;
+
+ // Calculate total repositories using latest counts
+ const latestByVersion = {};
+ versions.forEach(version => {
+ const versionData = byVersion[version];
+ if (versionData.length > 0) {
+ latestByVersion[version] = versionData[versionData.length - 1].count;
+ }
+ });
+
+ const totalRepos = Object.values(latestByVersion).reduce((sum, count) => sum + count, 0);
+ const latestVersion = versions[versions.length - 1];
+ const latestCount = latestByVersion[latestVersion] || 0;
+
+ const statsHtml = `
+
+
Total Repositories
+
${totalRepos}
+
+
+
Versions Tracked
+
${versions.length}
+
+
+
Latest Version
+
${latestVersion}
+
+
+
Latest Usage
+
${latestCount}
+
+ `;
+
+ const statsGrid = document.getElementById('stats-grid');
+ if (statsGrid) {
+ statsGrid.innerHTML = statsHtml;
+ }
+}
+
+function createTrendChart(processedData) {
+ const { data, versions, byVersion } = processedData;
+
+ const datasets = versions.map((version, index) => {
+ const versionData = byVersion[version];
+ const colors = [
+ '#667eea', '#764ba2', '#f093fb', '#4facfe',
+ '#43e97b', '#fa709a', '#fee140', '#30cfd0'
+ ];
+ const color = colors[index % colors.length];
+
+ return {
+ label: version,
+ data: versionData.map(d => ({ x: d.date, y: d.count })),
+ borderColor: color,
+ backgroundColor: color + '20',
+ tension: 0.4,
+ fill: true
+ };
+ });
+
+ const canvas = document.getElementById('trendChart');
+ if (!canvas) return;
+
+ // Destroy existing chart if it exists
+ if (chartInstances.trendChart) {
+ chartInstances.trendChart.destroy();
+ }
+
+ const ctx = canvas.getContext('2d');
+ chartInstances.trendChart = new Chart(ctx, {
+ type: 'line',
+ data: { datasets },
+ options: {
+ responsive: true,
+ maintainAspectRatio: true,
+ plugins: {
+ legend: {
+ display: true,
+ position: 'top'
+ },
+ tooltip: {
+ mode: 'index',
+ intersect: false
+ }
+ },
+ scales: {
+ x: {
+ type: 'time',
+ time: {
+ unit: 'month',
+ displayFormats: {
+ month: 'MMM yyyy'
+ }
+ },
+ title: {
+ display: true,
+ text: 'Date'
+ }
+ },
+ y: {
+ beginAtZero: true,
+ title: {
+ display: true,
+ text: 'Repository Count'
+ }
+ }
+ }
+ }
+ });
+}
+
+function createVersionChart(processedData) {
+ const { data } = processedData;
+
+ // Group by date and sum all version counts for each date
+ const totalsByDate = {};
+ data.forEach(item => {
+ if (!totalsByDate[item.date]) {
+ totalsByDate[item.date] = 0;
+ }
+ totalsByDate[item.date] += item.count;
+ });
+
+ // Convert to array and sort by date
+ const chartData = Object.entries(totalsByDate)
+ .map(([date, count]) => ({ x: date, y: count }))
+ .sort((a, b) => new Date(a.x) - new Date(b.x));
+
+ const canvas = document.getElementById('versionChart');
+ if (!canvas) return;
+
+ // Destroy existing chart if it exists
+ if (chartInstances.versionChart) {
+ chartInstances.versionChart.destroy();
+ }
+
+ const ctx = canvas.getContext('2d');
+ chartInstances.versionChart = new Chart(ctx, {
+ type: 'line',
+ data: {
+ datasets: [{
+ label: 'Total Repositories',
+ data: chartData,
+ borderColor: '#667eea',
+ backgroundColor: 'rgba(102, 126, 234, 0.1)',
+ tension: 0.4,
+ fill: true,
+ borderWidth: 3,
+ pointRadius: 5,
+ pointHoverRadius: 7,
+ pointBackgroundColor: '#667eea',
+ pointBorderColor: '#fff',
+ pointBorderWidth: 2
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: true,
+ plugins: {
+ legend: {
+ display: false
+ },
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ return `Total: ${context.parsed.y} repositories`;
+ }
+ }
+ }
+ },
+ scales: {
+ x: {
+ type: 'time',
+ time: {
+ unit: 'month',
+ displayFormats: {
+ month: 'MMM yyyy'
+ }
+ },
+ title: {
+ display: true,
+ text: 'Date'
+ }
+ },
+ y: {
+ beginAtZero: true,
+ title: {
+ display: true,
+ text: 'Total Repository Count'
+ }
+ }
+ }
+ }
+ });
+}
+
+function createLatestChart(processedData) {
+ const { versions, byVersion } = processedData;
+
+ // Get latest count for each version
+ const latestCounts = versions.map(version => {
+ const versionData = byVersion[version];
+ return versionData[versionData.length - 1].count;
+ });
+
+ const colors = versions.map((_, index) => {
+ const palette = [
+ '#667eea', '#764ba2', '#f093fb', '#4facfe',
+ '#43e97b', '#fa709a', '#fee140', '#30cfd0'
+ ];
+ return palette[index % palette.length];
+ });
+
+ const canvas = document.getElementById('latestChart');
+ if (!canvas) return;
+
+ // Destroy existing chart if it exists
+ if (chartInstances.latestChart) {
+ chartInstances.latestChart.destroy();
+ }
+
+ const ctx = canvas.getContext('2d');
+ chartInstances.latestChart = new Chart(ctx, {
+ type: 'doughnut',
+ data: {
+ labels: versions,
+ datasets: [{
+ data: latestCounts,
+ backgroundColor: colors,
+ borderColor: '#fff',
+ borderWidth: 2
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: true,
+ plugins: {
+ legend: {
+ display: true,
+ position: 'right'
+ },
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ const label = context.label || '';
+ const value = context.parsed || 0;
+ const total = context.dataset.data.reduce((a, b) => a + b, 0);
+ const percentage = ((value / total) * 100).toFixed(1);
+ return `${label}: ${value} (${percentage}%)`;
+ }
+ }
+ }
+ }
+ }
+ });
+}
+
+// Main execution
+async function init() {
+ // Prevent multiple initializations
+ if (isInitialized) {
+ return;
+ }
+
+ // Check if we're on the usage metrics page
+ const canvas = document.getElementById('trendChart');
+ if (!canvas) {
+ return;
+ }
+
+ isInitialized = true;
+
+ try {
+ const data = await loadData();
+ const processedData = processData(data);
+
+ const loadingEl = document.getElementById('loading');
+ const contentEl = document.getElementById('content');
+
+ if (loadingEl) loadingEl.style.display = 'none';
+ if (contentEl) contentEl.style.display = 'block';
+
+ createStats(processedData);
+ createTrendChart(processedData);
+ createVersionChart(processedData);
+ createLatestChart(processedData);
+
+ // Set update time
+ const updateTimeEl = document.getElementById('update-time');
+ if (updateTimeEl) {
+ updateTimeEl.textContent = new Date().toLocaleString();
+ }
+ } catch (error) {
+ console.error('Initialization failed:', error);
+ isInitialized = false; // Allow retry on error
+ }
+}
+
+// Run when page loads
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+} else {
+ init();
+}
diff --git a/docs/usage-metrics.csv b/docs/usage-metrics.csv
new file mode 100644
index 0000000000..f979626853
--- /dev/null
+++ b/docs/usage-metrics.csv
@@ -0,0 +1,758 @@
+date,version,count
+2023-09-01,v0.13.0,109
+2023-09-01,v0.14.0,36
+2023-09-01,v0.15.0,64
+2023-09-01,v0.16.0,10
+2023-09-01,v0.17.0,52
+2023-09-01,v0.18.0,25
+2023-09-01,v0.19.0,37
+2023-09-01,v0.20.0,2
+2023-09-01,v0.20.1,46
+2023-09-01,v0.21.0,76
+2023-09-01,v0.22.0,37
+2023-09-01,v0.23.0,207
+2023-09-01,v0.24.0,2
+2023-09-01,v0.24.1,83
+2023-09-01,v0.25.0,3
+2023-09-01,v0.26.0,0
+2023-09-01,v0.27.0,0
+2023-09-01,v0.28.0,0
+2023-09-01,v0.29.0,0
+2023-09-01,v0.29.1,0
+2023-09-01,v0.30.0,0
+2023-09-01,v0.31.0,0
+2023-09-01,v0.32.0,0
+2023-09-01,v0.33.0,0
+2023-09-01,v0.34.0,0
+2023-09-01,v0.35.0,0
+2023-10-01,v0.13.0,110
+2023-10-01,v0.14.0,36
+2023-10-01,v0.15.0,65
+2023-10-01,v0.16.0,11
+2023-10-01,v0.17.0,71
+2023-10-01,v0.18.0,25
+2023-10-01,v0.19.0,70
+2023-10-01,v0.20.0,1
+2023-10-01,v0.20.1,47
+2023-10-01,v0.21.0,66
+2023-10-01,v0.22.0,32
+2023-10-01,v0.23.0,170
+2023-10-01,v0.24.0,2
+2023-10-01,v0.24.1,17
+2023-10-01,v0.25.0,137
+2023-10-01,v0.26.0,65
+2023-10-01,v0.27.0,0
+2023-10-01,v0.28.0,0
+2023-10-01,v0.29.0,0
+2023-10-01,v0.29.1,0
+2023-10-01,v0.30.0,0
+2023-10-01,v0.31.0,0
+2023-10-01,v0.32.0,0
+2023-10-01,v0.33.0,0
+2023-10-01,v0.34.0,0
+2023-10-01,v0.35.0,0
+2023-11-01,v0.13.0,121
+2023-11-01,v0.14.0,34
+2023-11-01,v0.15.0,65
+2023-11-01,v0.16.0,11
+2023-11-01,v0.17.0,71
+2023-11-01,v0.18.0,24
+2023-11-01,v0.19.0,69
+2023-11-01,v0.20.0,2
+2023-11-01,v0.20.1,47
+2023-11-01,v0.21.0,54
+2023-11-01,v0.22.0,31
+2023-11-01,v0.23.0,145
+2023-11-01,v0.24.0,2
+2023-11-01,v0.24.1,11
+2023-11-01,v0.25.0,48
+2023-11-01,v0.26.0,231
+2023-11-01,v0.27.0,0
+2023-11-01,v0.28.0,0
+2023-11-01,v0.29.0,0
+2023-11-01,v0.29.1,0
+2023-11-01,v0.30.0,0
+2023-11-01,v0.31.0,0
+2023-11-01,v0.32.0,0
+2023-11-01,v0.33.0,0
+2023-11-01,v0.34.0,0
+2023-11-01,v0.35.0,0
+2023-12-01,v0.13.0,120
+2023-12-01,v0.14.0,34
+2023-12-01,v0.15.0,65
+2023-12-01,v0.16.0,11
+2023-12-01,v0.17.0,71
+2023-12-01,v0.18.0,24
+2023-12-01,v0.19.0,68
+2023-12-01,v0.20.0,2
+2023-12-01,v0.20.1,47
+2023-12-01,v0.21.0,48
+2023-12-01,v0.22.0,28
+2023-12-01,v0.23.0,141
+2023-12-01,v0.24.0,2
+2023-12-01,v0.24.1,9
+2023-12-01,v0.25.0,47
+2023-12-01,v0.26.0,284
+2023-12-01,v0.27.0,70
+2023-12-01,v0.28.0,0
+2023-12-01,v0.29.0,0
+2023-12-01,v0.29.1,0
+2023-12-01,v0.30.0,0
+2023-12-01,v0.31.0,0
+2023-12-01,v0.32.0,0
+2023-12-01,v0.33.0,0
+2023-12-01,v0.34.0,0
+2023-12-01,v0.35.0,0
+2024-01-01,v0.13.0,118
+2024-01-01,v0.14.0,34
+2024-01-01,v0.15.0,65
+2024-01-01,v0.16.0,11
+2024-01-01,v0.17.0,71
+2024-01-01,v0.18.0,26
+2024-01-01,v0.19.0,64
+2024-01-01,v0.20.0,2
+2024-01-01,v0.20.1,40
+2024-01-01,v0.21.0,41
+2024-01-01,v0.22.0,29
+2024-01-01,v0.23.0,140
+2024-01-01,v0.24.0,2
+2024-01-01,v0.24.1,10
+2024-01-01,v0.25.0,54
+2024-01-01,v0.26.0,180
+2024-01-01,v0.27.0,289
+2024-01-01,v0.28.0,0
+2024-01-01,v0.29.0,0
+2024-01-01,v0.29.1,0
+2024-01-01,v0.30.0,0
+2024-01-01,v0.31.0,0
+2024-01-01,v0.32.0,0
+2024-01-01,v0.33.0,0
+2024-01-01,v0.34.0,0
+2024-01-01,v0.35.0,0
+2024-02-01,v0.13.0,119
+2024-02-01,v0.14.0,34
+2024-02-01,v0.15.0,64
+2024-02-01,v0.16.0,12
+2024-02-01,v0.17.0,71
+2024-02-01,v0.18.0,26
+2024-02-01,v0.19.0,64
+2024-02-01,v0.20.0,2
+2024-02-01,v0.20.1,39
+2024-02-01,v0.21.0,40
+2024-02-01,v0.22.0,27
+2024-02-01,v0.23.0,128
+2024-02-01,v0.24.0,1
+2024-02-01,v0.24.1,10
+2024-02-01,v0.25.0,48
+2024-02-01,v0.26.0,192
+2024-02-01,v0.27.0,186
+2024-02-01,v0.28.0,108
+2024-02-01,v0.29.0,0
+2024-02-01,v0.29.1,0
+2024-02-01,v0.30.0,0
+2024-02-01,v0.31.0,0
+2024-02-01,v0.32.0,0
+2024-02-01,v0.33.0,0
+2024-02-01,v0.34.0,0
+2024-02-01,v0.35.0,0
+2024-03-01,v0.13.0,114
+2024-03-01,v0.14.0,33
+2024-03-01,v0.15.0,64
+2024-03-01,v0.16.0,12
+2024-03-01,v0.17.0,71
+2024-03-01,v0.18.0,27
+2024-03-01,v0.19.0,66
+2024-03-01,v0.20.0,2
+2024-03-01,v0.20.1,36
+2024-03-01,v0.21.0,39
+2024-03-01,v0.22.0,30
+2024-03-01,v0.23.0,115
+2024-03-01,v0.24.0,1
+2024-03-01,v0.24.1,11
+2024-03-01,v0.25.0,47
+2024-03-01,v0.26.0,194
+2024-03-01,v0.27.0,154
+2024-03-01,v0.28.0,98
+2024-03-01,v0.29.0,0
+2024-03-01,v0.29.1,186
+2024-03-01,v0.30.0,0
+2024-03-01,v0.31.0,0
+2024-03-01,v0.32.0,0
+2024-03-01,v0.33.0,0
+2024-03-01,v0.34.0,0
+2024-03-01,v0.35.0,0
+2024-04-01,v0.13.0,114
+2024-04-01,v0.14.0,35
+2024-04-01,v0.15.0,64
+2024-04-01,v0.16.0,12
+2024-04-01,v0.17.0,71
+2024-04-01,v0.18.0,27
+2024-04-01,v0.19.0,69
+2024-04-01,v0.20.0,1
+2024-04-01,v0.20.1,34
+2024-04-01,v0.21.0,39
+2024-04-01,v0.22.0,29
+2024-04-01,v0.23.0,139
+2024-04-01,v0.24.0,1
+2024-04-01,v0.24.1,11
+2024-04-01,v0.25.0,42
+2024-04-01,v0.26.0,210
+2024-04-01,v0.27.0,142
+2024-04-01,v0.28.0,88
+2024-04-01,v0.29.0,0
+2024-04-01,v0.29.1,165
+2024-04-01,v0.30.0,219
+2024-04-01,v0.31.0,0
+2024-04-01,v0.32.0,0
+2024-04-01,v0.33.0,0
+2024-04-01,v0.34.0,0
+2024-04-01,v0.35.0,0
+2024-05-01,v0.13.0,119
+2024-05-01,v0.14.0,38
+2024-05-01,v0.15.0,64
+2024-05-01,v0.16.0,12
+2024-05-01,v0.17.0,70
+2024-05-01,v0.18.0,28
+2024-05-01,v0.19.0,68
+2024-05-01,v0.20.0,1
+2024-05-01,v0.20.1,31
+2024-05-01,v0.21.0,39
+2024-05-01,v0.22.0,26
+2024-05-01,v0.23.0,127
+2024-05-01,v0.24.0,1
+2024-05-01,v0.24.1,10
+2024-05-01,v0.25.0,43
+2024-05-01,v0.26.0,165
+2024-05-01,v0.27.0,143
+2024-05-01,v0.28.0,80
+2024-05-01,v0.29.0,0
+2024-05-01,v0.29.1,155
+2024-05-01,v0.30.0,161
+2024-05-01,v0.31.0,257
+2024-05-01,v0.32.0,0
+2024-05-01,v0.33.0,0
+2024-05-01,v0.34.0,0
+2024-05-01,v0.35.0,0
+2024-06-01,v0.13.0,121
+2024-06-01,v0.14.0,38
+2024-06-01,v0.15.0,64
+2024-06-01,v0.16.0,12
+2024-06-01,v0.17.0,71
+2024-06-01,v0.18.0,28
+2024-06-01,v0.19.0,68
+2024-06-01,v0.20.0,1
+2024-06-01,v0.20.1,32
+2024-06-01,v0.21.0,38
+2024-06-01,v0.22.0,25
+2024-06-01,v0.23.0,98
+2024-06-01,v0.24.0,1
+2024-06-01,v0.24.1,10
+2024-06-01,v0.25.0,45
+2024-06-01,v0.26.0,157
+2024-06-01,v0.27.0,159
+2024-06-01,v0.28.0,66
+2024-06-01,v0.29.0,0
+2024-06-01,v0.29.1,138
+2024-06-01,v0.30.0,156
+2024-06-01,v0.31.0,516
+2024-06-01,v0.32.0,32
+2024-06-01,v0.33.0,0
+2024-06-01,v0.34.0,0
+2024-06-01,v0.35.0,0
+2024-07-01,v0.13.0,121
+2024-07-01,v0.14.0,38
+2024-07-01,v0.15.0,64
+2024-07-01,v0.16.0,12
+2024-07-01,v0.17.0,71
+2024-07-01,v0.18.0,28
+2024-07-01,v0.19.0,68
+2024-07-01,v0.20.0,1
+2024-07-01,v0.20.1,32
+2024-07-01,v0.21.0,38
+2024-07-01,v0.22.0,25
+2024-07-01,v0.23.0,98
+2024-07-01,v0.24.0,1
+2024-07-01,v0.24.1,10
+2024-07-01,v0.25.0,45
+2024-07-01,v0.26.0,153
+2024-07-01,v0.27.0,142
+2024-07-01,v0.28.0,68
+2024-07-01,v0.29.0,0
+2024-07-01,v0.29.1,129
+2024-07-01,v0.30.0,136
+2024-07-01,v0.31.0,506
+2024-07-01,v0.32.0,101
+2024-07-01,v0.33.0,0
+2024-07-01,v0.34.0,0
+2024-07-01,v0.35.0,0
+2024-08-01,v0.13.0,120
+2024-08-01,v0.14.0,38
+2024-08-01,v0.15.0,64
+2024-08-01,v0.16.0,12
+2024-08-01,v0.17.0,69
+2024-08-01,v0.18.0,28
+2024-08-01,v0.19.0,67
+2024-08-01,v0.20.0,1
+2024-08-01,v0.20.1,31
+2024-08-01,v0.21.0,38
+2024-08-01,v0.22.0,25
+2024-08-01,v0.23.0,101
+2024-08-01,v0.24.0,1
+2024-08-01,v0.24.1,11
+2024-08-01,v0.25.0,51
+2024-08-01,v0.26.0,145
+2024-08-01,v0.27.0,126
+2024-08-01,v0.28.0,79
+2024-08-01,v0.29.0,0
+2024-08-01,v0.29.1,138
+2024-08-01,v0.30.0,149
+2024-08-01,v0.31.0,544
+2024-08-01,v0.32.0,296
+2024-08-01,v0.33.0,0
+2024-08-01,v0.34.0,0
+2024-08-01,v0.35.0,0
+2024-09-01,v0.13.0,122
+2024-09-01,v0.14.0,39
+2024-09-01,v0.15.0,66
+2024-09-01,v0.16.0,12
+2024-09-01,v0.17.0,69
+2024-09-01,v0.18.0,29
+2024-09-01,v0.19.0,66
+2024-09-01,v0.20.0,1
+2024-09-01,v0.20.1,31
+2024-09-01,v0.21.0,38
+2024-09-01,v0.22.0,26
+2024-09-01,v0.23.0,104
+2024-09-01,v0.24.0,1
+2024-09-01,v0.24.1,10
+2024-09-01,v0.25.0,48
+2024-09-01,v0.26.0,137
+2024-09-01,v0.27.0,135
+2024-09-01,v0.28.0,83
+2024-09-01,v0.29.0,0
+2024-09-01,v0.29.1,127
+2024-09-01,v0.30.0,147
+2024-09-01,v0.31.0,544
+2024-09-01,v0.32.0,305
+2024-09-01,v0.33.0,468
+2024-09-01,v0.34.0,0
+2024-09-01,v0.35.0,0
+2024-10-01,v0.13.0,124
+2024-10-01,v0.14.0,38
+2024-10-01,v0.15.0,67
+2024-10-01,v0.16.0,12
+2024-10-01,v0.17.0,68
+2024-10-01,v0.18.0,29
+2024-10-01,v0.19.0,66
+2024-10-01,v0.20.0,1
+2024-10-01,v0.20.1,34
+2024-10-01,v0.21.0,37
+2024-10-01,v0.22.0,30
+2024-10-01,v0.23.0,100
+2024-10-01,v0.24.0,1
+2024-10-01,v0.24.1,11
+2024-10-01,v0.25.0,48
+2024-10-01,v0.26.0,141
+2024-10-01,v0.27.0,127
+2024-10-01,v0.28.0,93
+2024-10-01,v0.29.0,0
+2024-10-01,v0.29.1,137
+2024-10-01,v0.30.0,142
+2024-10-01,v0.31.0,488
+2024-10-01,v0.32.0,266
+2024-10-01,v0.33.0,432
+2024-10-01,v0.34.0,0
+2024-10-01,v0.35.0,0
+2024-11-01,v0.13.0,120
+2024-11-01,v0.14.0,38
+2024-11-01,v0.15.0,67
+2024-11-01,v0.16.0,12
+2024-11-01,v0.17.0,69
+2024-11-01,v0.18.0,29
+2024-11-01,v0.19.0,65
+2024-11-01,v0.20.0,1
+2024-11-01,v0.20.1,33
+2024-11-01,v0.21.0,38
+2024-11-01,v0.22.0,30
+2024-11-01,v0.23.0,99
+2024-11-01,v0.24.0,1
+2024-11-01,v0.24.1,11
+2024-11-01,v0.25.0,48
+2024-11-01,v0.26.0,137
+2024-11-01,v0.27.0,129
+2024-11-01,v0.28.0,93
+2024-11-01,v0.29.0,0
+2024-11-01,v0.29.1,120
+2024-11-01,v0.30.0,120
+2024-11-01,v0.31.0,488
+2024-11-01,v0.32.0,221
+2024-11-01,v0.33.0,500
+2024-11-01,v0.34.0,168
+2024-11-01,v0.35.0,0
+2024-12-01,v0.13.0,122
+2024-12-01,v0.14.0,37
+2024-12-01,v0.15.0,66
+2024-12-01,v0.16.0,12
+2024-12-01,v0.17.0,69
+2024-12-01,v0.18.0,29
+2024-12-01,v0.19.0,64
+2024-12-01,v0.20.0,1
+2024-12-01,v0.20.1,55
+2024-12-01,v0.21.0,37
+2024-12-01,v0.22.0,30
+2024-12-01,v0.23.0,102
+2024-12-01,v0.24.0,1
+2024-12-01,v0.24.1,11
+2024-12-01,v0.25.0,51
+2024-12-01,v0.26.0,131
+2024-12-01,v0.27.0,120
+2024-12-01,v0.28.0,87
+2024-12-01,v0.29.0,0
+2024-12-01,v0.29.1,115
+2024-12-01,v0.30.0,124
+2024-12-01,v0.31.0,406
+2024-12-01,v0.32.0,208
+2024-12-01,v0.33.0,524
+2024-12-01,v0.34.0,458
+2024-12-01,v0.35.0,0
+2025-01-01,v0.13.0,123
+2025-01-01,v0.14.0,34
+2025-01-01,v0.15.0,66
+2025-01-01,v0.16.0,12
+2025-01-01,v0.17.0,50
+2025-01-01,v0.18.0,29
+2025-01-01,v0.19.0,64
+2025-01-01,v0.20.0,1
+2025-01-01,v0.20.1,54
+2025-01-01,v0.21.0,36
+2025-01-01,v0.22.0,30
+2025-01-01,v0.23.0,101
+2025-01-01,v0.24.0,1
+2025-01-01,v0.24.1,11
+2025-01-01,v0.25.0,50
+2025-01-01,v0.26.0,137
+2025-01-01,v0.27.0,127
+2025-01-01,v0.28.0,89
+2025-01-01,v0.29.0,0
+2025-01-01,v0.29.1,117
+2025-01-01,v0.30.0,134
+2025-01-01,v0.31.0,354
+2025-01-01,v0.32.0,227
+2025-01-01,v0.33.0,514
+2025-01-01,v0.34.0,644
+2025-01-01,v0.35.0,3
+2025-01-01,v0.36.0,0
+2025-01-01,v0.37.0,0
+2025-01-01,v0.38.0,0
+2025-01-01,v0.39.0,0
+2025-01-01,v0.40.0,0
+2025-02-01,v0.13.0,120
+2025-02-01,v0.14.0,33
+2025-02-01,v0.15.0,66
+2025-02-01,v0.16.0,12
+2025-02-01,v0.17.0,51
+2025-02-01,v0.18.0,31
+2025-02-01,v0.19.0,64
+2025-02-01,v0.20.0,1
+2025-02-01,v0.20.1,56
+2025-02-01,v0.21.0,36
+2025-02-01,v0.22.0,30
+2025-02-01,v0.23.0,100
+2025-02-01,v0.24.0,1
+2025-02-01,v0.24.1,11
+2025-02-01,v0.25.0,48
+2025-02-01,v0.26.0,132
+2025-02-01,v0.27.0,124
+2025-02-01,v0.28.0,92
+2025-02-01,v0.29.0,0
+2025-02-01,v0.29.1,121
+2025-02-01,v0.30.0,138
+2025-02-01,v0.31.0,424
+2025-02-01,v0.32.0,209
+2025-02-01,v0.33.0,510
+2025-02-01,v0.34.0,414
+2025-02-01,v0.35.0,556
+2025-02-01,v0.36.0,0
+2025-02-01,v0.37.0,0
+2025-02-01,v0.38.0,0
+2025-02-01,v0.39.0,0
+2025-02-01,v0.40.0,0
+2025-03-01,v0.13.0,128
+2025-03-01,v0.14.0,33
+2025-03-01,v0.15.0,66
+2025-03-01,v0.16.0,12
+2025-03-01,v0.17.0,50
+2025-03-01,v0.18.0,31
+2025-03-01,v0.19.0,63
+2025-03-01,v0.20.0,1
+2025-03-01,v0.20.1,54
+2025-03-01,v0.21.0,36
+2025-03-01,v0.22.0,32
+2025-03-01,v0.23.0,100
+2025-03-01,v0.24.0,1
+2025-03-01,v0.24.1,11
+2025-03-01,v0.25.0,47
+2025-03-01,v0.26.0,133
+2025-03-01,v0.27.0,125
+2025-03-01,v0.28.0,94
+2025-03-01,v0.29.0,0
+2025-03-01,v0.29.1,118
+2025-03-01,v0.30.0,148
+2025-03-01,v0.31.0,396
+2025-03-01,v0.32.0,180
+2025-03-01,v0.33.0,468
+2025-03-01,v0.34.0,388
+2025-03-01,v0.35.0,668
+2025-03-01,v0.36.0,0
+2025-03-01,v0.37.0,0
+2025-03-01,v0.38.0,0
+2025-03-01,v0.39.0,0
+2025-03-01,v0.40.0,0
+2025-04-01,v0.13.0,127
+2025-04-01,v0.14.0,33
+2025-04-01,v0.15.0,67
+2025-04-01,v0.16.0,12
+2025-04-01,v0.17.0,50
+2025-04-01,v0.18.0,32
+2025-04-01,v0.19.0,63
+2025-04-01,v0.20.0,1
+2025-04-01,v0.20.1,54
+2025-04-01,v0.21.0,36
+2025-04-01,v0.22.0,32
+2025-04-01,v0.23.0,99
+2025-04-01,v0.24.0,1
+2025-04-01,v0.24.1,11
+2025-04-01,v0.25.0,47
+2025-04-01,v0.26.0,131
+2025-04-01,v0.27.0,103
+2025-04-01,v0.28.0,95
+2025-04-01,v0.29.0,0
+2025-04-01,v0.29.1,103
+2025-04-01,v0.30.0,155
+2025-04-01,v0.31.0,401
+2025-04-01,v0.32.0,189
+2025-04-01,v0.33.0,460
+2025-04-01,v0.34.0,326
+2025-04-01,v0.35.0,526
+2025-04-01,v0.36.0,316
+2025-04-01,v0.37.0,0
+2025-04-01,v0.38.0,0
+2025-04-01,v0.39.0,0
+2025-04-01,v0.40.0,0
+2025-05-01,v0.13.0,123
+2025-05-01,v0.14.0,33
+2025-05-01,v0.15.0,68
+2025-05-01,v0.16.0,12
+2025-05-01,v0.17.0,51
+2025-05-01,v0.18.0,33
+2025-05-01,v0.19.0,63
+2025-05-01,v0.20.0,1
+2025-05-01,v0.20.1,54
+2025-05-01,v0.21.0,34
+2025-05-01,v0.22.0,30
+2025-05-01,v0.23.0,98
+2025-05-01,v0.24.0,2
+2025-05-01,v0.24.1,10
+2025-05-01,v0.25.0,48
+2025-05-01,v0.26.0,130
+2025-05-01,v0.27.0,130
+2025-05-01,v0.28.0,96
+2025-05-01,v0.29.0,0
+2025-05-01,v0.29.1,99
+2025-05-01,v0.30.0,154
+2025-05-01,v0.31.0,373
+2025-05-01,v0.32.0,191
+2025-05-01,v0.33.0,484
+2025-05-01,v0.34.0,384
+2025-05-01,v0.35.0,508
+2025-05-01,v0.36.0,213
+2025-05-01,v0.37.0,524
+2025-05-01,v0.38.0,0
+2025-05-01,v0.39.0,0
+2025-05-01,v0.40.0,0
+2025-06-01,v0.13.0,123
+2025-06-01,v0.14.0,33
+2025-06-01,v0.15.0,68
+2025-06-01,v0.16.0,12
+2025-06-01,v0.17.0,51
+2025-06-01,v0.18.0,33
+2025-06-01,v0.19.0,63
+2025-06-01,v0.20.0,1
+2025-06-01,v0.20.1,54
+2025-06-01,v0.21.0,35
+2025-06-01,v0.22.0,29
+2025-06-01,v0.23.0,93
+2025-06-01,v0.24.0,2
+2025-06-01,v0.24.1,12
+2025-06-01,v0.25.0,51
+2025-06-01,v0.26.0,137
+2025-06-01,v0.27.0,128
+2025-06-01,v0.28.0,97
+2025-06-01,v0.29.0,0
+2025-06-01,v0.29.1,109
+2025-06-01,v0.30.0,154
+2025-06-01,v0.31.0,412
+2025-06-01,v0.32.0,194
+2025-06-01,v0.33.0,464
+2025-06-01,v0.34.0,424
+2025-06-01,v0.35.0,534
+2025-06-01,v0.36.0,186
+2025-06-01,v0.37.0,752
+2025-06-01,v0.38.0,0
+2025-06-01,v0.39.0,0
+2025-06-01,v0.40.0,0
+2025-07-01,v0.13.0,118
+2025-07-01,v0.14.0,33
+2025-07-01,v0.15.0,67
+2025-07-01,v0.16.0,12
+2025-07-01,v0.17.0,52
+2025-07-01,v0.18.0,33
+2025-07-01,v0.19.0,64
+2025-07-01,v0.20.0,1
+2025-07-01,v0.20.1,54
+2025-07-01,v0.21.0,35
+2025-07-01,v0.22.0,30
+2025-07-01,v0.23.0,93
+2025-07-01,v0.24.0,2
+2025-07-01,v0.24.1,13
+2025-07-01,v0.25.0,55
+2025-07-01,v0.26.0,139
+2025-07-01,v0.27.0,131
+2025-07-01,v0.28.0,98
+2025-07-01,v0.29.0,0
+2025-07-01,v0.29.1,114
+2025-07-01,v0.30.0,160
+2025-07-01,v0.31.0,380
+2025-07-01,v0.32.0,203
+2025-07-01,v0.33.0,464
+2025-07-01,v0.34.0,404
+2025-07-01,v0.35.0,500
+2025-07-01,v0.36.0,216
+2025-07-01,v0.37.0,1100
+2025-07-01,v0.38.0,97
+2025-07-01,v0.39.0,0
+2025-07-01,v0.40.0,0
+2025-08-01,v0.13.0,116
+2025-08-01,v0.14.0,33
+2025-08-01,v0.15.0,87
+2025-08-01,v0.16.0,12
+2025-08-01,v0.17.0,50
+2025-08-01,v0.18.0,33
+2025-08-01,v0.19.0,64
+2025-08-01,v0.20.0,1
+2025-08-01,v0.20.1,55
+2025-08-01,v0.21.0,35
+2025-08-01,v0.22.0,30
+2025-08-01,v0.23.0,92
+2025-08-01,v0.24.0,1
+2025-08-01,v0.24.1,16
+2025-08-01,v0.25.0,48
+2025-08-01,v0.26.0,146
+2025-08-01,v0.27.0,132
+2025-08-01,v0.28.0,94
+2025-08-01,v0.29.0,0
+2025-08-01,v0.29.1,114
+2025-08-01,v0.30.0,156
+2025-08-01,v0.31.0,399
+2025-08-01,v0.32.0,200
+2025-08-01,v0.33.0,436
+2025-08-01,v0.34.0,340
+2025-08-01,v0.35.0,466
+2025-08-01,v0.36.0,192
+2025-08-01,v0.37.0,840
+2025-08-01,v0.38.0,912
+2025-08-01,v0.39.0,0
+2025-08-01,v0.40.0,0
+2025-09-01,v0.13.0,115
+2025-09-01,v0.14.0,33
+2025-09-01,v0.15.0,87
+2025-09-01,v0.16.0,12
+2025-09-01,v0.17.0,50
+2025-09-01,v0.18.0,33
+2025-09-01,v0.19.0,64
+2025-09-01,v0.20.0,1
+2025-09-01,v0.20.1,56
+2025-09-01,v0.21.0,35
+2025-09-01,v0.22.0,29
+2025-09-01,v0.23.0,92
+2025-09-01,v0.24.0,1
+2025-09-01,v0.24.1,17
+2025-09-01,v0.25.0,51
+2025-09-01,v0.26.0,152
+2025-09-01,v0.27.0,117
+2025-09-01,v0.28.0,93
+2025-09-01,v0.29.0,0
+2025-09-01,v0.29.1,103
+2025-09-01,v0.30.0,158
+2025-09-01,v0.31.0,359
+2025-09-01,v0.32.0,198
+2025-09-01,v0.33.0,420
+2025-09-01,v0.34.0,344
+2025-09-01,v0.35.0,460
+2025-09-01,v0.36.0,204
+2025-09-01,v0.37.0,852
+2025-09-01,v0.38.0,592
+2025-09-01,v0.39.0,392
+2025-09-01,v0.40.0,0
+2025-10-01,v0.13.0,115
+2025-10-01,v0.14.0,33
+2025-10-01,v0.15.0,87
+2025-10-01,v0.16.0,12
+2025-10-01,v0.17.0,50
+2025-10-01,v0.18.0,32
+2025-10-01,v0.19.0,65
+2025-10-01,v0.20.0,1
+2025-10-01,v0.20.1,57
+2025-10-01,v0.21.0,35
+2025-10-01,v0.22.0,28
+2025-10-01,v0.23.0,92
+2025-10-01,v0.24.0,1
+2025-10-01,v0.24.1,18
+2025-10-01,v0.25.0,50
+2025-10-01,v0.26.0,151
+2025-10-01,v0.27.0,127
+2025-10-01,v0.28.0,93
+2025-10-01,v0.29.0,0
+2025-10-01,v0.29.1,103
+2025-10-01,v0.30.0,174
+2025-10-01,v0.31.0,367
+2025-10-01,v0.32.0,191
+2025-10-01,v0.33.0,398
+2025-10-01,v0.34.0,352
+2025-10-01,v0.35.0,534
+2025-10-01,v0.36.0,191
+2025-10-01,v0.37.0,852
+2025-10-01,v0.38.0,564
+2025-10-01,v0.39.0,584
+2025-10-01,v0.40.0,0
+2025-11-01,v0.13.0,114
+2025-11-01,v0.14.0,31
+2025-11-01,v0.15.0,87
+2025-11-01,v0.16.0,11
+2025-11-01,v0.17.0,50
+2025-11-01,v0.18.0,32
+2025-11-01,v0.19.0,60
+2025-11-01,v0.20.0,1
+2025-11-01,v0.20.1,56
+2025-11-01,v0.21.0,35
+2025-11-01,v0.22.0,28
+2025-11-01,v0.23.0,90
+2025-11-01,v0.24.0,1
+2025-11-01,v0.24.1,18
+2025-11-01,v0.25.0,49
+2025-11-01,v0.26.0,151
+2025-11-01,v0.27.0,130
+2025-11-01,v0.28.0,93
+2025-11-01,v0.29.0,0
+2025-11-01,v0.29.1,103
+2025-11-01,v0.30.0,165
+2025-11-01,v0.31.0,353
+2025-11-01,v0.32.0,198
+2025-11-01,v0.33.0,416
+2025-11-01,v0.34.0,312
+2025-11-01,v0.35.0,520
+2025-11-01,v0.36.0,173
+2025-11-01,v0.37.0,808
+2025-11-01,v0.38.0,656
+2025-11-01,v0.39.0,544
+2025-11-01,v0.40.0,240
diff --git a/docs/usage-metrics.md b/docs/usage-metrics.md
new file mode 100644
index 0000000000..a1d8269299
--- /dev/null
+++ b/docs/usage-metrics.md
@@ -0,0 +1,34 @@
+# Usage Metrics
+
+Loading metrics data...
+
+
+
+
+
+
+
+
+
Total Adoption Over Time
+
+
+
+
+
Usage Trend Over Time
+
+
+
+
+
Latest Usage by Version
+
+
+
+
+
+
+## GitHub Stars
+
+[](https://www.star-history.com/#testcontainers/testcontainers-go&type=date&legend=top-left)
diff --git a/mkdocs.yml b/mkdocs.yml
index ac77b5b3bc..10b1dc85f2 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -19,6 +19,12 @@ theme:
extra_css:
- css/extra.css
- css/tc-header.css
+ - css/usage-metrics.css
+extra_javascript:
+ - https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js
+ - https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js
+ - https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js
+ - js/usage-metrics.js
repo_name: testcontainers-go
repo_url: https://github.com/testcontainers/testcontainers-go
markdown_extensions:
@@ -148,6 +154,7 @@ nav:
- system_requirements/using_colima.md
- system_requirements/using_podman.md
- system_requirements/rancher.md
+ - Usage Metrics: usage-metrics.md
- Dependabot: dependabot.md
- Contributing: contributing.md
- Getting help: getting_help.md
diff --git a/modulegen/internal/mkdocs/types.go b/modulegen/internal/mkdocs/types.go
index 460f958282..6a599b56a7 100644
--- a/modulegen/internal/mkdocs/types.go
+++ b/modulegen/internal/mkdocs/types.go
@@ -23,6 +23,7 @@ type Config struct {
Favicon string `yaml:"favicon"`
} `yaml:"theme"`
ExtraCSS []string `yaml:"extra_css"`
+ ExtraJavascript []string `yaml:"extra_javascript"`
RepoName string `yaml:"repo_name"`
RepoURL string `yaml:"repo_url"`
MarkdownExtensions []any `yaml:"markdown_extensions"`
@@ -34,6 +35,7 @@ type Config struct {
Modules []string `yaml:"Modules,omitempty"`
SystemRequirements []any `yaml:"System Requirements,omitempty"`
Dependabot string `yaml:"Dependabot,omitempty"`
+ UsageMetrics string `yaml:"Usage Metrics,omitempty"`
Contributing string `yaml:"Contributing,omitempty"`
GettingHelp string `yaml:"Getting help,omitempty"`
} `yaml:"nav"`
diff --git a/scripts/changed-modules.sh b/scripts/changed-modules.sh
index 42419653a1..ac530995be 100755
--- a/scripts/changed-modules.sh
+++ b/scripts/changed-modules.sh
@@ -86,6 +86,7 @@ readonly excluded_files=(
".github/workflows/release-drafter.yml"
".github/workflows/scorecards.yml"
".github/workflows/sonar-*.yml"
+ ".github/workflows/usage-metrics.yml"
"scripts/bump-*.sh"
"scripts/check_environment.sh"
"scripts/*release.sh"
@@ -98,6 +99,7 @@ readonly excluded_files=(
"RELEASING.md"
"requirements.txt"
"runtime.txt"
+ "docs/usage-metrics.csv"
)
# define an array of modules that won't be part of the build
@@ -124,8 +126,11 @@ readonly rootModule="\"\""
# capture the modulegen module
readonly modulegenModule="\"modulegen\""
+# capture the usage-metrics module
+readonly usageMetricsModule="\"usage-metrics\""
+
# merge all modules and examples into a single array
-allModules=(${rootModule} ${modulegenModule} "${modules[@]}")
+allModules=(${rootModule} ${modulegenModule} ${usageMetricsModule} "${modules[@]}")
# sort allModules array
IFS=$'\n' allModules=($(sort <<<"${allModules[*]}"))
diff --git a/usage-metrics/Makefile b/usage-metrics/Makefile
new file mode 100644
index 0000000000..748cb213ea
--- /dev/null
+++ b/usage-metrics/Makefile
@@ -0,0 +1 @@
+include ../commons-test.mk
diff --git a/usage-metrics/README.md b/usage-metrics/README.md
new file mode 100644
index 0000000000..6b5390a558
--- /dev/null
+++ b/usage-metrics/README.md
@@ -0,0 +1,188 @@
+# Testcontainers-Go Usage Metrics
+
+This directory contains the automation system for tracking testcontainers-go usage across GitHub repositories.
+
+## Overview
+
+The system automatically collects usage metrics by querying the GitHub Code Search API for references to testcontainers-go in `go.mod` files across public repositories. The data is visualized in an interactive dashboard integrated into the main MkDocs documentation site at https://golang.testcontainers.org/usage-metrics/
+
+## Components
+
+### 📊 Data Collection (`scripts/`)
+- **collect-metrics.go**: Go program that queries GitHub's Code Search API
+- Searches for `"testcontainers/testcontainers-go {version}"` in go.mod files
+- Excludes forks and testcontainers organization repositories
+- Stores results in CSV format with timestamps
+
+### 💾 Data Storage (`docs/usage-metrics.csv`)
+- **usage-metrics.csv**: Historical usage data in CSV format
+- Format: `date,version,count`
+- Version-controlled for historical tracking
+- Integrated with MkDocs site
+
+### 🌐 Website (integrated into `docs/`)
+- **docs/usage-metrics.md**: Markdown page for the dashboard
+- **docs/js/usage-metrics.js**: JavaScript for chart rendering
+- **docs/css/usage-metrics.css**: Styles for the dashboard
+- Uses Chart.js for visualizations
+- Shows trends, version comparisons, and statistics
+- Responsive design for mobile and desktop
+
+### 🤖 Automation (`.github/workflows/usage-metrics.yml`)
+- Runs monthly on the 1st at 9 AM UTC
+- Can be manually triggered with custom versions
+- Automatically queries all versions from v0.13.0 to latest
+- Creates pull requests for metrics updates (not direct commits)
+- Data is deployed via the main MkDocs site when PR is merged
+
+## Versions Tracked
+
+The system tracks all versions (including patch versions) from **v0.13.0** to the **latest release** (currently v0.40.0). This includes versions like v0.34.1, v0.29.1, etc.
+
+## Usage
+
+### Manual Collection
+
+To manually collect metrics for specific versions (queries run sequentially with automatic retry and backoff):
+
+```bash
+cd usage-metrics
+go run collect-metrics.go -version v0.37.0 -version v0.38.0 -version v0.39.0 -csv ../docs/usage-metrics.csv
+```
+
+The collection script includes automatic retry with exponential backoff (5s, 10s, 20s, 40s, 60s) for rate limit resilience. For example, to test with a few recent versions:
+
+```bash
+cd usage-metrics
+go run collect-metrics.go -version v0.38.0 -version v0.39.0 -version v0.40.0 -csv ../docs/usage-metrics.csv
+```
+
+### Running Locally
+
+To view the dashboard locally with the full MkDocs site:
+
+```bash
+# Serve the docs
+make serve-docs
+
+# Open http://localhost:8000/usage-metrics/
+```
+
+### Manual Workflow Trigger
+
+You can manually trigger the collection workflow from GitHub:
+
+1. Go to Actions → "Update Usage Metrics"
+2. Click "Run workflow"
+3. Optionally specify versions (e.g., `v0.39.0,v0.38.0`) or leave empty for all versions
+4. Click "Run workflow"
+
+## Data Format
+
+The CSV file has three columns:
+
+- **date**: Collection date in YYYY-MM-DD format
+- **version**: Version string (e.g., v0.27.0)
+- **count**: Number of repositories using this version
+
+Example:
+```csv
+date,version,count
+2024-01-15,v0.27.0,133
+2024-02-15,v0.27.0,145
+```
+
+## Viewing the Dashboard
+
+The dashboard is integrated into the main documentation site:
+- **Production**: https://golang.testcontainers.org/usage-metrics/
+- **Local**: http://localhost:8000/usage-metrics/ (when running `make serve-docs`)
+
+The dashboard displays:
+- Total repositories using testcontainers-go
+- Number of versions tracked
+- Latest version information
+- Usage trends over time (line chart)
+- Version comparison (bar chart)
+- Distribution by version (doughnut chart)
+
+## Rate Limiting
+
+GitHub API rate limits:
+- **Unauthenticated**: 10 requests/minute
+- **Authenticated**: 30 requests/minute
+
+The collection script queries versions sequentially with automatic retry and exponential backoff (5s, 10s, 20s, 40s, 60s) to handle rate limit errors gracefully. The script will automatically retry up to 5 times if it encounters rate limiting.
+
+## Customization
+
+### Changing Collection Frequency
+
+Edit the cron schedule in the workflow:
+
+```yaml
+schedule:
+ - cron: '0 9 1 * *' # Monthly on the 1st at 9 AM UTC
+```
+
+### Customizing Charts
+
+Edit `docs/js/usage-metrics.js` to modify chart types, colors, or add new visualizations.
+
+### Changing Version Range
+
+By default, the workflow queries all versions from v0.13.0 onwards. To change this, modify the awk pattern in the workflow file.
+
+## Architecture Decisions
+
+### Why CSV?
+- Simple and human-readable
+- Version-controlled with Git
+- Easy to import/export
+- No database required
+- Suitable for the data volume
+
+### Why Integrate with MkDocs?
+- Single documentation site for all content
+- Consistent look and feel
+- Same deployment pipeline
+- Easy maintenance
+- No separate hosting needed
+
+### Why Go for Collection?
+- Native GitHub API support
+- Easy to integrate with existing Go project
+- Simple deployment
+- Good CSV handling
+
+## Troubleshooting
+
+### API Rate Limiting
+If you hit rate limits:
+1. The collection script includes automatic retry with exponential backoff (5s, 10s, 20s, 40s, 60s up to 5 attempts)
+2. Queries run sequentially to minimize rate limit issues
+3. The workflow uses the `gh` CLI which automatically uses GitHub's token for higher limits
+
+### CSV Not Updating
+Check the workflow logs:
+1. Go to Actions → "Update Usage Metrics"
+2. Click on the latest run
+3. Review the "Query versions" step
+
+### Charts Not Displaying
+1. Ensure CSV file is properly formatted
+2. Check browser console for JavaScript errors
+3. Verify the file paths are correct
+4. Make sure Chart.js and PapaParse CDN links are accessible
+
+## Contributing
+
+To add features or fix issues:
+
+1. Test changes locally with `mkdocs serve`
+2. Update this README if needed
+3. Submit a pull request
+
+## License
+
+Same as the main testcontainers-go repository (MIT).
diff --git a/usage-metrics/collect-metrics.go b/usage-metrics/collect-metrics.go
new file mode 100644
index 0000000000..4e4e511367
--- /dev/null
+++ b/usage-metrics/collect-metrics.go
@@ -0,0 +1,204 @@
+package main
+
+import (
+ "encoding/csv"
+ "encoding/json"
+ "errors"
+ "flag"
+ "fmt"
+ "log"
+ "net/url"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+)
+
+type searchResponse struct {
+ TotalCount int `json:"total_count"`
+}
+
+type usageMetric struct {
+ Date string
+ Version string
+ Count int
+}
+
+type arrayFlags []string
+
+func (a *arrayFlags) String() string {
+ return strings.Join(*a, ",")
+}
+
+func (a *arrayFlags) Set(value string) error {
+ *a = append(*a, value)
+ return nil
+}
+
+func main() {
+ var versions arrayFlags
+ csvPath := flag.String("csv", "../../docs/usage-metrics.csv", "Path to CSV file")
+ flag.Var(&versions, "version", "Version to query (can be specified multiple times)")
+ flag.Parse()
+
+ if len(versions) == 0 {
+ log.Fatal("At least one version is required. Use -version flag (can be repeated)")
+ }
+
+ if err := collectMetrics(versions, *csvPath); err != nil {
+ log.Fatalf("Failed to collect metrics: %v", err)
+ }
+}
+
+func collectMetrics(versions []string, csvPath string) error {
+ date := time.Now().Format("2006-01-02")
+ metrics := make([]usageMetric, 0, len(versions))
+
+ // Query all versions sequentially
+ for _, version := range versions {
+ version = strings.TrimSpace(version)
+ if version == "" {
+ continue
+ }
+
+ count, err := queryGitHubUsageWithRetry(version)
+ if err != nil {
+ log.Printf("Warning: Failed to query version %s after retries: %v", version, err)
+ continue
+ }
+
+ metric := usageMetric{
+ Date: date,
+ Version: version,
+ Count: count,
+ }
+
+ metrics = append(metrics, metric)
+ fmt.Printf("Successfully queried: %s has %d usages on %s\n", version, count, metric.Date)
+ }
+
+ // Sort metrics by version
+ sort.Slice(metrics, func(i, j int) bool {
+ return metrics[i].Version < metrics[j].Version
+ })
+
+ // Write all metrics to CSV
+ for _, metric := range metrics {
+ if err := appendToCSV(csvPath, metric); err != nil {
+ log.Printf("Warning: Failed to write metric for %s: %v", metric.Version, err)
+ continue
+ }
+ fmt.Printf("Successfully recorded: %s has %d usages on %s\n", metric.Version, metric.Count, metric.Date)
+ }
+
+ return nil
+}
+
+func queryGitHubUsageWithRetry(version string) (int, error) {
+ var lastErr error
+ // Backoff intervals: 5s, 10s, 20s, 40s, 60s
+ backoffIntervals := []time.Duration{
+ 5 * time.Second,
+ 10 * time.Second,
+ 20 * time.Second,
+ 40 * time.Second,
+ 60 * time.Second,
+ }
+
+ // maxRetries includes the initial attempt plus one retry per backoff interval
+ maxRetries := len(backoffIntervals) + 1
+
+ for attempt := 0; attempt < maxRetries; attempt++ {
+ if attempt > 0 {
+ // Use predefined backoff intervals
+ waitTime := backoffIntervals[attempt-1]
+ log.Printf("Retrying version %s in %v (attempt %d/%d)", version, waitTime, attempt+1, maxRetries)
+ time.Sleep(waitTime)
+ }
+
+ count, err := queryGitHubUsage(version)
+ if err == nil {
+ return count, nil
+ }
+
+ lastErr = err
+
+ // Check if it's a rate limit error
+ if strings.Contains(err.Error(), "rate limit") || strings.Contains(err.Error(), "403") {
+ log.Printf("Rate limit hit for version %s, will retry with backoff", version)
+ continue
+ }
+
+ // For non-rate-limit errors, retry but with shorter backoff
+ log.Printf("Error querying version %s: %v", version, err)
+ }
+
+ return 0, fmt.Errorf("max retries reached: %w", lastErr)
+}
+
+func queryGitHubUsage(version string) (int, error) {
+ query := fmt.Sprintf(`"testcontainers/testcontainers-go %s" filename:go.mod -is:fork -org:testcontainers`, version)
+
+ params := url.Values{}
+ params.Add("q", query)
+ endpoint := "/search/code?" + params.Encode()
+
+ output, err := exec.Command("gh", "api",
+ "-H", "Accept: application/vnd.github+json",
+ "-H", "X-GitHub-Api-Version: 2022-11-28",
+ endpoint,
+ ).Output()
+ if err != nil {
+ exitErr := &exec.ExitError{}
+ if errors.As(err, &exitErr) {
+ return 0, fmt.Errorf("gh api failed: %s", string(exitErr.Stderr))
+ }
+ return 0, fmt.Errorf("gh api: %w", err)
+ }
+
+ var resp searchResponse
+ if err := json.Unmarshal(output, &resp); err != nil {
+ return 0, fmt.Errorf("unmarshal: %w", err)
+ }
+
+ return resp.TotalCount, nil
+}
+
+func appendToCSV(csvPath string, metric usageMetric) error {
+ absPath, err := filepath.Abs(csvPath)
+ if err != nil {
+ return fmt.Errorf("resolve path: %w", err)
+ }
+
+ _, err = os.Stat(absPath)
+ fileExists := !os.IsNotExist(err)
+
+ file, err := os.OpenFile(absPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
+ if err != nil {
+ return fmt.Errorf("open file: %w", err)
+ }
+ defer file.Close()
+
+ writer := csv.NewWriter(file)
+
+ if !fileExists {
+ if err := writer.Write([]string{"date", "version", "count"}); err != nil {
+ return fmt.Errorf("write header: %w", err)
+ }
+ }
+
+ record := []string{metric.Date, metric.Version, strconv.Itoa(metric.Count)}
+ if err := writer.Write(record); err != nil {
+ return fmt.Errorf("write record: %w", err)
+ }
+
+ writer.Flush()
+ if err := writer.Error(); err != nil {
+ return fmt.Errorf("flush csv: %w", err)
+ }
+
+ return nil
+}
diff --git a/usage-metrics/go.mod b/usage-metrics/go.mod
new file mode 100644
index 0000000000..16436e89f6
--- /dev/null
+++ b/usage-metrics/go.mod
@@ -0,0 +1,5 @@
+module github.com/testcontainers/testcontainers-go/usage-metrics
+
+go 1.24
+
+toolchain go1.24.7