From 1fe707177f926c73b0f964b6bbae15ee4bfaf3bc Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Mon, 2 Nov 2026 13:39:57 -0600 Subject: [PATCH 1/2] Read the latest view counts from the staged stats The daily workflow keeps a fresh resource-stats.json on automation/traffic-stats-staging, but publishing it to master takes a weekly pull request that Actions cannot open under the org's policy, so the site's counts were only as fresh as the last merged weekly PR. The catalog and the resource pages now also read the staged file from raw.githubusercontent.com (assets/live-stats.js) and take only its view counts. The committed file stays the source for everything else and the fallback whenever the staged file cannot be read in 1.5 seconds, is older than the committed file, or was counted by a different method (sources.views). No token or PR is needed. - The privacy notice discloses the request (no cookies, no referrer). - Tests cover accept/reject rules, fetch failures, the URL matching the workflow's staging branch, and the wiring in the catalog and the resource pages. - traffic-data/README.md documents the behavior and what to change if the branch is renamed. - Regenerated the 58 resource pages for the new script tag. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- assets/live-stats.js | 71 +++++++++++++++++++ assets/privacy-notice.js | 1 + assets/resource-page.js | 16 +++-- index.html | 14 ++++ r/add-engagegroupadmins/index.html | 1 + r/agent-365-enablement-checklist/index.html | 1 + r/agent-365-lifecycle-atlas/index.html | 1 + r/agent-brainstorming-template/index.html | 1 + r/agents-cost-calculator/index.html | 1 + r/ai-council/index.html | 1 + r/analyze-sharepointrisk/index.html | 1 + r/autoreply-agent/index.html | 1 + r/comparedocs/index.html | 1 + r/copilot-agents-guide/index.html | 1 + .../index.html | 1 + r/copilot-interaction-report/index.html | 1 + r/copilot-studio-workflow/index.html | 1 + r/create-engagecommunities/index.html | 1 + r/deepresearch/index.html | 1 + r/delete-allcommunityposts/index.html | 1 + r/delete-engagegroups/index.html | 1 + r/delete-engageusers/index.html | 1 + r/disable-insightsheadspace/index.html | 1 + .../index.html | 1 + .../index.html | 1 + r/export-copilotinteractions/index.html | 1 + r/export-engagefiles/index.html | 1 + r/export-engagenetworkdata/index.html | 1 + r/export-m365copilotreports/index.html | 1 + r/export-simpleplacesvisual/index.html | 1 + r/find-mailboxdelegates/index.html | 1 + r/get-aadadminrolemembers/index.html | 1 + r/get-allinsightslicensedusers/index.html | 1 + r/get-engagegroupinfo/index.html | 1 + r/get-groupsmembersmanagers/index.html | 1 + r/get-licenseusage/index.html | 1 + r/get-m365copilotreadiness/index.html | 1 + r/get-recurringteamsmeetings/index.html | 1 + r/get-siteinventory/index.html | 1 + r/get-teamsuseractivityreport/index.html | 1 + r/get-yammersitesize/index.html | 1 + r/managersimulator/index.html | 1 + .../index.html | 1 + .../index.html | 1 + r/migrate-copilotstudioagents/index.html | 1 + r/omniagent/index.html | 1 + r/powerclaw-agent/index.html | 1 + r/preflight-onedrive/index.html | 1 + r/productquote-agent/index.html | 1 + r/purview-audit-copilot-report/index.html | 1 + r/reasoningagent/index.html | 1 + .../index.html | 1 + r/simplegraph/index.html | 1 + .../index.html | 1 + r/teams-phone-system-snippets/index.html | 1 + r/teams-phone-user-migration/index.html | 1 + r/teams-rooms-snippets/index.html | 1 + r/teams-upgrade-snippets/index.html | 1 + r/update-vivaengagelicensingtoggle/index.html | 1 + r/vibewriting-agent/index.html | 1 + .../index.html | 1 + r/viva-insights-copilot-dashboard/index.html | 1 + tools/catalog-build/site-pages.js | 1 + tools/catalog-build/test-site-output.js | 41 +++++++++++ traffic-data/README.md | 11 +++ 65 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 assets/live-stats.js diff --git a/assets/live-stats.js b/assets/live-stats.js new file mode 100644 index 00000000..573688e5 --- /dev/null +++ b/assets/live-stats.js @@ -0,0 +1,71 @@ +/* + * Latest view counts for the catalog and the resource pages. + * + * The daily traffic workflow keeps a fresh resource-stats.json on the automation/traffic-stats-staging + * branch. Publishing it to master needs a pull request that GitHub Actions cannot open here, so the + * copy on the site is only as fresh as the last merged weekly PR. This script reads the staging copy + * straight from raw.githubusercontent.com, which serves it to any site and caches it for five minutes. + * + * Only the view counts are taken from it. The local file stays the source for everything else + * (upvotes, discussion links), and it is the fallback when the staging copy cannot be read, is + * older than the local file, or was rendered by a different counting method (sources.views). + * Exposes window.FastTrackLiveStats = { url, fetch, pick }. It never throws and never blocks a page. + */ +(function () { + 'use strict'; + + var LIVE_STATS_URL = 'https://raw.githubusercontent.com/microsoft/FastTrack/automation/traffic-stats-staging/resource-stats.json'; + var TIMEOUT_MS = 5000; + + // Resolves with the parsed staging file, or null on any failure or timeout. + function fetchLive() { + return new Promise(function (resolve) { + try { + if (!window.fetch) { resolve(null); return; } + var controller = window.AbortController ? new AbortController() : null; + var timer = setTimeout(function () { + if (controller) controller.abort(); + resolve(null); + }, TIMEOUT_MS); + var options = { credentials: 'omit', referrerPolicy: 'no-referrer' }; + if (controller) options.signal = controller.signal; + window.fetch(LIVE_STATS_URL, options) + .then(function (response) { + if (!response.ok) throw new Error('HTTP ' + response.status); + return response.json(); + }) + .then(function (doc) { clearTimeout(timer); resolve(doc); }) + .catch(function () { clearTimeout(timer); resolve(null); }); + } catch (error) { + resolve(null); + } + }); + } + + // Returns { slug: views } when the staging copy can replace the local view counts, else null. + function pick(local, live) { + try { + if (!local || !live || !live.resources || typeof live.resources !== 'object') return null; + if (!local.sources || !live.sources || !local.sources.views || local.sources.views !== live.sources.views) return null; + var localTime = Date.parse(local.generatedAt); + var liveTime = Date.parse(live.generatedAt); + if (!(liveTime >= localTime)) return null; + + var views = {}; + var found = false; + Object.keys(live.resources).forEach(function (slug) { + var entry = live.resources[slug]; + var count = entry ? Number(entry.views) : NaN; + if (isFinite(count) && count >= 0) { + views[slug] = Math.round(count); + found = true; + } + }); + return found ? views : null; + } catch (error) { + return null; + } + } + + window.FastTrackLiveStats = { url: LIVE_STATS_URL, fetch: fetchLive, pick: pick }; +})(); diff --git a/assets/privacy-notice.js b/assets/privacy-notice.js index 163299e6..25267fa1 100644 --- a/assets/privacy-notice.js +++ b/assets/privacy-notice.js @@ -95,6 +95,7 @@ '', '', diff --git a/assets/resource-page.js b/assets/resource-page.js index 0f51c0ea..fd3554d5 100644 --- a/assets/resource-page.js +++ b/assets/resource-page.js @@ -53,17 +53,25 @@ } // Views come from the published stats, so the page needs no rebuild when the counts change. The row - // stays hidden if the file cannot be read. Keep VIEWS_TIP in step with the catalog (index.html). + // stays hidden if the file cannot be read. The latest counts are taken from the staged stats when + // they can be read in time (assets/live-stats.js). Keep VIEWS_TIP in step with the catalog (index.html). var VIEWS_TIP = "Catalog page views recorded by Microsoft Clarity since Jul 2026, plus views of the resource's GitHub folder from saved snapshots (Apr\u2013Jun and 14\u201327 Oct 2026). Some days were never captured, so this is a minimum."; + var LIVE_STATS_WAIT_MS = 1500; var viewsRow = document.getElementById('viewsRow'); var viewsValue = document.getElementById('viewsValue'); if (viewsRow && viewsValue && slug && script && script.src && window.fetch) { - fetch(new URL('../resource-stats.json', script.src).href, { cache: 'no-cache' }) - .then(function (response) { if (!response.ok) throw new Error('HTTP ' + response.status); return response.json(); }) - .then(function (data) { + var liveStats = window.FastTrackLiveStats ? window.FastTrackLiveStats.fetch() : Promise.resolve(null); + var liveOrTimeout = Promise.race([liveStats, new Promise(function (resolve) { setTimeout(function () { resolve(null); }, LIVE_STATS_WAIT_MS); })]); + var localStats = fetch(new URL('../resource-stats.json', script.src).href, { cache: 'no-cache' }) + .then(function (response) { if (!response.ok) throw new Error('HTTP ' + response.status); return response.json(); }); + Promise.all([localStats, liveOrTimeout]) + .then(function (results) { + var data = results[0]; if (!data || !data.resources || typeof data.resources !== 'object') return; var entry = data.resources[slug]; var views = entry ? Number(entry.views) : 0; + var fresh = window.FastTrackLiveStats ? window.FastTrackLiveStats.pick(data, results[1]) : null; + if (fresh && Object.prototype.hasOwnProperty.call(fresh, slug)) views = fresh[slug]; if (!isFinite(views) || views < 0) views = 0; var short = views >= 10000 ? (views / 1000).toFixed(0) + 'k' : views >= 1000 ? (views / 1000).toFixed(1) + 'k' : String(views); viewsValue.textContent = short; diff --git a/index.html b/index.html index 64c5ddae..6ed1756e 100644 --- a/index.html +++ b/index.html @@ -1641,6 +1641,7 @@

Related resources

+ + diff --git a/r/agent-365-enablement-checklist/index.html b/r/agent-365-enablement-checklist/index.html index 32f33840..91a2cb7c 100644 --- a/r/agent-365-enablement-checklist/index.html +++ b/r/agent-365-enablement-checklist/index.html @@ -103,6 +103,7 @@

Related resources

+ diff --git a/r/agent-365-lifecycle-atlas/index.html b/r/agent-365-lifecycle-atlas/index.html index c1d30f27..58ec8644 100644 --- a/r/agent-365-lifecycle-atlas/index.html +++ b/r/agent-365-lifecycle-atlas/index.html @@ -102,6 +102,7 @@

Related resources

+ diff --git a/r/agent-brainstorming-template/index.html b/r/agent-brainstorming-template/index.html index db3b44a4..202b6967 100644 --- a/r/agent-brainstorming-template/index.html +++ b/r/agent-brainstorming-template/index.html @@ -99,6 +99,7 @@

Related resources

+ diff --git a/r/agents-cost-calculator/index.html b/r/agents-cost-calculator/index.html index f279d5ba..57280fda 100644 --- a/r/agents-cost-calculator/index.html +++ b/r/agents-cost-calculator/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/ai-council/index.html b/r/ai-council/index.html index dc348088..7580d8f5 100644 --- a/r/ai-council/index.html +++ b/r/ai-council/index.html @@ -101,6 +101,7 @@

Related resources

+ diff --git a/r/analyze-sharepointrisk/index.html b/r/analyze-sharepointrisk/index.html index edb8d801..73f9f0aa 100644 --- a/r/analyze-sharepointrisk/index.html +++ b/r/analyze-sharepointrisk/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/autoreply-agent/index.html b/r/autoreply-agent/index.html index a488a91a..d4bd926f 100644 --- a/r/autoreply-agent/index.html +++ b/r/autoreply-agent/index.html @@ -98,6 +98,7 @@

Related resources

+ diff --git a/r/comparedocs/index.html b/r/comparedocs/index.html index ad0a3162..2cda027d 100644 --- a/r/comparedocs/index.html +++ b/r/comparedocs/index.html @@ -101,6 +101,7 @@

Related resources

+ diff --git a/r/copilot-agents-guide/index.html b/r/copilot-agents-guide/index.html index 8f5a8ae5..10d6cddf 100644 --- a/r/copilot-agents-guide/index.html +++ b/r/copilot-agents-guide/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/copilot-analytics-reports-navigator/index.html b/r/copilot-analytics-reports-navigator/index.html index 4a5baa6a..40e6dc30 100644 --- a/r/copilot-analytics-reports-navigator/index.html +++ b/r/copilot-analytics-reports-navigator/index.html @@ -103,6 +103,7 @@

Related resources

+ diff --git a/r/copilot-interaction-report/index.html b/r/copilot-interaction-report/index.html index 2b7c4017..d423da61 100644 --- a/r/copilot-interaction-report/index.html +++ b/r/copilot-interaction-report/index.html @@ -98,6 +98,7 @@

Related resources

+ diff --git a/r/copilot-studio-workflow/index.html b/r/copilot-studio-workflow/index.html index a400f572..ad43ea78 100644 --- a/r/copilot-studio-workflow/index.html +++ b/r/copilot-studio-workflow/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/create-engagecommunities/index.html b/r/create-engagecommunities/index.html index 747a9817..9dc5031c 100644 --- a/r/create-engagecommunities/index.html +++ b/r/create-engagecommunities/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/deepresearch/index.html b/r/deepresearch/index.html index ef314f19..9d812324 100644 --- a/r/deepresearch/index.html +++ b/r/deepresearch/index.html @@ -101,6 +101,7 @@

Related resources

+ diff --git a/r/delete-allcommunityposts/index.html b/r/delete-allcommunityposts/index.html index 06ed7250..7ac094c6 100644 --- a/r/delete-allcommunityposts/index.html +++ b/r/delete-allcommunityposts/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/delete-engagegroups/index.html b/r/delete-engagegroups/index.html index fc1b51a6..ed02036c 100644 --- a/r/delete-engagegroups/index.html +++ b/r/delete-engagegroups/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/delete-engageusers/index.html b/r/delete-engageusers/index.html index baa9ec6a..96bb55d2 100644 --- a/r/delete-engageusers/index.html +++ b/r/delete-engageusers/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/disable-insightsheadspace/index.html b/r/disable-insightsheadspace/index.html index 78fb0fc1..91f93b95 100644 --- a/r/disable-insightsheadspace/index.html +++ b/r/disable-insightsheadspace/index.html @@ -102,6 +102,7 @@

Related resources

+ diff --git a/r/ess-servicenow-itsm-get-request-details/index.html b/r/ess-servicenow-itsm-get-request-details/index.html index f6bb7933..9a25e5b2 100644 --- a/r/ess-servicenow-itsm-get-request-details/index.html +++ b/r/ess-servicenow-itsm-get-request-details/index.html @@ -101,6 +101,7 @@

Related resources

+ diff --git a/r/ess-servicenow-itsm-get-user-requests/index.html b/r/ess-servicenow-itsm-get-user-requests/index.html index 6f628fa6..55df9af6 100644 --- a/r/ess-servicenow-itsm-get-user-requests/index.html +++ b/r/ess-servicenow-itsm-get-user-requests/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/export-copilotinteractions/index.html b/r/export-copilotinteractions/index.html index 6f5aa0c7..e9b72423 100644 --- a/r/export-copilotinteractions/index.html +++ b/r/export-copilotinteractions/index.html @@ -101,6 +101,7 @@

Related resources

+ diff --git a/r/export-engagefiles/index.html b/r/export-engagefiles/index.html index cb4d5049..5c23647b 100644 --- a/r/export-engagefiles/index.html +++ b/r/export-engagefiles/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/export-engagenetworkdata/index.html b/r/export-engagenetworkdata/index.html index a6b4680c..b2a20ec4 100644 --- a/r/export-engagenetworkdata/index.html +++ b/r/export-engagenetworkdata/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/export-m365copilotreports/index.html b/r/export-m365copilotreports/index.html index 4b26815a..7af7a80e 100644 --- a/r/export-m365copilotreports/index.html +++ b/r/export-m365copilotreports/index.html @@ -99,6 +99,7 @@

Related resources

+ diff --git a/r/export-simpleplacesvisual/index.html b/r/export-simpleplacesvisual/index.html index c4c67419..7964994a 100644 --- a/r/export-simpleplacesvisual/index.html +++ b/r/export-simpleplacesvisual/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/find-mailboxdelegates/index.html b/r/find-mailboxdelegates/index.html index 4c7498d0..e2534f37 100644 --- a/r/find-mailboxdelegates/index.html +++ b/r/find-mailboxdelegates/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/get-aadadminrolemembers/index.html b/r/get-aadadminrolemembers/index.html index 709b2eed..4b956230 100644 --- a/r/get-aadadminrolemembers/index.html +++ b/r/get-aadadminrolemembers/index.html @@ -102,6 +102,7 @@

Related resources

+ diff --git a/r/get-allinsightslicensedusers/index.html b/r/get-allinsightslicensedusers/index.html index 3cc2b79d..604d20ea 100644 --- a/r/get-allinsightslicensedusers/index.html +++ b/r/get-allinsightslicensedusers/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/get-engagegroupinfo/index.html b/r/get-engagegroupinfo/index.html index defb9688..c970c92f 100644 --- a/r/get-engagegroupinfo/index.html +++ b/r/get-engagegroupinfo/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/get-groupsmembersmanagers/index.html b/r/get-groupsmembersmanagers/index.html index caf04d62..900a4c55 100644 --- a/r/get-groupsmembersmanagers/index.html +++ b/r/get-groupsmembersmanagers/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/get-licenseusage/index.html b/r/get-licenseusage/index.html index 9cc6eb05..0d20d13e 100644 --- a/r/get-licenseusage/index.html +++ b/r/get-licenseusage/index.html @@ -103,6 +103,7 @@

Related resources

+ diff --git a/r/get-m365copilotreadiness/index.html b/r/get-m365copilotreadiness/index.html index 4eba0054..2dcd4f2f 100644 --- a/r/get-m365copilotreadiness/index.html +++ b/r/get-m365copilotreadiness/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/get-recurringteamsmeetings/index.html b/r/get-recurringteamsmeetings/index.html index 9a58104a..4a4628be 100644 --- a/r/get-recurringteamsmeetings/index.html +++ b/r/get-recurringteamsmeetings/index.html @@ -99,6 +99,7 @@

Related resources

+ diff --git a/r/get-siteinventory/index.html b/r/get-siteinventory/index.html index 10d3b2a5..92cb1657 100644 --- a/r/get-siteinventory/index.html +++ b/r/get-siteinventory/index.html @@ -102,6 +102,7 @@

Related resources

+ diff --git a/r/get-teamsuseractivityreport/index.html b/r/get-teamsuseractivityreport/index.html index 94c29459..2309cd96 100644 --- a/r/get-teamsuseractivityreport/index.html +++ b/r/get-teamsuseractivityreport/index.html @@ -98,6 +98,7 @@

Related resources

+ diff --git a/r/get-yammersitesize/index.html b/r/get-yammersitesize/index.html index 5c7b9432..4084f7a0 100644 --- a/r/get-yammersitesize/index.html +++ b/r/get-yammersitesize/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/managersimulator/index.html b/r/managersimulator/index.html index 92b8a9c0..cc5084e1 100644 --- a/r/managersimulator/index.html +++ b/r/managersimulator/index.html @@ -101,6 +101,7 @@

Related resources

+ diff --git a/r/microsoft-agent-365-customer-implementation-guide/index.html b/r/microsoft-agent-365-customer-implementation-guide/index.html index 3089a56d..856a0c64 100644 --- a/r/microsoft-agent-365-customer-implementation-guide/index.html +++ b/r/microsoft-agent-365-customer-implementation-guide/index.html @@ -103,6 +103,7 @@

Related resources

+ diff --git a/r/microsoft-agent-365-technical-advisory/index.html b/r/microsoft-agent-365-technical-advisory/index.html index 0aa79bcc..1895eebf 100644 --- a/r/microsoft-agent-365-technical-advisory/index.html +++ b/r/microsoft-agent-365-technical-advisory/index.html @@ -103,6 +103,7 @@

Related resources

+ diff --git a/r/migrate-copilotstudioagents/index.html b/r/migrate-copilotstudioagents/index.html index 7d51915f..e6ee68bb 100644 --- a/r/migrate-copilotstudioagents/index.html +++ b/r/migrate-copilotstudioagents/index.html @@ -98,6 +98,7 @@

Related resources

+ diff --git a/r/omniagent/index.html b/r/omniagent/index.html index adf1661e..3b3cda45 100644 --- a/r/omniagent/index.html +++ b/r/omniagent/index.html @@ -101,6 +101,7 @@

Related resources

+ diff --git a/r/powerclaw-agent/index.html b/r/powerclaw-agent/index.html index 6dfa46cb..56a6e7cc 100644 --- a/r/powerclaw-agent/index.html +++ b/r/powerclaw-agent/index.html @@ -99,6 +99,7 @@

Related resources

+ diff --git a/r/preflight-onedrive/index.html b/r/preflight-onedrive/index.html index 79da75c0..48c0201f 100644 --- a/r/preflight-onedrive/index.html +++ b/r/preflight-onedrive/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/productquote-agent/index.html b/r/productquote-agent/index.html index 6f37507d..bc4703fd 100644 --- a/r/productquote-agent/index.html +++ b/r/productquote-agent/index.html @@ -98,6 +98,7 @@

Related resources

+ diff --git a/r/purview-audit-copilot-report/index.html b/r/purview-audit-copilot-report/index.html index 41304519..96b7e429 100644 --- a/r/purview-audit-copilot-report/index.html +++ b/r/purview-audit-copilot-report/index.html @@ -99,6 +99,7 @@

Related resources

+ diff --git a/r/reasoningagent/index.html b/r/reasoningagent/index.html index 7e292e84..086d1fc6 100644 --- a/r/reasoningagent/index.html +++ b/r/reasoningagent/index.html @@ -101,6 +101,7 @@

Related resources

+ diff --git a/r/researcher-automation-opportunities/index.html b/r/researcher-automation-opportunities/index.html index 1bb9e2e8..b3fdda6b 100644 --- a/r/researcher-automation-opportunities/index.html +++ b/r/researcher-automation-opportunities/index.html @@ -100,6 +100,7 @@

Related resources

+ diff --git a/r/simplegraph/index.html b/r/simplegraph/index.html index cee0e707..a7e50446 100644 --- a/r/simplegraph/index.html +++ b/r/simplegraph/index.html @@ -101,6 +101,7 @@

Related resources

+ diff --git a/r/teams-audio-conferencing-snippets/index.html b/r/teams-audio-conferencing-snippets/index.html index ec43cdc8..023a86ff 100644 --- a/r/teams-audio-conferencing-snippets/index.html +++ b/r/teams-audio-conferencing-snippets/index.html @@ -107,6 +107,7 @@

Related resources

+ diff --git a/r/teams-phone-system-snippets/index.html b/r/teams-phone-system-snippets/index.html index fc0f9887..08aac948 100644 --- a/r/teams-phone-system-snippets/index.html +++ b/r/teams-phone-system-snippets/index.html @@ -106,6 +106,7 @@

Related resources

+ diff --git a/r/teams-phone-user-migration/index.html b/r/teams-phone-user-migration/index.html index 68258a46..89573c18 100644 --- a/r/teams-phone-user-migration/index.html +++ b/r/teams-phone-user-migration/index.html @@ -98,6 +98,7 @@

Related resources

+ diff --git a/r/teams-rooms-snippets/index.html b/r/teams-rooms-snippets/index.html index 51a74c42..cc1f0f40 100644 --- a/r/teams-rooms-snippets/index.html +++ b/r/teams-rooms-snippets/index.html @@ -104,6 +104,7 @@

Related resources

+ diff --git a/r/teams-upgrade-snippets/index.html b/r/teams-upgrade-snippets/index.html index 20f6c2fc..17a233a0 100644 --- a/r/teams-upgrade-snippets/index.html +++ b/r/teams-upgrade-snippets/index.html @@ -106,6 +106,7 @@

Related resources

+ diff --git a/r/update-vivaengagelicensingtoggle/index.html b/r/update-vivaengagelicensingtoggle/index.html index f62147c3..7cdb682b 100644 --- a/r/update-vivaengagelicensingtoggle/index.html +++ b/r/update-vivaengagelicensingtoggle/index.html @@ -101,6 +101,7 @@

Related resources

+ diff --git a/r/vibewriting-agent/index.html b/r/vibewriting-agent/index.html index 65329293..fc35bf4b 100644 --- a/r/vibewriting-agent/index.html +++ b/r/vibewriting-agent/index.html @@ -98,6 +98,7 @@

Related resources

+ diff --git a/r/viva-engage-community-creation-app/index.html b/r/viva-engage-community-creation-app/index.html index 91c98b6b..e423ab14 100644 --- a/r/viva-engage-community-creation-app/index.html +++ b/r/viva-engage-community-creation-app/index.html @@ -105,6 +105,7 @@

Related resources

+ diff --git a/r/viva-insights-copilot-dashboard/index.html b/r/viva-insights-copilot-dashboard/index.html index 51fd689f..201cfa71 100644 --- a/r/viva-insights-copilot-dashboard/index.html +++ b/r/viva-insights-copilot-dashboard/index.html @@ -98,6 +98,7 @@

Related resources

+ diff --git a/tools/catalog-build/site-pages.js b/tools/catalog-build/site-pages.js index 7436cd5f..f46e7d5f 100644 --- a/tools/catalog-build/site-pages.js +++ b/tools/catalog-build/site-pages.js @@ -323,6 +323,7 @@ ${archived ? '\n

This resource is archived and kept for ref ${siteFooter('../../')} + diff --git a/tools/catalog-build/test-site-output.js b/tools/catalog-build/test-site-output.js index 2d90165f..d51f3d1f 100644 --- a/tools/catalog-build/test-site-output.js +++ b/tools/catalog-build/test-site-output.js @@ -1,6 +1,7 @@ import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; +import vm from 'node:vm'; import { renderDownloadPage, renderFeed, renderMarkdown, renderResourcePage, renderSitemap } from './site-pages.js'; // Guards the static resource pages (escaping, status handling, sitemap/feed) and the @@ -142,4 +143,44 @@ assert(/\.row\[hidden\]\s*\{\s*display:\s*none/.test(resourceCss), 'A hidden det const viewsTip = source => /VIEWS_TIP = "([^"]+)"/.exec(source)?.[1]; assert(viewsTip(indexSource) && viewsTip(indexSource) === viewsTip(resourcePageSource), 'The catalog and the resource pages must explain view counts with the same text.'); +// 11. The site reads the latest view counts from the staged stats on raw.githubusercontent.com, because +// publishing them to master needs a pull request that Actions cannot open. Only the counts are taken, +// and the committed file stays the fallback. +const liveStatsSource = readFileSync(join(assetsDirectory, 'live-stats.js'), 'utf8'); +const workflowSource = readFileSync(join(toolDirectory, '..', '..', '.github', 'workflows', 'traffic-stats.yml'), 'utf8'); +const stagingBranch = /STAGING_BRANCH:\s*(\S+)/.exec(workflowSource)?.[1]; +assert(stagingBranch, 'The traffic workflow must name its staging branch.'); +const liveContext = { setTimeout, clearTimeout }; +liveContext.window = liveContext; +vm.runInNewContext(liveStatsSource, liveContext); +const liveStats = liveContext.FastTrackLiveStats; +assert(liveStats.url === `https://raw.githubusercontent.com/microsoft/FastTrack/${stagingBranch}/resource-stats.json`, 'The live stats URL must point at resource-stats.json on the workflow\'s staging branch.'); + +const localDoc = { generatedAt: '2026-11-02T19:13:42.161Z', sources: { views: 'clarity+github-traffic' }, resources: { a: { views: 10 } } }; +const liveDoc = (overrides = {}) => ({ + generatedAt: '2026-11-03T06:04:00.000Z', sources: { views: 'clarity+github-traffic' }, + resources: { a: { views: 12, upvotes: 99, discussion: { url: 'https://example.com' } }, b: { views: 3.4 }, bad: { views: 'x' }, neg: { views: -1 } }, + ...overrides +}); +assert(JSON.stringify(liveStats.pick(localDoc, liveDoc())) === '{"a":12,"b":3}', 'Newer staged stats must supply rounded view counts only, and skip invalid ones.'); +assert(liveStats.pick(localDoc, liveDoc({ generatedAt: localDoc.generatedAt }))?.a === 12, 'Staged stats rendered at the same moment as the local file must be accepted.'); +assert(liveStats.pick(localDoc, liveDoc({ generatedAt: '2026-11-02T06:04:00.000Z' })) === null, 'Staged stats older than the local file must be ignored.'); +assert(liveStats.pick(localDoc, liveDoc({ sources: { views: 'clarity' } })) === null, 'Staged stats counted by a different method must be ignored.'); +assert(liveStats.pick(localDoc, liveDoc({ generatedAt: 'soon' })) === null && liveStats.pick({ ...localDoc, generatedAt: undefined }, liveDoc()) === null, 'A missing or invalid timestamp must not be trusted.'); +assert(liveStats.pick(localDoc, null) === null && liveStats.pick(localDoc, { resources: 'x' }) === null && liveStats.pick(null, liveDoc()) === null && liveStats.pick(localDoc, liveDoc({ resources: {} })) === null, 'Unusable staged stats must fall back to the local file.'); + +liveContext.fetch = () => Promise.resolve({ ok: true, json: () => Promise.resolve(liveDoc()) }); +assert((await liveStats.fetch())?.resources?.a?.views === 12, 'A readable staged file must be returned.'); +liveContext.fetch = () => Promise.resolve({ ok: false, status: 404, json: () => Promise.resolve({}) }); +assert(await liveStats.fetch() === null, 'An HTTP error must resolve to null, not throw.'); +liveContext.fetch = () => Promise.reject(new Error('offline')); +assert(await liveStats.fetch() === null, 'A network failure must resolve to null, not throw.'); + +assert(indexSource.includes('') && indexSource.indexOf('assets/live-stats.js') < indexSource.indexOf('// --- DATASET ---'), 'The catalog must load live-stats.js before its main script.'); +assert(/window\.FastTrackLiveStats\.pick\(statsData, live\)/.test(indexSource) && /STATS\[slug\] = \{ \.\.\.\(STATS\[slug\] \|\| \{\}\), views: liveViews\[slug\] \}/.test(indexSource), 'The catalog must take only the view counts from the staged stats.'); +assert(page.includes('src="../../assets/live-stats.js" defer') && page.indexOf('live-stats.js') < page.indexOf('resource-page.js'), 'A resource page must load live-stats.js before resource-page.js.'); +assert(!counted.includes('live-stats.js'), 'A download page must not load the live stats.'); +assert(/FastTrackLiveStats\.pick\(data, results\[1\]\)/.test(resourcePageSource), 'The resource page script must prefer the staged view counts.'); +assert(privacyScript.includes('raw.githubusercontent.com'), 'The privacy notice must disclose the request to raw.githubusercontent.com.'); + console.log('Static resource pages and landing-page guards are valid.'); diff --git a/traffic-data/README.md b/traffic-data/README.md index 33d1fdb7..f0b49333 100644 --- a/traffic-data/README.md +++ b/traffic-data/README.md @@ -19,6 +19,17 @@ Both are needed. Clarity never sees a visit that goes straight to GitHub, and th The count is a minimum, because some days were never captured: GitHub traffic from 2026-07-01 to 2026-10-13, and Clarity from 2026-07-28 to 2026-09-06. Visitors who block analytics are not counted either. +## How the live site stays current + +Daily collection writes `resource-stats.json` to the `automation/traffic-stats-staging` branch. Publishing it to `master` takes the weekly pull request, which GitHub Actions cannot open under the organization's policy, so the committed copy is only as fresh as the last merged weekly PR. + +So that the numbers on the site do not wait for that PR, the catalog and the resource pages also read the staged file straight from `raw.githubusercontent.com` (`assets/live-stats.js`). Only the view counts are taken from it. The committed `resource-stats.json` stays the source for everything else and the fallback in every other case: the staged file cannot be read within 1.5 seconds, it is older than the committed file, or it was counted by a different method (`sources.views`). A page therefore never shows a wrong count because of the staged file, only a less fresh one. + +- The staged file is cached for five minutes by raw.githubusercontent.com, so a daily run shows up on the site within a few minutes of finishing. +- If the staging branch is renamed, update `LIVE_STATS_URL` in `assets/live-stats.js`. `npm run check` fails until it matches `STAGING_BRANCH` in the workflow. +- If you change how views are counted, change `sources.views` in `build-stats.js` too. Until the next daily run re-renders the staged file with the new method, the site uses the committed file. +- The weekly pull request is still worth merging: it keeps the committed copy, which is the fallback, current. + Data is collected daily at 06:00 UTC via GitHub Actions. Each Friday at 08:00 `America/Chicago` (including daylight saving time), the workflow updates one review-required pull request from the Friday-only `automation/traffic-stats-weekly` From baf8f15177b81a64f23e712616593dc5a105c6bd Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Mon, 2 Nov 2026 13:56:01 -0600 Subject: [PATCH 2/2] Check footer links and the privacy host with exact matches CodeQL flags substring tests on URLs (js/incomplete-url-substring-sanitization). These are test assertions, not sanitization, but a substring test on a URL also proves little about the host it names. Compare against the exact set of hrefs on each page, and check the privacy notice against the set of hostnames it names, using the host of the live stats URL instead of a hard-coded string. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tools/catalog-build/test-site-output.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/catalog-build/test-site-output.js b/tools/catalog-build/test-site-output.js index d51f3d1f..0bee2dea 100644 --- a/tools/catalog-build/test-site-output.js +++ b/tools/catalog-build/test-site-output.js @@ -95,8 +95,10 @@ const footerPages = { 'the cost calculator': [costToolSource, '../../assets/'] }; for (const [name, [source, assets]] of Object.entries(footerPages)) { - assert(source.includes('https://www.microsoft.com/en-us/legal/terms-of-use'), `${name} must link the Microsoft Terms and Conditions.`); - assert(source.includes('https://privacy.microsoft.com/en-us/privacystatement'), `${name} must link the Microsoft Privacy Statement.`); + // Exact-match lookups: a substring test on a URL says nothing about which host it names. + const hrefs = new Set([...source.matchAll(/href="([^"]*)"/g)].map(match => match[1])); + assert(hrefs.has('https://www.microsoft.com/en-us/legal/terms-of-use'), `${name} must link the Microsoft Terms and Conditions.`); + assert(hrefs.has('https://privacy.microsoft.com/en-us/privacystatement'), `${name} must link the Microsoft Privacy Statement.`); assert(/