1/** 2 * Business-site analytics wrapper (Plausible CE fork). 3 * 4 * Usage: 5 * import { analytics } from './analytics-sdk' 6 * analytics.init({ domain: 'example.com', siteId: '1', endpointBase: 'https://stats.example.com' }) 7 * analytics.page() 8 * analytics.track('generation_start', { tool: 'image_generation', model: 'gpt_image_2' }) 9 * const attrs = analytics.register() // also returns fields for business DB 10 */ 11 12import { ensureFirstTouch, getFirstTouch, registrationAttribution } from './attribution.js' 13import { detectPlatform } from './platform.js' 14import { bindAutoTrack } from './auto-track.js' 15import { createReplay } from './replay.js' 16 17const ALLOWED_EVENTS = new Set([ 18 'button_click', 19 'register_success', 20 'generation_start', 21 'generation_success', 22 'generation_failed', 23 'pricing_view', 24 'checkout_start', 25 'purchase' 26]) 27 28const ALLOWED_PROPS = new Set([ 29 'tool', 30 'model', 31 'locale', 32 'surface', 33 'platform', 34 'name', 35 'reason', 36 'duration_bucket', 37 'plan', 38 'currency', 39 'source', 40 'landing', 41 'medium', 42 'campaign' 43]) 44 45const PROP_RE = /^[a-z0-9][a-z0-9_+-]{0,63}$/ 46const FREE_KEYS = new Set(['landing', 'name']) 47 48let config = { 49 domain: null, 50 siteId: null, 51 endpointBase: '', 52 autoTrack: true, 53 replaySampleRate: 0.1 54} 55 56let unbindAuto = null 57let replayCtrl = null 58 59function isDev() { 60 return typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production' 61} 62 63function sanitizeProps(props = {}) { 64 const out = {} 65 for (const [k, v] of Object.entries(props)) { 66 if (!ALLOWED_PROPS.has(k) && !FREE_KEYS.has(k)) continue 67 if (!PROP_RE.test(k) && !FREE_KEYS.has(k)) continue 68 if (v == null) continue 69 if (/email|password|token|prompt|filename|user_id|authorization/i.test(k)) continue 70 const s = String(v) 71 if (!FREE_KEYS.has(k) && !PROP_RE.test(s) && s.length > 64) continue 72 out[k] = FREE_KEYS.has(k) ? s.slice(0, 200) : s.slice(0, 64) 73 } 74 return out 75} 76 77function plausible() { 78 if (typeof window === 'undefined') return () => {} 79 return ( 80 window.plausible || 81 function () { 82 ;(window.plausible.q = window.plausible.q || []).push(arguments) 83 } 84 ) 85} 86 87function platform() { 88 return detectPlatform() 89} 90 91export const analytics = { 92 init(opts = {}) { 93 config = { ...config, ...opts } 94 ensureFirstTouch() 95 96 if (config.autoTrack && typeof document !== 'undefined') { 97 if (unbindAuto) unbindAuto() 98 unbindAuto = bindAutoTrack((name, props) => this.track(name, props), platform) 99 } 100 101 return this 102 }, 103 104 page() { 105 ensureFirstTouch() 106 plausible()('pageview') 107 }, 108 109 track(eventName, props = {}) { 110 if (!ALLOWED_EVENTS.has(eventName)) { 111 if (isDev()) console.warn('[analytics] unknown event dropped:', eventName) 112 return 113 } 114 const merged = { platform: platform(), ...props } 115 if (!merged.source) { 116 const ft = getFirstTouch() 117 if (ft?.source) merged.source = ft.source 118 } 119 plausible()(eventName, { props: sanitizeProps(merged) }) 120 }, 121 122 /** 123 * Fire register_success + return attribution for business DB insert. 124 * Do NOT send user_id / email to Plausible. 125 */ 126 register(extra = {}) { 127 const p = platform() 128 const ft = getFirstTouch() 129 this.track('register_success', { 130 source: ft.source, 131 platform: p, 132 landing: ft.landing, 133 ...extra 134 }) 135 return registrationAttribution(p) 136 }, 137 138 getFirstTouch, 139 getPlatform: platform, 140 141 /** Start rrweb sampling; no-ops if siteId missing. */ 142 async replay(action = 'start', opts = {}) { 143 if (action === 'stop') { 144 if (replayCtrl) await replayCtrl.stop() 145 return { stopped: true } 146 } 147 if (!config.siteId) {
148 if (isDev()) console.info('[analytics] replay needs init({ siteId })') 149 return { started: false, reason: 'no_site_id' } 150 } 151 replayCtrl = createReplay({ 152 siteId: config.siteId, 153 endpointBase: config.endpointBase, 154 sampleRate: config.replaySampleRate, 155 ...opts 156 }) 157 return replayCtrl.start(opts) 158 } 159} 160 161export default analytics
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.