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.
</p>
<ScriptSetupBlock
site={{ domain: site.domain, name: site.name }}
site={{ domain: site.domain, name: site.name, script_features: site.script_features }}
showFrameworkPicker
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">

View File

@@ -37,6 +37,7 @@ type FeatureKey = (typeof FEATURES)[number]['key'] | 'frustration'
export interface ScriptSetupBlockSite {
domain: string
name?: string
script_features?: Record<string, unknown>
}
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<string, unknown>) => void
/** Show framework picker. Default true. */
showFrameworkPicker?: boolean
/** Optional class for the root wrapper. */
className?: string
}
export default function ScriptSetupBlock({
site,
onScriptCopy,
showFrameworkPicker = true,
className = '',
}: ScriptSetupBlockProps) {
const [features, setFeatures] = useState<Record<FeatureKey, boolean>>({
const DEFAULT_FEATURES: Record<FeatureKey, boolean> = {
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<Record<FeatureKey, boolean>>({
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({
<Select
variant="input"
value={storage}
onChange={setStorage}
onChange={(v: string) => { setStorage(v); onFeaturesChange?.({ ...features, storage: v, ttl }) }}
options={STORAGE_OPTIONS}
/>
</div>
@@ -213,7 +230,7 @@ export default function ScriptSetupBlock({
<Select
variant="input"
value={ttl}
onChange={setTtl}
onChange={(v: string) => { setTtl(v); onFeaturesChange?.({ ...features, storage, ttl: v }) }}
options={TTL_OPTIONS}
/>
</div>

View File

@@ -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<string, unknown>
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<string, unknown>
// Hide unknown locations from stats
hide_unknown_locations?: boolean
// Data retention (months); 0 = keep forever