Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/fetch-telemetry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Fetch Telemetry Data

on:
schedule:
# Run daily at 08:00 UTC (00:00 Pacific during PST, 01:00 during PDT)
- cron: '0 8 * * *'
workflow_dispatch:

permissions:
contents: write

jobs:
fetch-telemetry:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Fetch telemetry overview
run: |
HTTP_STATUS=$(curl -s -o /tmp/overview.json -w "%{http_code}" \
--max-time 30 \
https://telemetry.cozystack.io/api/overview)

if [ "$HTTP_STATUS" -ne 200 ]; then
echo "Telemetry API returned HTTP $HTTP_STATUS, skipping update"
exit 0
fi

# Validate JSON structure (must have generated_at and periods keys)
if ! python3 -c "
import json, sys
d = json.load(open('/tmp/overview.json'))
assert 'generated_at' in d, 'missing generated_at'
assert 'periods' in d and isinstance(d['periods'], dict), 'missing or invalid periods'
"; then
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
echo "Invalid JSON structure, skipping update"
exit 0
fi

cp /tmp/overview.json data/usage-stats/overview.json

- name: Check for changes
id: changes
run: |
if git diff --quiet data/usage-stats/overview.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Commit and push
if: steps.changes.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add data/usage-stats/overview.json
git commit -m "Update telemetry data"
git push
90 changes: 90 additions & 0 deletions assets/scss/_telemetry.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* telemetry page */

.telemetry-page {
margin-top: 4rem;

@include media-breakpoint-down(sm) {
margin-top: 2rem;
}

.nav-tabs {
border-bottom: 2px solid $primary;

.nav-link {
color: $cozy-mid-gray;
font-weight: 600;
border: none;
border-bottom: 3px solid transparent;
padding: 0.75rem 1.5rem;

&:hover {
color: $primary;
border-bottom-color: rgba($primary, 0.3);
}

&.active {
color: $primary;
border-bottom-color: $primary;
background: transparent;
}
}
}

.telemetry-card {
border: none;
border-radius: 0.75rem;
transition: transform 0.15s ease;

&:hover {
transform: translateY(-2px);
}

.telemetry-icon {
font-size: 1.75rem;
color: $primary;
margin-bottom: 0.5rem;
}

.telemetry-value {
font-size: 2.5rem;
font-weight: 700;
color: $cozy-black;
line-height: 1.2;
}

.telemetry-label {
font-size: 0.95rem;
font-weight: 600;
color: $cozy-mid-gray;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-top: 0.25rem;
}

.telemetry-secondary {
font-size: 0.85rem;
color: $cozy-light-gray;
margin-top: 0.25rem;
}
}

.table {
th {
font-weight: 600;
}

.table-primary {
--bs-table-bg: #{rgba($primary, 0.08)};
--bs-table-border-color: #{rgba($primary, 0.15)};
color: $cozy-black;
}

code {
color: $primary;
font-weight: 500;
background: rgba($primary, 0.06);
padding: 0.15rem 0.4rem;
border-radius: 0.25rem;
}
}
}
1 change: 1 addition & 0 deletions assets/scss/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,4 @@ a {
@import "announcement-banner";
@import "tabs_alerts";
@import "override-docsy-tabs";
@import "telemetry";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Move the telemetry import above all style rules.

Line 152 places @import "telemetry"; after rule blocks, which violates the current Stylelint rule (no-invalid-position-at-import-rule) and will keep lint failing.

♻️ Proposed fix
 // Import Docsy styles_project (allows further customization)
 `@import` "docsy/styles_project";
+@import "telemetry";
@@
 `@import` "tabs_alerts";
 `@import` "override-docsy-tabs";
-@import "telemetry";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@import "telemetry";
// Import Docsy styles_project (allows further customization)
`@import` "docsy/styles_project";
`@import` "telemetry";
`@import` "tabs_alerts";
`@import` "override-docsy-tabs";
🧰 Tools
🪛 Stylelint (17.6.0)

[error] 152-152: Unexpected invalid position @import rule (no-invalid-position-at-import-rule)

(no-invalid-position-at-import-rule)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/scss/main.scss` at line 152, The `@import` "telemetry"; statement is
placed after style rule blocks causing a Stylelint
no-invalid-position-at-import-rule failure; move the import so it appears before
any CSS/SCSS rule definitions (i.e., place `@import` "telemetry"; at the very top
of main.scss, above all selectors, variables, and mixins) to satisfy the linter.

5 changes: 5 additions & 0 deletions content/en/oss-health/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: OSS Health
description: Open source project health metrics for Cozystack
type: oss-health
---
6 changes: 6 additions & 0 deletions content/en/oss-health/telemetry/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Telemetry
description: Anonymous usage statistics collected from Cozystack clusters
type: oss-health
layout: telemetry
---
4 changes: 4 additions & 0 deletions data/usage-stats/overview.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"generated_at": "",
"periods": {}
}
7 changes: 7 additions & 0 deletions hugo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ params:

menus:
main:
- name: OSS Health
weight: 3
identifier: oss-health
- name: Telemetry
parent: oss-health
url: /oss-health/telemetry/
weight: 1
- name: Enterprise support
url: /support
weight: 5
Expand Down
18 changes: 18 additions & 0 deletions layouts/oss-health/baseof.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="{{ .Site.Language.Lang }}" class="no-js">
<head>
{{ partial "head.html" . }}
</head>
<body class="td-{{ .Kind }}">
<header>
{{ partial "navbar.html" . }}
</header>
<div class="container-fluid td-outer">
<div class="td-main">
{{ block "main" . }}{{ end }}
</div>
{{ partial "footer.html" . }}
</div>
{{ partial "scripts.html" . }}
</body>
</html>
132 changes: 132 additions & 0 deletions layouts/oss-health/telemetry.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
{{ define "main" }}
{{ $data := index (index .Site.Data "usage-stats") "overview" }}

<div class="telemetry-page container py-5">
<div class="text-center mb-5">
<h1 class="display-5 fw-bold">Telemetry</h1>
<p class="lead text-muted">Anonymous usage statistics collected from Cozystack clusters worldwide.</p>
</div>

{{ if and $data $data.periods (gt (len $data.periods) 0) }}

{{ $tabIds := slice "month" "quarter" "year" }}
{{ $tabLabels := dict "month" "Last Month" "quarter" "Last Quarter" "year" "Last 12 Months" }}
{{ $tabIcons := dict "month" "fa-calendar-day" "quarter" "fa-calendar-week" "year" "fa-calendar-alt" }}

<!-- Tabs -->
{{ $firstTab := "" }}
{{ range $id := $tabIds }}
{{ if and (eq $firstTab "") (index $data.periods $id) }}
{{ $firstTab = $id }}
{{ end }}
{{ end }}

<ul class="nav nav-tabs justify-content-center mb-4" id="telemetryTabs" role="tablist">
{{ range $id := $tabIds }}
{{ if index $data.periods $id }}
<li class="nav-item" role="presentation">
<button class="nav-link {{ if eq $id $firstTab }}active{{ end }}" id="{{ $id }}-tab" data-bs-toggle="tab" data-bs-target="#{{ $id }}-pane" type="button" role="tab" aria-controls="{{ $id }}-pane" aria-selected="{{ if eq $id $firstTab }}true{{ else }}false{{ end }}">
<i class="fas {{ index $tabIcons $id }} me-1"></i> {{ index $tabLabels $id }}
</button>
</li>
{{ end }}
{{ end }}
</ul>

<!-- Tab content -->
<div class="tab-content" id="telemetryTabContent">

{{ range $id := $tabIds }}
{{ $period := index $data.periods $id }}
{{ if $period }}
<div class="tab-pane fade {{ if eq $id $firstTab }}show active{{ end }}" id="{{ $id }}-pane" role="tabpanel" aria-labelledby="{{ $id }}-tab">
<h4 class="text-center text-muted mb-4">{{ $period.label }}</h4>

<!-- Summary cards -->
<div class="row g-3 mb-5">
<div class="col-md-4 col-sm-6">
<div class="card text-center h-100 shadow-sm telemetry-card">
<div class="card-body">
<div class="telemetry-icon"><i class="fas fa-server"></i></div>
<div class="telemetry-value">{{ $period.clusters }}</div>
<div class="telemetry-label">Clusters</div>
</div>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="card text-center h-100 shadow-sm telemetry-card">
<div class="card-body">
<div class="telemetry-icon"><i class="fas fa-microchip"></i></div>
<div class="telemetry-value">{{ $period.total_nodes }}</div>
<div class="telemetry-label">Total Nodes</div>
<div class="telemetry-secondary">avg {{ printf "%.1f" $period.avg_nodes_per_cluster }} per cluster</div>
</div>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="card text-center h-100 shadow-sm telemetry-card">
<div class="card-body">
<div class="telemetry-icon"><i class="fas fa-users"></i></div>
<div class="telemetry-value">{{ $period.total_tenants }}</div>
<div class="telemetry-label">Tenants</div>
<div class="telemetry-secondary">avg {{ printf "%.1f" $period.avg_tenants_per_cluster }} per cluster</div>
</div>
</div>
</div>
</div>

<!-- Application usage table -->
{{ if $period.apps }}
<h5 class="mb-3">Application Usage</h5>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-primary">
<tr>
<th scope="col">Application</th>
<th scope="col" class="text-end">Instances</th>
</tr>
</thead>
<tbody>
{{ range $app, $count := $period.apps }}
<tr>
<td><code>{{ $app }}</code></td>
<td class="text-end">{{ $count }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ end }}
</div>
{{ end }}
{{ end }}

</div>

{{ if $data.generated_at }}
<p class="text-center text-muted mt-4">
<small>Data last updated: {{ $data.generated_at }}</small>
</p>
{{ end }}

{{ else }}

<div class="text-center py-5">
<div class="mb-3"><i class="fas fa-chart-bar fa-3x text-muted"></i></div>
<h4 class="text-muted">No telemetry data available yet</h4>
<p class="text-muted">Statistics will appear here after the first monthly collection cycle.</p>
</div>

{{ end }}

<div class="text-center mt-5 mb-3">
<p class="text-muted">
<small>
Cozystack collects anonymous, aggregate telemetry to understand usage patterns.
<a href="{{ "docs/telemetry/" | relURL }}">Learn more about what is collected</a>.
</small>
</p>
</div>
</div>

{{ end }}