PageSourceSearch

https://www.freeaiwork.com/analytics-sdk/index.js

js freeaiwork.com collected 2026-10-04 14:09:44 UTC 4,037 bytes, 161 lines download raw bytes

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.