From c401c7351bac4bfbcf7f9fb85382f9f67bbb7ebf Mon Sep 17 00:00:00 2001 From: Jingxi-Polymaker Date: Mon, 7 Sep 2026 14:14:43 +0800 Subject: [PATCH] feat: track website usage with PostHog browser SDK --- README.md | 18 ++++++ analytics.js | 47 +++++++++++++++ app.js | 37 +++++++++--- index.html | 1 + scripts/website-analytics.test.mjs | 94 ++++++++++++++++++++++++++++++ 5 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 analytics.js create mode 100644 scripts/website-analytics.test.mjs diff --git a/README.md b/README.md index 654c2374..5710370f 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,24 @@ and `seo_injected` events under `github-actions`. Generated files stay in the temporary runner; this workflow does not commit changes or publish the site. Pull request tests and preset-update jobs continue to run without analytics. +### Website usage analytics + +`analytics.js` loads the PostHog browser SDK on `presets.polymaker.com` using the +public write-only project token. Local and preview hosts do not send PostHog +events. This is separate from the Node build SDK and GitHub Actions secrets. + +PostHog records pageviews (`$pageview`), page leaves, automatic interactions +(`$autocapture`), and `filter_changed` with the selected filter and value. +The existing successful-download handlers send `download_single`, +`download_selected`, `download_bundle`, and `download_bundle_batch` to both +Google Analytics and PostHog, including material/printer/slicer metadata where +available. A download event means the browser was handed the generated file; +it cannot confirm the user saved or imported it. Session recording is disabled. + +View these events in PostHog **Activity**, or use **Web analytics** for visits +and **Product analytics** for filter and download usage. Ad blockers or a +visitor's analytics opt-out can prevent events from arriving. + ## 🔗 Links - **Download Page**: [https://presets.polymaker.com](https://presets.polymaker.com) diff --git a/analytics.js b/analytics.js new file mode 100644 index 00000000..80f8cc91 --- /dev/null +++ b/analytics.js @@ -0,0 +1,47 @@ +// Browser analytics. This is the public, write-only PostHog project token. +// The Node build client is configured separately through GitHub Actions secrets. +(function () { + if (window.location.hostname !== 'presets.polymaker.com') return; + if (window.posthog && (window.posthog.__loaded || window.posthog.__SV)) return; + + // PostHog's asynchronous snippet queues early interactions while the SDK loads. + // https://posthog.com/docs/libraries/js + var posthog = window.posthog = []; + posthog._i = []; + posthog.__SV = 1; + posthog.people = []; + posthog.init = function (token, config) { + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.crossOrigin = 'anonymous'; + script.async = true; + script.src = config.api_host.replace('.i.posthog.com', '-assets.i.posthog.com') + '/static/array.js'; + var firstScript = document.getElementsByTagName('script')[0]; + firstScript.parentNode.insertBefore(script, firstScript); + + function queueMethod(name) { + posthog[name] = function () { + posthog.push([name].concat(Array.prototype.slice.call(arguments))); + }; + } + var methods = ['capture', 'register', 'identify', 'reset', 'set_config', 'opt_in_capturing', 'opt_out_capturing']; + for (var i = 0; i < methods.length; i++) queueMethod(methods[i]); + posthog._i.push([token, config, 'posthog']); + }; + + window.posthog.init('phc_CdvtTDKLvkXysqQN43CbH674J2p9r9LoEZBGEHbrfsYi', { + api_host: 'https://us.i.posthog.com', + ui_host: 'https://us.posthog.com', + defaults: '2026-05-30', + capture_pageview: true, + capture_pageleave: true, + autocapture: true, + person_profiles: 'identified_only', + cross_subdomain_cookie: false, + disable_session_recording: true, + disable_surveys: true, + loaded: function (posthog) { + posthog.register({ site: 'polymaker-presets' }); + } + }); +}()); diff --git a/app.js b/app.js index 5ff57cb3..c8a7c4db 100644 --- a/app.js +++ b/app.js @@ -23,14 +23,23 @@ function getGaValue(value) { return value; } -function trackGaEvent(eventName, params) { +function trackUsageEvent(eventName, params) { if (typeof window === 'undefined') return; - if (typeof window.gtag !== 'function') return; + // Each provider is independent: blocked analytics must never break downloads. try { - window.gtag('event', eventName, params || {}); + if (typeof window.gtag === 'function') { + window.gtag('event', eventName, params || {}); + } } catch (e) { console.warn('GA event failed:', eventName, e); } + try { + if (window.posthog && typeof window.posthog.capture === 'function') { + window.posthog.capture(eventName, params || {}); + } + } catch (e) { + console.warn('PostHog event failed:', eventName, e); + } } function applyTheme(theme) { @@ -537,7 +546,7 @@ function init() { document.body.removeChild(a); if (originalPresets && originalPresets.length) { var firstPreset = originalPresets[0]; - trackGaEvent('download_bundle', { + trackUsageEvent('download_bundle', { file_type: 'bbsflmt', slicer: getGaValue(firstPreset.slicer || 'BambuStudio'), material: getGaValue(firstPreset.material), @@ -776,6 +785,15 @@ function init() { } render(); + trackUsageEvent('filter_changed', { + filter: name, + value: value || 'all', + series: filterState.series || 'all', + brand: filterState.brand || 'all', + model: filterState.model || 'all', + slicer: filterState.slicer || 'all', + strict: filterState.strict + }); }); } @@ -1046,7 +1064,7 @@ function init() { a.click(); document.body.removeChild(a); var filters = getEffectiveFilters(); - trackGaEvent('download_selected', { + trackUsageEvent('download_selected', { file_type: 'zip', selected_count: presetIds.length, slicer: getGaValue(filters.effectiveSlicer || filterState.slicer), @@ -1269,7 +1287,7 @@ function init() { document.body.appendChild(a); a.click(); document.body.removeChild(a); - trackGaEvent('download_bundle_batch', { + trackUsageEvent('download_bundle_batch', { file_type: 'zip', material_count: materials.length, slicer: 'BambuStudio' @@ -1516,6 +1534,7 @@ function init() { strictCheckbox.addEventListener('change', function () { filterState.strict = strictCheckbox.checked; render(); + trackUsageEvent('filter_changed', { filter: 'strict', value: filterState.strict }); }); } @@ -1823,8 +1842,8 @@ function init() { a.click(); document.body.removeChild(a); var rowContext = getRowGaContext(directJsonLink.closest('tr')); - trackGaEvent('download_single', { - file_type: 'json', + trackUsageEvent('download_single', { + file_type: /\.ini$/i.test(djFilename) ? 'ini' : 'json', slicer: rowContext.slicer, material: rowContext.material, brand: rowContext.brand, @@ -1879,7 +1898,7 @@ function init() { a.click(); document.body.removeChild(a); var rowContext = getRowGaContext(bambuJsonLink.closest('tr'), 'BambuStudio'); - trackGaEvent('download_single', { + trackUsageEvent('download_single', { file_type: 'json', slicer: rowContext.slicer, material: getGaValue(bjMaterial || rowContext.material), diff --git a/index.html b/index.html index 49bc5bba..f19806fa 100644 --- a/index.html +++ b/index.html @@ -17,6 +17,7 @@ +