Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
54 changes: 54 additions & 0 deletions .github/workflows/fetch-telemetry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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
if ! python3 -c "import json; json.load(open('/tmp/overview.json'))"; then
echo "Invalid JSON response, skipping update"
exit 0
fi

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

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
- 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 @@ -12,3 +12,4 @@
@import "support";
@import "ecosystem";
@import "adopters_wall";
@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 @@ -172,6 +172,13 @@ params:

menus:
main:
- name: OSS Health
weight: 3
identifier: oss-health
- name: Telemetry
parent: oss-health
url: /oss-health/telemetry/
weight: 1
- name: Ænix
url: https://aenix.io
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>
136 changes: 136 additions & 0 deletions layouts/oss-health/telemetry.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
{{ 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) }}

<!-- Tabs -->
<ul class="nav nav-tabs justify-content-center mb-4" id="telemetryTabs" role="tablist">
{{ with index $data.periods "month" }}
<li class="nav-item" role="presentation">
<button class="nav-link active" id="month-tab" data-bs-toggle="tab" data-bs-target="#month-pane" type="button" role="tab" aria-controls="month-pane" aria-selected="true">
<i class="fas fa-calendar-day me-1"></i> Last Month
</button>
</li>
{{ end }}
{{ with index $data.periods "quarter" }}
<li class="nav-item" role="presentation">
<button class="nav-link" id="quarter-tab" data-bs-toggle="tab" data-bs-target="#quarter-pane" type="button" role="tab" aria-controls="quarter-pane" aria-selected="false">
<i class="fas fa-calendar-week me-1"></i> Last Quarter
</button>
</li>
{{ end }}
{{ with index $data.periods "year" }}
<li class="nav-item" role="presentation">
<button class="nav-link" id="year-tab" data-bs-toggle="tab" data-bs-target="#year-pane" type="button" role="tab" aria-controls="year-pane" aria-selected="false">
<i class="fas fa-calendar-alt me-1"></i> Last 12 Months
</button>
</li>
{{ end }}
</ul>
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.

medium

The current tab implementation hardcodes the active class on the 'Last Month' tab. If the 'month' data is missing from the JSON but other periods (quarter/year) are present, the first available pane will be shown (due to the logic on line 45), but no tab will be highlighted as active.

Refactoring the tabs to use a loop similar to the panes ensures that the active state remains synchronized and makes the code more maintainable.

  <ul class="nav nav-tabs justify-content-center mb-4" id="telemetryTabs" role="tablist">
    {{ $firstTab := true }}
    {{ $tabIds := slice "month" "quarter" "year" }}
    {{ $tabIcons := dict "month" "fa-calendar-day" "quarter" "fa-calendar-week" "year" "fa-calendar-alt" }}
    {{ $tabLabels := dict "month" "Last Month" "quarter" "Last Quarter" "year" "Last 12 Months" }}
    {{ range $id := $tabIds }}
      {{ with index $data.periods $id }}
      <li class="nav-item" role="presentation">
        <button class="nav-link {{ if $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 $firstTab }}true{{ else }}false{{ end }}">
          <i class="fas {{ index $tabIcons $id }} me-1"></i> {{ index $tabLabels $id }}
        </button>
      </li>
      {{ $firstTab = false }}
      {{ end }}
    {{ end }}
  </ul>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f699fc9 — tab activation is now dynamic: a $firstTab variable is computed from the first available period, and both button and pane use it consistently.


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

{{ $first := true }}
{{ $tabIds := slice "month" "quarter" "year" }}
{{ range $id := $tabIds }}
{{ $period := index $data.periods $id }}
{{ if $period }}
<div class="tab-pane fade {{ if $first }}show active{{ end }}" id="{{ $id }}-pane" role="tabpanel" aria-labelledby="{{ $id }}-tab">
<h4 class="text-center text-muted mb-4">{{ $period.label }}</h4>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

<!-- 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 {{ $period.avg_nodes_per_cluster }} per cluster</div>
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.

medium

Average values calculated from telemetry data might contain many decimal places. It is recommended to format these values to a single decimal place for a cleaner user interface.

Suggested change
<div class="telemetry-secondary">avg {{ $period.avg_nodes_per_cluster }} per cluster</div>
<div class="telemetry-secondary">avg {{ printf "%.1f" $period.avg_nodes_per_cluster }} per cluster</div>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f699fc9 — now uses printf "%.1f" for consistent display.

</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 {{ $period.avg_tenants_per_cluster }} per cluster</div>
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.

medium

Similar to the nodes average, the tenants average should be formatted to avoid displaying excessive decimal places.

Suggested change
<div class="telemetry-secondary">avg {{ $period.avg_tenants_per_cluster }} per cluster</div>
<div class="telemetry-secondary">avg {{ printf "%.1f" $period.avg_tenants_per_cluster }} per cluster</div>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f699fc9 — same printf "%.1f" applied.

</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>
{{ $first = false }}
{{ 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="https://cozystack.io/docs/telemetry/">Learn more about what is collected</a>.
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.

medium

Using a hardcoded absolute URL for internal documentation links can break in preview environments or if the domain changes. It is better to use the relURL function to ensure the link is always relative to the site root.

Suggested change
<a href="https://cozystack.io/docs/telemetry/">Learn more about what is collected</a>.
<a href="{{ "docs/telemetry/" | relURL }}">Learn more about what is collected</a>.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f699fc9 — now uses {{ "docs/telemetry/" | relURL }}.

</small>
</p>
</div>
</div>

{{ end }}