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.
This commit is contained in:
Usman Baig
2026-03-22 15:31:45 +01:00
parent e626350f14
commit b7e92abb40
3 changed files with 38 additions and 11 deletions

View File

@@ -809,9 +809,15 @@ export default function SiteSettingsPage() {
Add this script to your website to start tracking visitors. Choose your framework for setup instructions. Add this script to your website to start tracking visitors. Choose your framework for setup instructions.
</p> </p>
<ScriptSetupBlock <ScriptSetupBlock
site={{ domain: site.domain, name: site.name }} site={{ domain: site.domain, name: site.name, script_features: site.script_features }}
showFrameworkPicker showFrameworkPicker
className="mb-4" className="mb-4"
onFeaturesChange={async (features) => {
try {
await updateSite(siteId, { name: site.name, script_features: features })
mutateSite()
} catch { /* silent — not critical */ }
}}
/> />
<div className="flex items-center gap-4 mt-4"> <div className="flex items-center gap-4 mt-4">

View File

@@ -37,6 +37,7 @@ type FeatureKey = (typeof FEATURES)[number]['key'] | 'frustration'
export interface ScriptSetupBlockSite { export interface ScriptSetupBlockSite {
domain: string domain: string
name?: string name?: string
script_features?: Record<string, unknown>
} }
interface ScriptSetupBlockProps { interface ScriptSetupBlockProps {
@@ -44,27 +45,39 @@ interface ScriptSetupBlockProps {
site: ScriptSetupBlockSite site: ScriptSetupBlockSite
/** Called when user copies the script (e.g. for analytics). */ /** Called when user copies the script (e.g. for analytics). */
onScriptCopy?: () => void onScriptCopy?: () => void
/** Called when features change so the parent can save to backend. */
onFeaturesChange?: (features: Record<string, unknown>) => void
/** Show framework picker. Default true. */ /** Show framework picker. Default true. */
showFrameworkPicker?: boolean showFrameworkPicker?: boolean
/** Optional class for the root wrapper. */ /** Optional class for the root wrapper. */
className?: string className?: string
} }
const DEFAULT_FEATURES: Record<FeatureKey, boolean> = {
scroll: true,
'404': true,
outbound: true,
downloads: true,
frustration: false,
}
export default function ScriptSetupBlock({ export default function ScriptSetupBlock({
site, site,
onScriptCopy, onScriptCopy,
onFeaturesChange,
showFrameworkPicker = true, showFrameworkPicker = true,
className = '', className = '',
}: ScriptSetupBlockProps) { }: ScriptSetupBlockProps) {
const sf = site.script_features || {}
const [features, setFeatures] = useState<Record<FeatureKey, boolean>>({ const [features, setFeatures] = useState<Record<FeatureKey, boolean>>({
scroll: true, scroll: sf.scroll != null ? Boolean(sf.scroll) : DEFAULT_FEATURES.scroll,
'404': true, '404': sf['404'] != null ? Boolean(sf['404']) : DEFAULT_FEATURES['404'],
outbound: true, outbound: sf.outbound != null ? Boolean(sf.outbound) : DEFAULT_FEATURES.outbound,
downloads: true, downloads: sf.downloads != null ? Boolean(sf.downloads) : DEFAULT_FEATURES.downloads,
frustration: false, frustration: sf.frustration != null ? Boolean(sf.frustration) : DEFAULT_FEATURES.frustration,
}) })
const [storage, setStorage] = useState('local') const [storage, setStorage] = useState(typeof sf.storage === 'string' ? sf.storage : 'local')
const [ttl, setTtl] = useState('24') const [ttl, setTtl] = useState(typeof sf.ttl === 'string' ? sf.ttl : '24')
const [framework, setFramework] = useState('') const [framework, setFramework] = useState('')
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
@@ -97,7 +110,11 @@ export default function ScriptSetupBlock({
}, [scriptSnippet, onScriptCopy]) }, [scriptSnippet, onScriptCopy])
const toggleFeature = (key: FeatureKey) => { 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 const selectedIntegration = framework ? getIntegration(framework) : null
@@ -201,7 +218,7 @@ export default function ScriptSetupBlock({
<Select <Select
variant="input" variant="input"
value={storage} value={storage}
onChange={setStorage} onChange={(v: string) => { setStorage(v); onFeaturesChange?.({ ...features, storage: v, ttl }) }}
options={STORAGE_OPTIONS} options={STORAGE_OPTIONS}
/> />
</div> </div>
@@ -213,7 +230,7 @@ export default function ScriptSetupBlock({
<Select <Select
variant="input" variant="input"
value={ttl} value={ttl}
onChange={setTtl} onChange={(v: string) => { setTtl(v); onFeaturesChange?.({ ...features, storage, ttl: v }) }}
options={TTL_OPTIONS} options={TTL_OPTIONS}
/> />
</div> </div>

View File

@@ -23,6 +23,8 @@ export interface Site {
hide_unknown_locations?: boolean hide_unknown_locations?: boolean
// Data retention (months); 0 = keep forever // Data retention (months); 0 = keep forever
data_retention_months?: number data_retention_months?: number
// Script feature toggles
script_features?: Record<string, unknown>
is_verified?: boolean is_verified?: boolean
created_at: string created_at: string
updated_at: string updated_at: string
@@ -49,6 +51,8 @@ export interface UpdateSiteRequest {
collect_screen_resolution?: boolean collect_screen_resolution?: boolean
// Bot and noise filtering // Bot and noise filtering
filter_bots?: boolean filter_bots?: boolean
// Script feature toggles
script_features?: Record<string, unknown>
// Hide unknown locations from stats // Hide unknown locations from stats
hide_unknown_locations?: boolean hide_unknown_locations?: boolean
// Data retention (months); 0 = keep forever // Data retention (months); 0 = keep forever