DEV Community

Cover image for Building an Automated, API-Driven Stats & Version Synchronizer for VS Code Extensions with 24-Hour Smart Caching (dotUniverse v1.2.0)
freerave
freerave

Posted on

Building an Automated, API-Driven Stats & Version Synchronizer for VS Code Extensions with 24-Hour Smart Caching (dotUniverse v1.2.0)

How I built a dual-path synchronization architecture for 7 VS Code extensions using VS Marketplace & Open VSX APIs, 24-hour client caching, and daily zero-dependency GitHub Actions.

As an open-source creator building a growing ecosystem of developer tools, one repetitive chore kept draining my time: keeping metrics and version numbers in sync.

My portfolio, dotUniverse, showcases 20+ open-source toolsβ€”including 7 VS Code extensions published across both the Visual Studio Marketplace and the Eclipse Open VSX Registry.

Every time I bumped a release or checked how many developers installed CodeTune, dotcommand, or DotShare, I had to:

  1. Log into multiple web dashboards.
  2. Manually calculate cross-platform sums.
  3. Edit HTML cards, version badges, and repository README tables by hand.

Manual upkeep is tedious, error-prone, and quickly falls behind. I wanted an automated, dual-path synchronization architecture:

  • At Runtime: Visitors see fresh, API-backed numbers and versions fetched directly from store APIs, cached intelligently on the client for 24 hours.
  • At Build/Repository Time: Static fallbacks in index.html and markdown tables in README.md are automatically kept fresh via a scheduled, zero-dependency GitHub Action.

Here is a deep dive into how I built this system using pure modern JavaScript, 24-hour client-side caching, and GitHub Actions CI/CD.


πŸ›οΈ The Architecture: Dual-Path Synchronization

Instead of treating this as a simple "fetch data in JavaScript" problem, I designed a dual-path synchronization pipeline sharing a single source of truth:

                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚ Visual Studio Marketplace    β”‚
                  β”‚   & Open VSX Registry APIs   β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                 β”‚
             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
             β”‚                                       β”‚
      [Runtime Path]                           [Build/CI Path]
             β”‚                                       β”‚
             β–Ό                                       β–Ό
  extension-stats.js                     sync-extension-stats.mjs
             β”‚                                       β”‚
      β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”                         β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”
      β–Ό             β–Ό                         β–Ό             β–Ό
  24h Cache     Fresh DOM                 index.html    README.md
 (localStorage) (No Flicker)              (Fallback)    (Markdown)
                                                     β”‚
                                                     β–Ό
                                              Git Commit & Push
                                              (Only on Changes)
Enter fullscreen mode Exit fullscreen mode

This decoupled design guarantees that:

  1. First-time and repeat visitors get an instant render from static markup or localStorage without waiting for API responses to render the initial content.
  2. Search engine crawlers and developers browsing the GitHub repository see accurate static numbers even without executing client-side JavaScript.

πŸ—οΈ The Challenge: Two Stores, Fragmented Metrics

Tracking extensions across the VS Code ecosystem is fragmented:

  • VS Code Marketplace: Owned by Microsoft. Its web interface displays a single "Installs" counter, but its underlying gallery API exposes separate metrics.
  • Open VSX Registry: The vendor-neutral marketplace used by VSCodium, Gitpod, and Eclipse Theia. It provides a clean REST API with different metric structures.

Crucially, both APIs support CORS with access-control-allow-origin: *, allowing modern browsers to query them directly without requiring a proxy server.


πŸ” Layer 1: Querying Store APIs Directly

1. Batch-Querying Visual Studio Marketplace

Instead of making 7 separate HTTP requests, Microsoft's Marketplace API allows querying multiple extensions in a single POST request to _apis/public/gallery/extensionquery:

async function fetchVsMarketplace(extensions) {
  const body = {
    filters: [{
      criteria: extensions.map(ext => ({ filterType: 7, value: ext.vsId }))
    }],
    flags: 914 // Requests statistics, versions, and metadata
  };

  const res = await fetch('https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json;api-version=3.0-preview.1'
    },
    body: JSON.stringify(body)
  });

  const json = await res.json();
  const map = {};

  for (const ext of json?.results?.[0]?.extensions || []) {
    const stats = {};
    for (const s of ext.statistics || []) {
      stats[s.statisticName] = s.value;
    }

    // Empirical verification: Microsoft Marketplace UI displays (install + downloadCount)
    const installs = Math.round((stats.install || 0) + (stats.downloadCount || 0));

    // The API returns the latest release first in versions[0]
    const version = ext.versions?.[0]?.version || null;

    map[ext.extensionName.toLowerCase()] = { installs, version };
  }

  return map;
}
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Empirical Verification on Marketplace Installs: Microsoft's API returns two separate download metrics: install (active installed instances) and downloadCount (package downloads/updates). Through empirical cross-referencing between the API response and the live web pages for all 7 extensions, the "Installs" figure shown on the Marketplace product page aligned with (stats.install + stats.downloadCount). This was observed across the seven extensions tested during development; it should be treated as an empirical observation rather than a guaranteed API contract.

2. Querying Open VSX Registry

Open VSX provides an open REST endpoint at https://open-vsx.org/api/{namespace}/{extension}:

async function fetchOpenVsx(extensions) {
  const map = {};
  const ovsxExts = extensions.filter(e => e.ovsxId);

  const promises = ovsxExts.map(async (ext) => {
    try {
      const res = await fetch(`https://open-vsx.org/api/${ext.ovsxId}`);
      if (!res.ok) return;
      const data = await res.json();
      map[ext.key] = {
        downloads: data.downloadCount || 0,
        version: data.version || null
      };
    } catch (err) {
      console.warn(`Open VSX fetch failed for ${ext.key}:`, err);
    }
  });

  await Promise.allSettled(promises);
  return map;
}
Enter fullscreen mode Exit fullscreen mode

⚑ Layer 2: The Client-Side Module (extension-stats.js)

In dotUniverse's modular architecture, we created js/modules/extension-stats.js.

Why a 24-Hour Cache is Essential

Firing external API requests on every page refresh is bad engineeringβ€”it wastes bandwidth, risks third-party rate limits, and causes text flicker.

Because developer tools are typically released over days or weeks, a 24-hour cache TTL (24 * 60 * 60 * 1000) is a practical default: it reduces unnecessary API requests while keeping the displayed statistics reasonably fresh:

export const ExtensionStats = {
  CACHE_KEY: 'dotuniverse_ext_stats_v1',
  CACHE_TTL: 24 * 60 * 60 * 1000, // 24 hours (1 day)

  init() {
    // 1. Instantly apply cached data to DOM (avoids visible flicker)
    const cached = this.getCached();
    if (cached && cached.data) {
      this.applyToDOM(cached.data);
    }

    // 2. Fetch in background only when the 24-hour window expires
    if (!cached || Date.now() - cached.timestamp > this.CACHE_TTL) {
      this.refresh();
    }
  },
  // ...
};
Enter fullscreen mode Exit fullscreen mode

Clean DOM Mutation via Semantic Attributes

In index.html, each extension card includes a semantic data-ext-name identifier:

<div class="ext-card" data-ext-name="codetune">
  <div class="ext-header">
    <div class="ext-name"><span class="prefix">Code</span><span class="suffix">Tune</span></div>
    <span class="ext-version">v1.3.0</span>
  </div>
  ...
  <div class="ext-stores">
    <a ... class="store-badge vs">
      <span class="store-info"><span class="store-name">VS Marketplace</span><span class="store-dl">737 installs</span></span>
    </a>
    <a ... class="store-badge ovsx">
      <span class="store-info"><span class="store-name">Open VSX</span><span class="store-dl">1,500 downloads</span></span>
    </a>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

When fresh metrics arrive:

  1. .ext-version updates to the store's current version (e.g. v1.3.0, v2.2.1, v3.5.1).
  2. Store badges update with formatted counts (e.g., 1,040 installs, 6,000 downloads).
  3. The grand total (19,500+) is calculated and smoothly updated in both the header stats and the hero banner.

πŸ€– Layer 3: Automated Daily CI/CD Synchronization

To keep the repository README.md and static HTML in sync for users without JavaScript or search bots, we created a zero-dependency Node script: scripts/sync-extension-stats.mjs.

Running this script fetches live data, parses index.html and README.md, updates markdown tables, Shields.io badges, and prints a formatted terminal overview:

$ node scripts/sync-extension-stats.mjs

πŸ”„ Fetching live extension stats from VS Marketplace and Open VSX...

πŸ“Š Live Extension Stats:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ (index) β”‚ Extension    β”‚ Version  β”‚ VS Marketplace β”‚ Open VSX β”‚ Total Downloads β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 0       β”‚ 'CodeTune'   β”‚ 'v1.3.0' β”‚ '734'          β”‚ '1,486'  β”‚ '2,220'         β”‚
β”‚ 1       β”‚ 'dotcommand' β”‚ 'v2.0.1' β”‚ '778'          β”‚ '970'    β”‚ '1,748'         β”‚
β”‚ 2       β”‚ 'DotEnvy'    β”‚ 'v2.2.1' β”‚ '758'          β”‚ '2,173'  β”‚ '2,931'         β”‚
β”‚ 3       β”‚ 'DotShare'   β”‚ 'v3.5.1' β”‚ '1,040'        β”‚ '5,987'  β”‚ '7,027'         β”‚
β”‚ 4       β”‚ 'DotFetch'   β”‚ 'v2.1.1' β”‚ '470'          β”‚ '2,766'  β”‚ '3,236'         β”‚
β”‚ 5       β”‚ 'DotReadme'  β”‚ 'v1.2.0' β”‚ '364'          β”‚ '1,719'  β”‚ '2,083'         β”‚
β”‚ 6       β”‚ 'DotSense'   β”‚ 'v1.3.0' β”‚ '257'          β”‚ '0'      β”‚ '257'           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ† Grand Total Downloads: 19,502 (Marketplace: 4,401, Open VSX: 15,101)

βœ… index.html updated successfully.
βœ… README.md updated successfully.
Enter fullscreen mode Exit fullscreen mode

To run this hands-free, we added .github/workflows/update-extension-stats.yml:

name: Auto Update Extension Stats

on:
  schedule:
    # Runs everyday at midnight UTC
    - cron: '0 0 * * *'
  workflow_dispatch: # Allows manual trigger from GitHub UI

permissions:
  contents: write

jobs:
  sync-stats:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Sync extension downloads and versions
        run: node scripts/sync-extension-stats.mjs

      - name: Commit and push changes if updated
        run: |
          git config --global user.name "github-actions[bot]"
          git config --global user.email "github-actions[bot]@users.noreply.github.com"
          git add index.html README.md
          if git diff --staged --quiet; then
            echo "No changes detected."
          else
            git commit -m "chore: auto-update extension downloads and versions [skip ci]"
            git push
          fi
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Clean Commit History: The git diff --staged --quiet check ensures that GitHub Actions only commits when marketplace metrics or versions actually change. This keeps the Git history clean from empty daily noise.


πŸš€ The Bigger Picture: Building in Public

Automating tool statistics ties directly into how I build in public. Alongside extensions, dotUniverse features an open Growth Challenge Tracker across 9 creator and developer platforms.

Here is the current state for October 2026:

Platform Current (Now) Target (Goal) Remaining Progress
πŸš€ dev.to 3,986 4,200 +214 95%
πŸ’Ό LinkedIn 709 800 +91 89%
🎡 TikTok 37 60 +23 62%
▢️ YouTube 30 60 +30 50%
✍️ Medium 6 10 +4 60%
πŸ“˜ Facebook 15 20 +5 75%
πŸ“Έ Instagram 7 10 +3 70%
𝕏 X / Twitter 18 20 +2 90%
πŸ¦‹ Bluesky 21 30 +9 70%

On dev.to specifically, we recently passed 15,400+ total views, 93 published posts, 97 reactions, and 67 comments.

Because dev.to is currently at 3,986 / 4,200, we explicitly commented out the celebratory 15-DAY RECORD badge in the HTML until the 4,200 mark is officially crossed. Milestones are meaningful only when they are genuine!


πŸ’‘ Key Engineering Takeaways

  1. Leverage Dual-Path Synchronization: Combining client-side background sync with scheduled CI/CD ensures that both web visitors and static repository viewers get consistently updated extension statistics.
  2. Explore Public API Capabilities: Both the Visual Studio Marketplace and Open VSX support CORS directly. Before introducing a custom serverless proxy, inspect the network headers first.
  3. Respect the Client Cache: A 24-hour localStorage TTL reduces visible flickering, avoids API-dependent initial rendering on repeat visits, respects third-party rate limits, and matches the realistic release frequency of developer tools.
  4. Conditional CI/CD Commits: Always check git diff --staged --quiet before committing automated bot changes to keep your repository history concise and meaningful.

πŸ”— Project Links

How do you automate metrics across your open-source repositories? Do you rely on static shields or live client-side queries? Let's discuss in the comments below!

Top comments (0)