From b7e92abb40e42378b70b7167076a65701aa505d6 Mon Sep 17 00:00:00 2001
From: Usman Baig
Date: Sun, 22 Mar 2026 15:31:45 +0100
Subject: [PATCH] feat: persist script feature toggles to backend
Features (scroll, 404, outbound, downloads, frustration, storage, ttl)
are saved to site.script_features JSONB column on every toggle change.
Values are read from the site object on load.
---
app/sites/[id]/settings/page.tsx | 8 +++++-
components/sites/ScriptSetupBlock.tsx | 37 +++++++++++++++++++--------
lib/api/sites.ts | 4 +++
3 files changed, 38 insertions(+), 11 deletions(-)
diff --git a/app/sites/[id]/settings/page.tsx b/app/sites/[id]/settings/page.tsx
index 6795db4..9d26399 100644
--- a/app/sites/[id]/settings/page.tsx
+++ b/app/sites/[id]/settings/page.tsx
@@ -809,9 +809,15 @@ export default function SiteSettingsPage() {
Add this script to your website to start tracking visitors. Choose your framework for setup instructions.
{
+ try {
+ await updateSite(siteId, { name: site.name, script_features: features })
+ mutateSite()
+ } catch { /* silent — not critical */ }
+ }}
/>
diff --git a/components/sites/ScriptSetupBlock.tsx b/components/sites/ScriptSetupBlock.tsx
index 339fbfb..6648d27 100644
--- a/components/sites/ScriptSetupBlock.tsx
+++ b/components/sites/ScriptSetupBlock.tsx
@@ -37,6 +37,7 @@ type FeatureKey = (typeof FEATURES)[number]['key'] | 'frustration'
export interface ScriptSetupBlockSite {
domain: string
name?: string
+ script_features?: Record
}
interface ScriptSetupBlockProps {
@@ -44,27 +45,39 @@ interface ScriptSetupBlockProps {
site: ScriptSetupBlockSite
/** Called when user copies the script (e.g. for analytics). */
onScriptCopy?: () => void
+ /** Called when features change so the parent can save to backend. */
+ onFeaturesChange?: (features: Record) => void
/** Show framework picker. Default true. */
showFrameworkPicker?: boolean
/** Optional class for the root wrapper. */
className?: string
}
+const DEFAULT_FEATURES: Record = {
+ scroll: true,
+ '404': true,
+ outbound: true,
+ downloads: true,
+ frustration: false,
+}
+
export default function ScriptSetupBlock({
site,
onScriptCopy,
+ onFeaturesChange,
showFrameworkPicker = true,
className = '',
}: ScriptSetupBlockProps) {
+ const sf = site.script_features || {}
const [features, setFeatures] = useState>({
- scroll: true,
- '404': true,
- outbound: true,
- downloads: true,
- frustration: false,
+ scroll: sf.scroll != null ? Boolean(sf.scroll) : DEFAULT_FEATURES.scroll,
+ '404': sf['404'] != null ? Boolean(sf['404']) : DEFAULT_FEATURES['404'],
+ outbound: sf.outbound != null ? Boolean(sf.outbound) : DEFAULT_FEATURES.outbound,
+ downloads: sf.downloads != null ? Boolean(sf.downloads) : DEFAULT_FEATURES.downloads,
+ frustration: sf.frustration != null ? Boolean(sf.frustration) : DEFAULT_FEATURES.frustration,
})
- const [storage, setStorage] = useState('local')
- const [ttl, setTtl] = useState('24')
+ const [storage, setStorage] = useState(typeof sf.storage === 'string' ? sf.storage : 'local')
+ const [ttl, setTtl] = useState(typeof sf.ttl === 'string' ? sf.ttl : '24')
const [framework, setFramework] = useState('')
const [copied, setCopied] = useState(false)
@@ -97,7 +110,11 @@ export default function ScriptSetupBlock({
}, [scriptSnippet, onScriptCopy])
const toggleFeature = (key: FeatureKey) => {
- setFeatures((prev) => ({ ...prev, [key]: !prev[key] }))
+ setFeatures((prev) => {
+ const next = { ...prev, [key]: !prev[key] }
+ onFeaturesChange?.({ ...next, storage, ttl })
+ return next
+ })
}
const selectedIntegration = framework ? getIntegration(framework) : null
@@ -201,7 +218,7 @@ export default function ScriptSetupBlock({
{ setStorage(v); onFeaturesChange?.({ ...features, storage: v, ttl }) }}
options={STORAGE_OPTIONS}
/>
@@ -213,7 +230,7 @@ export default function ScriptSetupBlock({
{ setTtl(v); onFeaturesChange?.({ ...features, storage, ttl: v }) }}
options={TTL_OPTIONS}
/>
diff --git a/lib/api/sites.ts b/lib/api/sites.ts
index 7093b57..5cdf284 100644
--- a/lib/api/sites.ts
+++ b/lib/api/sites.ts
@@ -23,6 +23,8 @@ export interface Site {
hide_unknown_locations?: boolean
// Data retention (months); 0 = keep forever
data_retention_months?: number
+ // Script feature toggles
+ script_features?: Record
is_verified?: boolean
created_at: string
updated_at: string
@@ -49,6 +51,8 @@ export interface UpdateSiteRequest {
collect_screen_resolution?: boolean
// Bot and noise filtering
filter_bots?: boolean
+ // Script feature toggles
+ script_features?: Record
// Hide unknown locations from stats
hide_unknown_locations?: boolean
// Data retention (months); 0 = keep forever